From b7bb193cd5e3b860b3f736aedde3912ec9fe113b Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 16 Jul 2020 09:08:05 -0700 Subject: [PATCH 01/76] [Actions UI] Fixed Connectors edit flyout retains state after being closed (#71911) * [Actions UI] Fixed Connectors edit flyout retains state after being closed * Fixed failing test --- .../connector_edit_flyout.tsx | 13 +++- .../apps/triggers_actions_ui/connectors.ts | 69 ++++++++++--------- 2 files changed, 46 insertions(+), 36 deletions(-) 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 68fd8b65f1a41..52425a616aad4 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 @@ -51,11 +51,20 @@ export const ConnectorEditFlyout = ({ consumer, } = useActionsConnectorsContext(); const canSave = hasSaveActionsCapability(capabilities); - const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]); + const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: { ...initialConnector, secrets: {} }, }); const [isSaving, setIsSaving] = useState(false); + const setConnector = (key: string, value: any) => { + dispatch({ command: { type: 'setConnector' }, payload: { key, value } }); + }; + + const closeFlyout = useCallback(() => { + setEditFlyoutVisibility(false); + setConnector('connector', { ...initialConnector, secrets: {} }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setEditFlyoutVisibility]); if (!editFlyoutVisible) { return null; @@ -213,7 +222,7 @@ export const ConnectorEditFlyout = ({ - + {i18n.translate( 'xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel', { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 810a80c3401ca..86e355988da0b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -63,18 +63,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should edit a connector', async () => { const connectorName = generateUniqueKey(); const updatedConnectorName = `${connectorName}updated`; - - await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - - await testSubjects.click('.slack-card'); - - await testSubjects.setValue('nameInput', connectorName); - - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); - - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); - - await pageObjects.common.closeToast(); + await createConnector(connectorName); await pageObjects.triggersActionsUI.searchConnectors(connectorName); @@ -103,19 +92,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); - it('should delete a connector', async () => { - async function createConnector(connectorName: string) { - await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + it('should reset connector when canceling an edit', async () => { + const connectorName = generateUniqueKey(); + await createConnector(connectorName); + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); - await testSubjects.click('.slack-card'); + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); - await testSubjects.setValue('nameInput', connectorName); + await testSubjects.setValue('nameInput', 'some test name to cancel'); + await testSubjects.click('cancelSaveEditedConnectorButton'); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedConnectorButton"]'); - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); - await pageObjects.common.closeToast(); - } + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + const nameInputAfterCancel = await testSubjects.find('nameInput'); + const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); + expect(textAfterCancel).to.eql(connectorName); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + it('should delete a connector', async () => { const connectorName = generateUniqueKey(); await createConnector(connectorName); @@ -141,19 +142,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should bulk delete connectors', async () => { - async function createConnector(connectorName: string) { - await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - - await testSubjects.click('.slack-card'); - - await testSubjects.setValue('nameInput', connectorName); - - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); - - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); - await pageObjects.common.closeToast(); - } - const connectorName = generateUniqueKey(); await createConnector(connectorName); @@ -208,4 +196,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await testSubjects.exists('saveEditedActionButton')).to.be(false); }); }); + + async function createConnector(connectorName: string) { + await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); + + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + + await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await pageObjects.common.closeToast(); + } }; From c918d2d3552d29a23b5eb441c385d8b7f0b2bfc5 Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 16 Jul 2020 10:34:52 -0600 Subject: [PATCH 02/76] [QA][Code Coverage] Upload a new Team Assignment on every ci run (#71175) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 1 + scripts/load_team_assignment.js | 21 ++++++++ .../docs/team_assignment/README.md | 8 +++ .../team_assignment/security_privleges.png | Bin 0 -> 231895 bytes .../ingest_coverage/constants.js | 1 + .../team_assignment/get_data.js | 31 +++++++++++ .../ingest_coverage/team_assignment/index.js | 48 ++++++++++++++++++ .../ingestion_pipeline_painless.json | 1 + .../team_assignment/update_ingest_pipeline.js | 37 ++++++++++++++ .../shell_scripts/assign_teams.sh | 15 ++++++ vars/kibanaTeamAssign.groovy | 25 +++++++++ 11 files changed, 188 insertions(+) create mode 100644 scripts/load_team_assignment.js create mode 100644 src/dev/code_coverage/docs/team_assignment/README.md create mode 100644 src/dev/code_coverage/docs/team_assignment/security_privleges.png create mode 100644 src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js create mode 100644 src/dev/code_coverage/ingest_coverage/team_assignment/index.js create mode 100644 src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json create mode 100644 src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js create mode 100644 src/dev/code_coverage/shell_scripts/assign_teams.sh create mode 100644 vars/kibanaTeamAssign.groovy diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 3986367d660a1..ebb9c3dc86dd2 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -13,6 +13,7 @@ kibanaPipeline(timeoutMinutes: 240) { workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { catchError { kibanaCoverage.runTests() + kibanaTeamAssign.load('team_assignment', "### Upload Team Assignment JSON") handleIngestion(TIME_STAMP) } handleFail() diff --git a/scripts/load_team_assignment.js b/scripts/load_team_assignment.js new file mode 100644 index 0000000000000..b8f5edc833634 --- /dev/null +++ b/scripts/load_team_assignment.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/code_coverage/ingest_coverage/team_assignment').uploadTeamAssignmentJson(); diff --git a/src/dev/code_coverage/docs/team_assignment/README.md b/src/dev/code_coverage/docs/team_assignment/README.md new file mode 100644 index 0000000000000..3509fb5c2a4fc --- /dev/null +++ b/src/dev/code_coverage/docs/team_assignment/README.md @@ -0,0 +1,8 @@ +# Team Assignment Ingestion Pipeline + +Team assignment will occur once per ci run. +Team assignment uses an ingest pipeline. + +The coverage user has the coverage admin role. + +This role must have the rights depicted below ![Cluster Rights](./security_privleges.png) diff --git a/src/dev/code_coverage/docs/team_assignment/security_privleges.png b/src/dev/code_coverage/docs/team_assignment/security_privleges.png new file mode 100644 index 0000000000000000000000000000000000000000..002774f3ecefaebad90c086026e518df958adcb0 GIT binary patch literal 231895 zcmd?QcQjn>-##jhAVeZa^b|xFz1N6dVi+-6L>;|%MuJF0uhEGz+8~VHl8D}oUPkZC zh&Gtu>^$##-uHLbS<>xvC z1jG;mf@>8w34tx`BgPj51SG^z1qE#t1qEhpS0^i|y(IyG^2g95FJ0XU>W;x?6rIEC zjbd&G|(k#NY#ULmE8Vb1>fT26Pg z`$pX7mF6RKX7$7=A^>-akzT)e#zgRh=7qoit-for_C|MSP-xX28D?z~9BB{%H3b0^ z#cMvwW$w8-V}iS|vdajZxw6Pz+^Bx_#lh+2EfcWKBT9lU1~YMi%#4t|D#Ff-UHO zl$=@h-6SMk3=A3j@g@#M5m-A-N!LhL6u&|NCOJ*BI*~b4xEQhK`*FE6`PpKPX&ush z$JHi1nx-MU8^sU>%Rhw%W3$%5_n4pJzB#gl-eAi6gDef_r z%2I>`Kc%3$`~Jy8A7!eMw;M4JEJ*8RGqnls-3nE3(dJLMvKzXeEnj%AIAkk_DZ6(_ zXoFhtmQ+YVw$BFHA@Pgv7N41@!>C?sd?q*iF;F4W^ng5!a=~4cPWpQCH@!WBI;s}J z`L{}oDkm&WAy=Q?4Syf^q9}Lcsa*mc2L;vpH@TiUt}ojQIdmzDuZ@K@grz>`&X)f2 z)rJE?f`}59lgJJjQg^@VOC!!?9A2hS`^9e;pIv-*R_9

7_|cBb>f@Eh_C*olsr9EtmOP-T86F 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≤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�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

PZ$%ePVjX}vuHA#gG05D2Q+pzF3yAjl%p!@^-S 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 KQ_lUz`+op6`cJ+9 literal 0 HcmV?d00001 diff --git a/src/dev/code_coverage/ingest_coverage/constants.js b/src/dev/code_coverage/ingest_coverage/constants.js index ddee7106f4490..ae3079afd911d 100644 --- a/src/dev/code_coverage/ingest_coverage/constants.js +++ b/src/dev/code_coverage/ingest_coverage/constants.js @@ -32,3 +32,4 @@ export const TEAM_ASSIGNMENT_PIPELINE_NAME = process.env.PIPELINE_NAME || 'team_ export const CODE_COVERAGE_CI_JOB_NAME = 'elastic+kibana+code-coverage'; export const RESEARCH_CI_JOB_NAME = 'elastic+kibana+qa-research'; export const CI_JOB_NAME = process.env.COVERAGE_JOB_NAME || RESEARCH_CI_JOB_NAME; +export const RESEARCH_CLUSTER_ES_HOST = process.env.ES_HOST || 'http://localhost:9200'; diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js b/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js new file mode 100644 index 0000000000000..d9fbf5690d8a4 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/get_data.js @@ -0,0 +1,31 @@ +/* + * 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 { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { fromNullable } from '../either'; + +const ROOT = resolve(__dirname, '../../../../..'); +const resolveFromRoot = resolve.bind(null, ROOT); +const path = ` +src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json`; +const resolved = resolveFromRoot(path.trimStart()); +const getContents = (scriptPath) => readFileSync(scriptPath, 'utf8'); + +export const fetch = () => fromNullable(resolved).map(getContents); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js new file mode 100644 index 0000000000000..301f7fb2dee2f --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -0,0 +1,48 @@ +/* + * 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 { run } from '@kbn/dev-utils'; +import { TEAM_ASSIGNMENT_PIPELINE_NAME } from '../constants'; +import { fetch } from './get_data'; +import { noop } from '../utils'; +import { update } from './update_ingest_pipeline'; + +export const uploadTeamAssignmentJson = () => run(execute, { description }); + +const updatePipeline = update(TEAM_ASSIGNMENT_PIPELINE_NAME); + +function execute({ flags, log }) { + if (flags.verbose) log.verbose(`### Verbose logging enabled`); + + fetch().fold(noop, updatePipeline(log)); +} + +function description() { + return ` + +Upload the latest team assignment pipeline def from src, +to the cluster. + + +Examples: + +node scripts/load_team_assignment.js --verbose + + `; +} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json new file mode 100644 index 0000000000000..18e88b47ec887 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json @@ -0,0 +1 @@ +{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/es_archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]} diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js b/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js new file mode 100644 index 0000000000000..03844b2a5dd32 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/update_ingest_pipeline.js @@ -0,0 +1,37 @@ +/* + * 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 { createFailError } from '@kbn/dev-utils'; +import { RESEARCH_CLUSTER_ES_HOST } from '../constants'; +import { pretty, green } from '../utils'; + +const { Client } = require('@elastic/elasticsearch'); + +const node = RESEARCH_CLUSTER_ES_HOST; +const client = new Client({ node }); + +export const update = (id) => (log) => async (body) => { + try { + await client.ingest.putPipeline({ id, body }); + log.verbose(`### Ingestion Pipeline ID: ${green(id)}`); + log.verbose(`### Payload Partial: \n${body.slice(0, 600)}...`); + } catch (e) { + throw createFailError(`${pretty(e.meta)}`); + } +}; diff --git a/src/dev/code_coverage/shell_scripts/assign_teams.sh b/src/dev/code_coverage/shell_scripts/assign_teams.sh new file mode 100644 index 0000000000000..186cbecb436e9 --- /dev/null +++ b/src/dev/code_coverage/shell_scripts/assign_teams.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +echo "### Code Coverage Team Assignment" +echo "" + +PIPELINE_NAME=$1 +export PIPELINE_NAME + +ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}" +export ES_HOST + +node scripts/load_team_assignment.js --verbose + +echo "### Code Coverage Team Assignment - Complete" +echo "" diff --git a/vars/kibanaTeamAssign.groovy b/vars/kibanaTeamAssign.groovy new file mode 100644 index 0000000000000..e2298ed43d408 --- /dev/null +++ b/vars/kibanaTeamAssign.groovy @@ -0,0 +1,25 @@ +def loadIngestionPipeline(ingestionPipelineName, title) { + kibanaPipeline.bash(""" + source src/dev/ci_setup/setup_env.sh + yarn kbn bootstrap --prefer-offline + + . src/dev/code_coverage/shell_scripts/assign_teams.sh '${ingestionPipelineName}' + """, title) +} + +def loadWithVault(ingestionPipelineName, title) { + def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' + withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { + withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { + withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { + loadIngestionPipeline(ingestionPipelineName, title) + } + } + } +} + +def load(ingestionPipelineName, title) { + loadWithVault(ingestionPipelineName, title) +} + +return this From 4ec6a712838bc9243157acbd54d6f193c587efbb Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 16 Jul 2020 09:46:56 -0700 Subject: [PATCH 03/76] skip flaky suite (#72102) --- .../security_solution_endpoint/apps/endpoint/policy_details.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index cf76f297d83be..d57bd32441454 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -19,7 +19,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/72102 + describe.skip('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); describe('with an invalid policy id', () => { From fdc2e41ffc22980215e841b51df52488ba001b70 Mon Sep 17 00:00:00 2001 From: Ben Skelker <54019610+benskelker@users.noreply.github.com> Date: Thu, 16 Jul 2020 19:51:46 +0300 Subject: [PATCH 04/76] [DOCS] Updates links to security docs (#72099) --- docs/management/advanced-options.asciidoc | 2 +- docs/siem/index.asciidoc | 2 +- docs/siem/siem-ui.asciidoc | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9a94c25bcdf6e..7dc360fd721f4 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -228,7 +228,7 @@ might increase the search time. This setting is off by default. Users must opt-i `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. `siem:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on -{siem-guide}/siem-ui-overview.html#network-ui[IP detail] pages. +{security-guide}/siem-ui-overview.html#network-ui[IP detail] pages. `siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. `siem:newsFeedUrl`:: The URL from which the security news feed content is diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index 9d17b5209304f..ceb4ac2bf1f34 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -19,7 +19,7 @@ image::siem/images/overview-ui.png[SIEM Overview in Kibana] == Add data Kibana provides step-by-step instructions to help you add data. The -{siem-guide}[SIEM Guide] is a good source for more +{security-guide}[Security Guide] is a good source for more detailed information and instructions. [float] diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc index 21a2ed55fdfdc..1caa13dc6c903 100644 --- a/docs/siem/siem-ui.asciidoc +++ b/docs/siem/siem-ui.asciidoc @@ -43,7 +43,7 @@ for creating signals. The SIEM app comes with prebuilt rules that search for suspicious activity on your network and hosts. Additionally, you can create your own rules. -See {siem-guide}/detection-engine-overview.html[Detections] in the SIEM +See {security-guide}/detection-engine-overview.html[Detections] in the SIEM Guide for information on managing detection rules and signals via the UI or the Detections API. @@ -61,7 +61,7 @@ saved Timelines. Additionally, you can send cases to external systems from within SIEM (currently ServiceNow and Jira). For information about opening, updating, and closing cases, see -{siem-guide}/cases-overview.html[Cases] in the SIEM Guide. +{security-guide}/cases-overview.html[Cases] in the SIEM Guide. [role="screenshot"] image::siem/images/cases-ui.png[] @@ -82,7 +82,7 @@ Hosts and Network pages, or even from within Timeline itself. A timeline is responsive and persists as you move through the SIEM app collecting data. -See the {siem-guide}[SIEM Guide] for more details on data sources and an +See the {security-guide}[Security Guide] for more details on data sources and an overview of UI elements and capabilities. [float] From 2b43617856d973f6aec0924fa7974e5edfafd988 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 16 Jul 2020 11:55:21 -0500 Subject: [PATCH 05/76] [APM] Change "Error rate" to "Trans. error rate" on service map popover (#72073) Fixes #72067. --- .../components/app/ServiceMap/Popover/ServiceStatsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index 4a1a291249f50..be52018735099 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -59,7 +59,7 @@ export function ServiceStatsList({ }, { title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { - defaultMessage: 'Error rate (avg.)', + defaultMessage: 'Trans. error rate (avg.)', }), description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null, }, From f75ccd4d8d06fc1fe224438d34ac325b51f046df Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 16 Jul 2020 12:17:35 -0500 Subject: [PATCH 06/76] [ML] Fix job list crashing due to undefined processed records (#71966) * [ML] Fix job list crashing due to undefined processed records * [ML] Add explicit undefined check * [ML] Add check to toLocaleString and update test Co-authored-by: Elastic Machine --- .../application/util/string_utils.test.ts | 20 +++++++++++-------- .../public/application/util/string_utils.ts | 5 ++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts index 034c406afb4b2..d7ed30065219a 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -38,17 +38,17 @@ describe('ML - string utils', () => { initial_record_score: 0, }; - test('returns correct values without URI encoding', () => { + test('should return correct values without URI encoding', () => { const result = replaceStringTokens('user=$user$,time=$timestamp$', testRecord, false); expect(result).toBe("user=Des O'Connor,time=1454890500000"); }); - test('returns correct values for missing token without URI encoding', () => { + test('should return correct values for missing token without URI encoding', () => { const result = replaceStringTokens('user=$username$,time=$timestamp$', testRecord, false); expect(result).toBe('user=$username$,time=1454890500000'); }); - test('returns correct values with URI encoding', () => { + test('should return correct values with URI encoding', () => { const testString1 = 'https://www.google.co.uk/webhp#q=$testfield1$'; const testString2 = 'https://www.google.co.uk/webhp#q=$testfield2$'; const testString3 = 'https://www.google.co.uk/webhp#q=$testfield3$'; @@ -65,7 +65,7 @@ describe('ML - string utils', () => { expect(result4).toBe("https://www.google.co.uk/webhp#q=Des%20O'Connor"); }); - test('returns correct values for missing token with URI encoding', () => { + test('should return correct values for missing token with URI encoding', () => { const testString = 'https://www.google.co.uk/webhp#q=$username$&time=$timestamp$'; const result = replaceStringTokens(testString, testRecord, true); expect(result).toBe('https://www.google.co.uk/webhp#q=$username$&time=1454890500000'); @@ -73,7 +73,7 @@ describe('ML - string utils', () => { }); describe('detectorToString', () => { - test('returns the correct descriptions for detectors', () => { + test('should return the correct descriptions for detectors', () => { const detector1: Detector = { function: 'count', }; @@ -102,7 +102,7 @@ describe('ML - string utils', () => { }); describe('toLocaleString', () => { - test('returns correct comma placement for large numbers', () => { + test('should return correct comma placement for large numbers', () => { expect(toLocaleString(1)).toBe('1'); expect(toLocaleString(10)).toBe('10'); expect(toLocaleString(100)).toBe('100'); @@ -114,10 +114,14 @@ describe('ML - string utils', () => { expect(toLocaleString(100000000)).toBe('100,000,000'); expect(toLocaleString(1000000000)).toBe('1,000,000,000'); }); + test('should return empty string for undefined or null', () => { + expect(toLocaleString(undefined)).toBe(''); + expect(toLocaleString(null)).toBe(''); + }); }); describe('mlEscape', () => { - test('returns correct escaping of characters', () => { + test('should return correct escaping of characters', () => { expect(mlEscape('foo&bar')).toBe('foo&bar'); expect(mlEscape('foobar')).toBe('foo>bar'); @@ -128,7 +132,7 @@ describe('ML - string utils', () => { }); describe('escapeForElasticsearchQuery', () => { - test('returns correct escaping of reserved elasticsearch characters', () => { + test('should return correct escaping of reserved elasticsearch characters', () => { expect(escapeForElasticsearchQuery('foo+bar')).toBe('foo\\+bar'); expect(escapeForElasticsearchQuery('foo-bar')).toBe('foo\\-bar'); expect(escapeForElasticsearchQuery('foo=bar')).toBe('foo\\=bar'); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.ts b/x-pack/plugins/ml/public/application/util/string_utils.ts index aa283fd71bf79..f659f8fdc1be1 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -90,7 +90,10 @@ function quoteField(field: string): string { // add commas to large numbers // Number.toLocaleString is not supported on safari -export function toLocaleString(x: number): string { +export function toLocaleString(x: number | undefined | null): string { + if (x === undefined || x === null) { + return ''; + } let result = x.toString(); if (x && typeof x === 'number') { const parts = x.toString().split('.'); From d0d271c07d939056d7bbb5745c0b28f298dbaf8c Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 16 Jul 2020 13:22:25 -0400 Subject: [PATCH 07/76] [ML] DF Analytics creation: ensure `monitor` cluster privilege not required to create job (#71934) * add checkIndexExists endpoint wrapping field_caps * replace indexNames with checkIndexExists check * update translations * show error toast on index check fail * add new route to api doc --- .../details_step/details_step_form.tsx | 33 +++++++++++++- .../use_create_analytics_form/actions.ts | 2 - .../use_create_analytics_form/reducer.ts | 11 ----- .../hooks/use_create_analytics_form/state.ts | 2 - .../use_create_analytics_form.ts | 20 +-------- .../services/ml_api_service/index.ts | 10 +++++ x-pack/plugins/ml/server/routes/apidoc.json | 1 + x-pack/plugins/ml/server/routes/system.ts | 44 +++++++++++++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 10 files changed, 87 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 168d5e31f57c3..8442ca13910d1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useRef } from 'react'; +import React, { FC, Fragment, useRef, useEffect } from 'react'; +import { debounce } from 'lodash'; import { EuiFieldText, EuiFormRow, @@ -21,6 +22,8 @@ import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/us import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { ContinueButton } from '../continue_button'; import { ANALYTICS_STEPS } from '../../page'; +import { ml } from '../../../../../services/ml_api_service'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; export const DetailsStepForm: FC = ({ actions, @@ -28,7 +31,7 @@ export const DetailsStepForm: FC = ({ setCurrentStep, }) => { const { - services: { docLinks }, + services: { docLinks, notifications }, } = useMlKibana(); const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; @@ -59,6 +62,32 @@ export const DetailsStepForm: FC = ({ destinationIndexNameValid === false || (destinationIndexPatternTitleExists === true && createIndexPattern === true); + const debouncedIndexCheck = debounce(async () => { + try { + const { exists } = await ml.checkIndexExists({ index: destinationIndex }); + setFormState({ destinationIndexNameExists: exists }); + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ml.dataframe.analytics.create.errorCheckingIndexExists', { + defaultMessage: 'The following error occurred getting the existing index names: {error}', + values: { error: extractErrorMessage(e) }, + }) + ); + } + }, 400); + + useEffect(() => { + if (destinationIndexNameValid === true) { + debouncedIndexCheck(); + } else if (destinationIndex.trim() === '' && destinationIndexNameExists === true) { + setFormState({ destinationIndexNameExists: false }); + } + + return () => { + debouncedIndexCheck.cancel(); + }; + }, [destinationIndex]); + return ( } - | { type: ACTION.SET_INDEX_NAMES; indexNames: State['indexNames'] } | { type: ACTION.SET_INDEX_PATTERN_TITLES; payload: { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index b344e44c97d59..acdaf15cdf4b7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -480,9 +480,6 @@ export function reducer(state: State, action: Action): State { // update state attributes which are derived from other state attributes. if (action.payload.destinationIndex !== undefined) { - newFormState.destinationIndexNameExists = state.indexNames.some( - (name) => newFormState.destinationIndex === name - ); newFormState.destinationIndexNameEmpty = newFormState.destinationIndex === ''; newFormState.destinationIndexNameValid = isValidIndexName(newFormState.destinationIndex); newFormState.destinationIndexPatternTitleExists = @@ -514,14 +511,6 @@ export function reducer(state: State, action: Action): State { ? validateAdvancedEditor({ ...state, form: newFormState }) : validateForm({ ...state, form: newFormState }); - case ACTION.SET_INDEX_NAMES: { - const newState = { ...state, indexNames: action.indexNames }; - newState.form.destinationIndexNameExists = newState.indexNames.some( - (name) => newState.form.destinationIndex === name - ); - return newState; - } - case ACTION.SET_INDEX_PATTERN_TITLES: { const newState = { ...state, 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 68a3613f91b5e..725fc8751408e 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 @@ -94,7 +94,6 @@ export interface State { trainingPercent: number; }; disabled: boolean; - indexNames: EsIndexName[]; indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; isAdvancedEditorValidJson: boolean; @@ -165,7 +164,6 @@ export const getInitialState = (): State => ({ !mlNodesAvailable() || !checkPermission('canCreateDataFrameAnalytics') || !checkPermission('canStartStopDataFrameAnalytics'), - indexNames: [], indexPatternsMap: {}, isAdvancedEditorEnabled: false, isAdvancedEditorValidJson: true, 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 4c312be26613c..035610684d556 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 @@ -25,7 +25,6 @@ import { reducer } from './reducer'; import { getInitialState, getJobConfigFromFormState, - EsIndexName, FormMessage, State, SourceIndexMap, @@ -67,9 +66,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const resetAdvancedEditorMessages = () => dispatch({ type: ACTION.RESET_ADVANCED_EDITOR_MESSAGES }); - const setIndexNames = (indexNames: EsIndexName[]) => - dispatch({ type: ACTION.SET_INDEX_NAMES, indexNames }); - const setAdvancedEditorRawString = (advancedEditorRawString: string) => dispatch({ type: ACTION.SET_ADVANCED_EDITOR_RAW_STRING, advancedEditorRawString }); @@ -214,21 +210,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } try { - setIndexNames((await ml.getIndices()).map((index) => index.name)); - } catch (e) { - addRequestMessage({ - error: getErrorMessage(e), - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames', - { - defaultMessage: 'An error occurred getting the existing index names:', - } - ), - }); - } - - try { - // Set the index pattern titles which the user can choose as the source. + // Set the existing index pattern titles. const indexPatternsMap: SourceIndexMap = {}; const savedObjects = (await mlContext.indexPatterns.getCache()) || []; savedObjects.forEach((obj) => { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 599e4d4bb8a10..184039729f9ef 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -372,6 +372,16 @@ export function mlApiServicesProvider(httpService: HttpService) { }); }, + checkIndexExists({ index }: { index: string }) { + const body = JSON.stringify({ index }); + + return httpService.http<{ exists: boolean }>({ + path: `${basePath()}/index_exists`, + method: 'POST', + body, + }); + }, + getFieldCaps({ index, fields }: { index: string; fields: string[] }) { const body = JSON.stringify({ ...(index !== undefined ? { index } : {}), diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index c2cb1ad9f0a57..98f7a78537c5c 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -105,6 +105,7 @@ "MlNodeCount", "MlInfo", "MlEsSearch", + "MlIndexExists", "JobAuditMessages", "GetJobAuditMessages", diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 410d540ecb8f7..f2d8c89046178 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -226,4 +226,48 @@ export function systemRoutes( } }) ); + + /** + * @apiGroup SystemRoutes + * + * @api {post} /api/ml/index_exists ES Field caps wrapper checks if index exists + * @apiName MlIndexExists + */ + router.post( + { + path: '/api/ml/index_exists', + validate: { + body: schema.object({ index: schema.string() }), + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { index } = request.body; + + const options = { + index: [index], + fields: ['*'], + ignoreUnavailable: true, + allowNoIndices: true, + ignore: 404, + }; + + const fieldsResult = await context.ml!.mlClient.callAsCurrentUser('fieldCaps', options); + const result = { exists: false }; + + if (Array.isArray(fieldsResult.indices) && fieldsResult.indices.length !== 0) { + result.exists = true; + } + + return response.ok({ + body: result, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f7ff3634d46d4..b87957ae45289 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9483,7 +9483,6 @@ "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "既存のインデックス名の取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "データフレーム分析ジョブの開始中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "このタイトルのインデックスパターンが既に存在します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc20bd55954c3..01ffa4833a3bb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9488,7 +9488,6 @@ "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", - "xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "获取现有索引名称时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "启动数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "具有此名称的索引模式已存在。", From 9195ce4e397666201f418a2d77ded5fa1e08629e Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 16 Jul 2020 10:36:44 -0700 Subject: [PATCH 08/76] Fix clear filters on agents table not working (#71978) Co-authored-by: Elastic Machine --- .../sections/fleet/agent_list_page/index.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) 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 97616ed490bc9..034482c4cf9b5 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 @@ -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, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { EuiBasicTable, EuiButton, @@ -144,13 +144,20 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [search, setSearch] = useState(defaultKuery); const { pagination, pageSizeOptions, setPagination } = usePagination(); - // Configs state (for filtering) + // Configs state for filtering const [isConfigsFilterOpen, setIsConfigsFilterOpen] = useState(false); const [selectedConfigs, setSelectedConfigs] = useState([]); + // Status for filtering const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); const [selectedStatus, setSelectedStatus] = useState([]); + const clearFilters = useCallback(() => { + setSearch(''); + setSelectedConfigs([]); + setSelectedStatus([]); + }, [setSearch, setSelectedConfigs, setSelectedStatus]); + // Add a config id to current search const addConfigFilter = (configId: string) => { setSelectedConfigs([...selectedConfigs, configId]); @@ -500,7 +507,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { className="fleet__agentList__table" data-test-subj="fleetAgentListTable" - loading={isLoading && agentsRequest.isInitialRequest} + loading={isLoading} hasActions={true} noItemsMessage={ isLoading && agentsRequest.isInitialRequest ? ( @@ -508,15 +515,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { id="xpack.ingestManager.agentList.loadingAgentsMessage" defaultMessage="Loading agents…" /> - ) : !search.trim() && selectedConfigs.length === 0 && totalAgents === 0 ? ( - emptyPrompt - ) : ( + ) : search.trim() || selectedConfigs.length || selectedStatus.length ? ( setSearch('')}> + clearFilters()}> = () => { ), }} /> - ) + ) : !isLoading && totalAgents === 0 ? ( + emptyPrompt + ) : undefined } items={totalAgents ? agents : []} itemId="id" From 394e7ba8bada12dc04208d3efe72d6f0b7591a2d Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 16 Jul 2020 10:56:07 -0700 Subject: [PATCH 09/76] Aligns BrowserList config with Support Matrix (#71876) Starting with 7.9.0 we will be dropping support for IE11. Updating the config to reflect this should minimize the bundle sizes. Signed-off-by: Tyler Smalley --- .browserslistrc | 8 +++++--- src/legacy/server/sass/build.test.js | 9 --------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.browserslistrc b/.browserslistrc index 89114f393c462..04395b913c9c5 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,7 +1,9 @@ [production] -last 2 versions -> 5% -Safari 7 # for PhantomJS support: https://github.com/elastic/kibana/issues/27136 +last 2 Firefox versions +last 2 Chrome versions +last 2 Safari versions +> 0.25% +not ie 11 [dev] last 1 chrome versions diff --git a/src/legacy/server/sass/build.test.js b/src/legacy/server/sass/build.test.js index 71c43ac010962..155c300bf3036 100644 --- a/src/legacy/server/sass/build.test.js +++ b/src/legacy/server/sass/build.test.js @@ -50,9 +50,6 @@ it('builds light themed SASS', async () => { expect(readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')) .toMatchInlineSnapshot(` "foo bar { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; background: #e6f0f8 url(./images/img.png) url(ui/assets/favicons/favicon.ico); } /*# sourceMappingURL=... */" @@ -75,9 +72,6 @@ it('builds dark themed SASS', async () => { expect(readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')) .toMatchInlineSnapshot(` "foo bar { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; background: #232635 url(./images/img.png) url(ui/assets/favicons/favicon.ico); } /*# sourceMappingURL=... */" @@ -104,9 +98,6 @@ it('rewrites url imports', async () => { expect(readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')) .toMatchInlineSnapshot(` "foo bar { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; background: #232635 url(__REPLACE_WITH_PUBLIC_PATH__foo/bar/images/img.png) url(__REPLACE_WITH_PUBLIC_PATH__ui/favicons/favicon.ico); } /*# sourceMappingURL=... */" From 46eba14669773fa85d7092f3075e019f2bec431a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 16 Jul 2020 12:16:29 -0600 Subject: [PATCH 10/76] [Maps] fix notifying user about losing unsaved changes when navigating away from map (#72003) * [Maps] fix notifying user about losing unsaved changes when navigating away from map * clean up * tslint fixes Co-authored-by: Elastic Machine --- .../maps/public/routing/maps_router.js | 22 ++- .../routing/page_elements/breadcrumbs.js | 60 -------- .../top_nav_menu/top_nav_menu.js | 34 ++--- .../routing/routes/list/maps_list_view.js | 7 +- .../public/routing/routes/maps_app/index.js | 4 + .../routes/maps_app/load_map_and_render.js | 7 +- .../routing/routes/maps_app/maps_app_view.js | 137 +++++++++--------- .../maps/public/selectors/map_selectors.ts | 20 +++ 8 files changed, 130 insertions(+), 161 deletions(-) delete mode 100644 x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js index 840d4f2c66922..30b2137399c1e 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.js +++ b/x-pack/plugins/maps/public/routing/maps_router.js @@ -17,18 +17,18 @@ import { LoadMapAndRender } from './routes/maps_app/load_map_and_render'; export let goToSpecifiedPath; export let kbnUrlStateStorage; -export async function renderApp(context, { appBasePath, element, history }) { +export async function renderApp(context, { appBasePath, element, history, onAppLeave }) { goToSpecifiedPath = (path) => history.push(path); kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); - render(, element); + render(, element); return () => { unmountComponentAtNode(element); }; } -const App = ({ history, appBasePath }) => { +const App = ({ history, appBasePath, onAppLeave }) => { const store = getStore(); const I18nContext = getCoreI18n().Context; @@ -37,8 +37,20 @@ const App = ({ history, appBasePath }) => { - - + ( + + )} + /> + } + /> // Redirect other routes to list, or if hash-containing, their non-hash equivalents { - const isOnMapNow = currentPath.startsWith(`/${MAP_PATH}`); - const breadCrumbs = isOnMapNow - ? [ - { - text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { - defaultMessage: 'Maps', - }), - onClick: () => { - if (hasUnsavedChanges(savedMap, initialLayerListConfig)) { - const navigateAway = window.confirm( - i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { - defaultMessage: `Your unsaved changes might not be saved`, - }) - ); - if (navigateAway) { - goToSpecifiedPath('/'); - } - } else { - goToSpecifiedPath('/'); - } - }, - }, - { text: savedMap.title }, - ] - : []; - getCoreChrome().setBreadcrumbs(breadCrumbs); -}; diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js index 762d61d6d33f9..ac2dec0db59cc 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js @@ -21,7 +21,6 @@ import { showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -import { updateBreadcrumbs } from '../breadcrumbs'; import { goToSpecifiedPath } from '../../maps_router'; export function MapsTopNavMenu({ @@ -35,7 +34,6 @@ export function MapsTopNavMenu({ refreshConfig, setRefreshConfig, setRefreshStoreConfig, - initialLayerListConfig, indexPatterns, updateFiltersAndDispatch, isSaveDisabled, @@ -44,7 +42,7 @@ export function MapsTopNavMenu({ openMapSettings, inspectorAdapters, syncAppAndGlobalState, - currentPath, + setBreadcrumbs, isOpenSettingsDisabled, }) { const { TopNavMenu } = getNavigation().ui; @@ -64,14 +62,13 @@ export function MapsTopNavMenu({ // Nav settings const config = getTopNavConfig( savedMap, - initialLayerListConfig, isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, - currentPath + setBreadcrumbs ); const submitQuery = function ({ dateRange, query }) { @@ -121,14 +118,13 @@ export function MapsTopNavMenu({ function getTopNavConfig( savedMap, - initialLayerListConfig, isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, - currentPath + setBreadcrumbs ) { return [ { @@ -210,19 +206,15 @@ function getTopNavConfig( isTitleDuplicateConfirmed, onTitleDuplicate, }; - return doSave( - savedMap, - saveOptions, - initialLayerListConfig, - closeFlyout, - currentPath - ).then((response) => { - // If the save wasn't successful, put the original values back. - if (!response.id || response.error) { - savedMap.title = currentTitle; + return doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs).then( + (response) => { + // If the save wasn't successful, put the original values back. + if (!response.id || response.error) { + savedMap.title = currentTitle; + } + return response; } - return response; - }); + ); }; const saveModal = ( @@ -243,7 +235,7 @@ function getTopNavConfig( ]; } -async function doSave(savedMap, saveOptions, initialLayerListConfig, closeFlyout, currentPath) { +async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) { closeFlyout(); savedMap.syncWithStore(); let id; @@ -265,7 +257,7 @@ async function doSave(savedMap, saveOptions, initialLayerListConfig, closeFlyout if (id) { goToSpecifiedPath(`/map/${id}${window.location.hash}`); - updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath); + setBreadcrumbs(); getToasts().addSuccess({ title: i18n.translate('xpack.maps.mapController.saveSuccessMessage', { diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js index a32bd00dbae51..e9229883d708d 100644 --- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js @@ -33,7 +33,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { addHelpMenuToAppChrome } from '../../../help_menu_util'; import { Link } from 'react-router-dom'; -import { updateBreadcrumbs } from '../../page_elements/breadcrumbs'; import { goToSpecifiedPath } from '../../maps_router'; export const EMPTY_FILTER = ''; @@ -53,17 +52,13 @@ export class MapsListView extends React.Component { listingLimit: getUiSettings().get('savedObjects:listingLimit'), }; - UNSAFE_componentWillMount() { - this._isMounted = true; - updateBreadcrumbs(); - } - componentWillUnmount() { this._isMounted = false; this.debouncedFetch.cancel(); } componentDidMount() { + this._isMounted = true; this.initMapList(); } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index 6b47ac6e0352a..9b0d52e4fe297 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -11,6 +11,7 @@ import { getFilters, getQueryableUniqueIndexPatternIds, getRefreshConfig, + hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { replaceLayerList, @@ -34,6 +35,9 @@ function mapStateToProps(state = {}) { flyoutDisplay: getFlyoutDisplay(state), refreshConfig: getRefreshConfig(state), filters: getFilters(state), + hasUnsavedChanges: (savedMap, initialLayerListConfig) => { + return hasUnsavedChanges(state, savedMap, initialLayerListConfig); + }, }; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js index a17b83502e048..c87f6eb330531 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js @@ -27,9 +27,8 @@ export const LoadMapAndRender = class extends React.Component { } async _loadSavedMap() { - const { savedMapId } = this.props.match.params; try { - const savedMap = await getMapsSavedObjectLoader().get(savedMapId); + const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); if (this._isMounted) { this.setState({ savedMap }); } @@ -48,11 +47,11 @@ export const LoadMapAndRender = class extends React.Component { render() { const { savedMap, failedToLoad } = this.state; + if (failedToLoad) { return ; } - const currentPath = this.props.match.url; - return savedMap ? : null; + return savedMap ? : null; } }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index bf92f5a337121..29fbb5f46e29b 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -30,12 +30,15 @@ import { } from '../../state_syncing/global_sync'; import { AppStateManager } from '../../state_syncing/app_state_manager'; import { useAppStateSyncing } from '../../state_syncing/app_sync'; -import { updateBreadcrumbs } from '../../page_elements/breadcrumbs'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { GisMap } from '../../../connected_components/gis_map'; +import { goToSpecifiedPath } from '../../maps_router'; + +const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { + defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', +}); export class MapsAppView extends React.Component { - _visibleSubscription = null; _globalSyncUnsubscribe = null; _globalSyncChangeMonitorSubscription = null; _appSyncUnsubscribe = null; @@ -47,16 +50,13 @@ export class MapsAppView extends React.Component { indexPatterns: [], prevIndexPatternIds: [], initialized: false, - isVisible: true, savedQuery: '', - currentPath: '', initialLayerListConfig: null, }; } componentDidMount() { - const { savedMap, currentPath } = this.props; - this.setState({ currentPath }); + const { savedMap } = this.props; getCoreChrome().docTitle.change(savedMap.title); getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); @@ -77,48 +77,24 @@ export class MapsAppView extends React.Component { this._updateStateFromSavedQuery(initAppState.savedQuery); } - // Monitor visibility - this._visibleSubscription = getCoreChrome() - .getIsVisible$() - .subscribe((isVisible) => this.setState({ isVisible })); this._initMap(); - } - _initBreadcrumbUpdater = () => { - const { initialLayerListConfig, currentPath } = this.state; - updateBreadcrumbs(this.props.savedMap, initialLayerListConfig, currentPath); - }; + this._setBreadcrumbs(); - componentDidUpdate(prevProps, prevState) { - const { currentPath: prevCurrentPath } = prevState; - const { currentPath, initialLayerListConfig } = this.state; - const { savedMap } = this.props; - if (savedMap && initialLayerListConfig && currentPath !== prevCurrentPath) { - updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath); - } - // TODO: Handle null when converting to TS - this._handleStoreChanges(); - } - - _updateFromGlobalState = ({ changes, state: globalState }) => { - if (!changes || !globalState) { - return; - } - const newState = {}; - Object.keys(changes).forEach((key) => { - if (changes[key]) { - newState[key] = globalState[key]; + this.props.onAppLeave((actions) => { + if (this._hasUnsavedChanges()) { + if (!window.confirm(unsavedChangesWarning)) { + return; + } } + return actions.default(); }); + } - this.setState(newState, () => { - this._appStateManager.setQueryAndFilters({ - filters: getData().query.filterManager.getAppFilters(), - }); - const { time, filters, refreshInterval } = globalState; - this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time); - }); - }; + componentDidUpdate() { + // TODO: Handle null when converting to TS + this._handleStoreChanges(); + } componentWillUnmount() { if (this._globalSyncUnsubscribe) { @@ -127,9 +103,6 @@ export class MapsAppView extends React.Component { if (this._appSyncUnsubscribe) { this._appSyncUnsubscribe(); } - if (this._visibleSubscription) { - this._visibleSubscription.unsubscribe(); - } if (this._globalSyncChangeMonitorSubscription) { this._globalSyncChangeMonitorSubscription.unsubscribe(); } @@ -141,8 +114,55 @@ export class MapsAppView extends React.Component { filterManager.removeFilter(filter); } }); + + getCoreChrome().setBreadcrumbs([]); + } + + _hasUnsavedChanges() { + return this.props.hasUnsavedChanges(this.props.savedMap, this.state.initialLayerListConfig); } + _setBreadcrumbs = () => { + getCoreChrome().setBreadcrumbs([ + { + text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { + defaultMessage: 'Maps', + }), + onClick: () => { + if (this._hasUnsavedChanges()) { + const navigateAway = window.confirm(unsavedChangesWarning); + if (navigateAway) { + goToSpecifiedPath('/'); + } + } else { + goToSpecifiedPath('/'); + } + }, + }, + { text: this.props.savedMap.title }, + ]); + }; + + _updateFromGlobalState = ({ changes, state: globalState }) => { + if (!changes || !globalState) { + return; + } + const newState = {}; + Object.keys(changes).forEach((key) => { + if (changes[key]) { + newState[key] = globalState[key]; + } + }); + + this.setState(newState, () => { + this._appStateManager.setQueryAndFilters({ + filters: getData().query.filterManager.getAppFilters(), + }); + const { time, filters, refreshInterval } = globalState; + this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time); + }); + }; + _getInitialLayersFromUrlParam() { const locationSplit = window.location.href.split('?'); if (locationSplit.length <= 1) { @@ -301,13 +321,9 @@ export class MapsAppView extends React.Component { this._getInitialLayersFromUrlParam() ); this.props.replaceLayerList(layerList); - this.setState( - { - initialLayerListConfig: copyPersistentState(layerList), - savedMap, - }, - this._initBreadcrumbUpdater - ); + this.setState({ + initialLayerListConfig: copyPersistentState(layerList), + }); } _updateFiltersAndDispatch = (filters) => { @@ -407,18 +423,10 @@ export class MapsAppView extends React.Component { } _renderTopNav() { - const { - query, - time, - savedQuery, - initialLayerListConfig, - isVisible, - indexPatterns, - currentPath, - } = this.state; - const { savedMap, refreshConfig } = this.props; + const { query, time, savedQuery, indexPatterns } = this.state; + const { savedMap, refreshConfig, isFullScreen } = this.props; - return isVisible ? ( + return !isFullScreen ? ( { @@ -448,7 +455,7 @@ export class MapsAppView extends React.Component { this._updateStateFromSavedQuery(query); }} syncAppAndGlobalState={this._syncAppAndGlobalState} - currentPath={currentPath} + setBreadcrumbs={this._setBreadcrumbs} /> ) : null; } diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index f400e242b697f..fe2cfec3c761c 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -416,3 +416,23 @@ export const areLayersLoaded = createSelector( return true; } ); + +export function hasUnsavedChanges( + state: MapStoreState, + savedMap: unknown, + initialLayerListConfig: LayerDescriptor[] +) { + const layerListConfigOnly = copyPersistentState(getLayerListRaw(state)); + + // @ts-expect-error + const savedLayerList = savedMap.getLayerList(); + + return !savedLayerList + ? !_.isEqual(layerListConfigOnly, initialLayerListConfig) + : // savedMap stores layerList as a JSON string using JSON.stringify. + // JSON.stringify removes undefined properties from objects. + // savedMap.getLayerList converts the JSON string back into Javascript array of objects. + // Need to perform the same process for layerListConfigOnly to compare apples to apples + // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. + !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList); +} From acf0617762fb170e9c61dde40d297c269cff44c9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 16 Jul 2020 14:32:22 -0400 Subject: [PATCH 11/76] [ML] DF Analytics: UI improvements (#71949) * make hyper parameters one word * update analytics job types help text * update job type text * ensure long description has word break to prevent overflow --- .../components/advanced_step/advanced_step_details.tsx | 2 +- .../components/advanced_step/advanced_step_form.tsx | 2 +- .../components/configuration_step/job_type.tsx | 9 +++------ .../components/details_step/details_step_details.tsx | 6 +++++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx index 875590d0f9ee4..e7026508a90bb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx @@ -249,7 +249,7 @@ export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({

{i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle', { - defaultMessage: 'Hyper parameters', + defaultMessage: 'Hyperparameters', })}

diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 11184afb0e715..7409e414af2b6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -408,7 +408,7 @@ export const AdvancedStepForm: FC = ({

{i18n.translate('xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle', { - defaultMessage: 'Hyper parameters', + defaultMessage: 'Hyperparameters', })}

diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index da547ee6255a1..1e5dbee3499bd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -21,24 +21,21 @@ export const JobType: FC = ({ type, setFormState }) => { const outlierHelpText = i18n.translate( 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', { - defaultMessage: - 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', + defaultMessage: 'Outlier detection identifies unusual data points in the data set.', } ); const regressionHelpText = i18n.translate( 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', { - defaultMessage: - 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', + defaultMessage: 'Regression predicts numerical values in the data set.', } ); const classificationHelpText = i18n.translate( 'xpack.ml.dataframe.analytics.create.classificationHelpText', { - defaultMessage: - 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', + defaultMessage: 'Classification predicts labels of data points in the data set.', } ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx index 8a41eb4b8a865..fb08ea841d29e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx @@ -75,7 +75,11 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
- + From a994f577615bbbdd4d7b92a6764bfc54e3da6466 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 16 Jul 2020 14:44:48 -0400 Subject: [PATCH 12/76] use semver to compare versions (#72107) --- .../ingest_manager/server/services/epm/packages/install.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 35c5b58a93710..cd4259a0c30d7 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 @@ -6,6 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; +import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -95,8 +96,7 @@ export async function installPackage(options: { // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - - if (pkgVersion < latestPackage.version) + if (semver.lt(pkgVersion, latestPackage.version)) throw Boom.badRequest('Cannot install or update to an out-of-date package'); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); From 26a0cf55b505175590af43526a6f8126dabb5cf5 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 16 Jul 2020 13:58:13 -0500 Subject: [PATCH 13/76] [APM] Add `name` to `Observer` type (#72110) Backport #72078 is failing because of the type check because `observer.name` is not defined. Not sure why this wasn't a problem on master, but adding it here so I can include it. --- x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts index 42843130ec47f..0815b7cd88163 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts @@ -5,6 +5,7 @@ */ export interface Observer { + name?: string; version: string; version_major: number; } From c787b8adbe34201b926424a11c67bad23bb96a2c Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 16 Jul 2020 13:07:19 -0600 Subject: [PATCH 14/76] [SIEM][Detection Engine][Lists] Fixes bugs, adds tests (#71880) ## Summary * Adds the createEndpointListSchema to the create_endpoint_list_route for API boundary checks * Adds unit tests to the requests * Fixes a few bugs found in the find endpoints for types * Fixes or deletes the skipped tests * Updated TODO blocks and removed ones that are obsolete ### 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 --- x-pack/plugins/lists/common/constants.mock.ts | 2 + .../elastic_query/update_es_list_schema.ts | 1 - .../create_endpoint_list_item_schema.mock.ts | 30 +++ .../create_endpoint_list_item_schema.test.ts | 216 ++++++++++++++++++ .../delete_endpoint_list_item_schema.mock.ts | 13 ++ .../delete_endpoint_list_item_schema.test.ts | 51 +++++ .../delete_exception_list_item_schema.test.ts | 11 - .../delete_exception_list_schema.test.ts | 11 - .../request/delete_list_item_schema.mock.ts | 15 ++ .../request/delete_list_item_schema.test.ts | 36 +++ .../find_endpoint_list_item_schema.mock.ts | 28 +++ .../find_endpoint_list_item_schema.test.ts | 110 +++++++++ .../find_exception_list_item_schema.mock.ts | 52 +++++ .../find_exception_list_item_schema.test.ts | 130 +++++++++++ .../find_exception_list_schema.mock.ts | 30 +++ .../find_exception_list_schema.test.ts | 114 +++++++++ .../request/find_exception_list_schema.ts | 4 +- .../request/find_list_item_schema.mock.ts | 32 +++ .../request/find_list_item_schema.test.ts | 109 +++++++++ .../schemas/request/find_list_item_schema.ts | 7 +- .../schemas/request/find_list_schema.mock.ts | 25 ++ .../schemas/request/find_list_schema.test.ts | 105 +++++++++ .../read_endpoint_list_item_schema.mock.ts | 14 ++ .../read_endpoint_list_item_schema.test.ts | 87 +++++++ .../update_endpoint_list_item_schema.mock.ts | 33 +++ .../update_endpoint_list_item_schema.test.ts | 147 ++++++++++++ .../update_exception_list_item_schema.test.ts | 14 -- .../update_exception_list_schema.test.ts | 14 -- .../request/update_list_item_schema.mock.ts | 15 ++ .../request/update_list_item_schema.test.ts | 48 ++++ .../create_endpoint_list_schema.test.ts | 15 +- .../exception_list_item_schema.test.ts | 10 +- .../response/exception_list_item_schema.ts | 1 - .../response/exception_list_schema.test.ts | 10 +- .../schemas/response/exception_list_schema.ts | 1 - .../routes/create_endpoint_list_route.ts | 25 +- .../server/routes/export_list_item_route.ts | 1 - .../server/routes/find_list_item_route.ts | 10 +- .../server/routes/patch_list_item_route.ts | 1 - .../lists/server/routes/patch_list_route.ts | 1 - .../scripts/post_x_exception_list_items.sh | 29 +++ .../services/items/create_list_items_bulk.ts | 2 +- 42 files changed, 1518 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts create mode 100755 x-pack/plugins/lists/server/scripts/post_x_exception_list_items.sh diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 7f7a90eeba5a2..4924ba24426af 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -58,3 +58,5 @@ export const ITEM_TYPE = 'simple'; export const _TAGS = []; export const TAGS = []; export const COMMENTS = []; +export const FILTER = 'name:Nicolas Bourbaki'; +export const CURSOR = 'c29tZXN0cmluZ2ZvcnlvdQ=='; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts index d008a82308153..8f23f3744e563 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts @@ -16,7 +16,6 @@ import { updated_by, } from '../common/schemas'; -// TODO: Should we use partial here and everywhere these are instead of this OrUndefined? export const updateEsListSchema = t.exact( t.type({ description: descriptionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.mock.ts new file mode 100644 index 0000000000000..5c4aff1fedcd3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.mock.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 { + COMMENTS, + DESCRIPTION, + ENTRIES, + ITEM_TYPE, + META, + NAME, + TAGS, + _TAGS, +} from '../../constants.mock'; + +import { CreateEndpointListItemSchema } from './create_endpoint_list_item_schema'; + +export const getCreateEndpointListItemSchemaMock = (): CreateEndpointListItemSchema => ({ + _tags: _TAGS, + comments: COMMENTS, + description: DESCRIPTION, + entries: ENTRIES, + item_id: undefined, + meta: META, + name: NAME, + tags: TAGS, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts new file mode 100644 index 0000000000000..916e8db483454 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { getCreateCommentsArrayMock } from '../types/create_comments.mock'; +import { getCommentsMock } from '../types/comments.mock'; +import { CommentsArray } from '../types'; + +import { + CreateEndpointListItemSchema, + createEndpointListItemSchema, +} from './create_endpoint_list_item_schema'; +import { getCreateEndpointListItemSchemaMock } from './create_endpoint_list_item_schema.mock'; + +describe('create_endpoint_list_item_schema', () => { + test('it should validate a typical list item request not counting the auto generated uuid', () => { + const payload = getCreateEndpointListItemSchemaMock(); + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an undefined for "description"', () => { + const payload = getCreateEndpointListItemSchemaMock(); + delete payload.description; + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an undefined for "name"', () => { + const payload = getCreateEndpointListItemSchemaMock(); + delete payload.name; + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an undefined for "type"', () => { + const payload = getCreateEndpointListItemSchemaMock(); + delete payload.type; + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a "list_id" since it does not required one', () => { + const inputPayload: CreateEndpointListItemSchema & { list_id: string } = { + ...getCreateEndpointListItemSchemaMock(), + list_id: 'list-123', + }; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "list_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a "namespace_type" since it does not required one', () => { + const inputPayload: CreateEndpointListItemSchema & { namespace_type: string } = { + ...getCreateEndpointListItemSchemaMock(), + namespace_type: 'single', + }; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "namespace_type"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => { + const payload = getCreateEndpointListItemSchemaMock(); + const outputPayload = getCreateEndpointListItemSchemaMock(); + delete payload.meta; + delete outputPayload.meta; + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + const outputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload.comments; + outputPayload.comments = []; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should validate "comments" array', () => { + const inputPayload = { + ...getCreateEndpointListItemSchemaMock(), + comments: getCreateCommentsArrayMock(), + }; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(inputPayload); + }); + + test('it should NOT validate "comments" with "created_at" or "created_by" values', () => { + const inputPayload: Omit & { + comments?: CommentsArray; + } = { + ...getCreateEndpointListItemSchemaMock(), + comments: [getCommentsMock()], + }; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate an undefined for "entries" but return an array', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + const outputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload.entries; + outputPayload.entries = []; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + const outputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload.tags; + outputPayload.tags = []; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + const outputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload._tags; + outputPayload._tags = []; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should validate an undefined for "item_id" and auto generate a uuid', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as CreateEndpointListItemSchema).item_id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => { + const inputPayload = getCreateEndpointListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = createEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateEndpointListItemSchema).item_id; + expect(message.schema).toEqual(inputPayload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: CreateEndpointListItemSchema & { + extraKey: string; + } = { ...getCreateEndpointListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = createEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.mock.ts new file mode 100644 index 0000000000000..7fc775047e6a1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.mock.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 { ID } from '../../constants.mock'; + +import { DeleteEndpointListItemSchema } from './delete_endpoint_list_item_schema'; + +export const getDeleteEndpointListItemSchemaMock = (): DeleteEndpointListItemSchema => ({ + id: ID, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts new file mode 100644 index 0000000000000..fa75be8bc541e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + DeleteEndpointListItemSchema, + deleteEndpointListItemSchema, +} from './delete_endpoint_list_item_schema'; +import { getDeleteEndpointListItemSchemaMock } from './delete_endpoint_list_item_schema.mock'; + +describe('delete_endpoint_list_item_schema', () => { + test('it should validate a typical endpoint list item request', () => { + const payload = getDeleteEndpointListItemSchemaMock(); + const decoded = deleteEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept a value for "namespace_type" since it does not require one', () => { + const payload: DeleteEndpointListItemSchema & { + namespace_type: string; + } = { ...getDeleteEndpointListItemSchemaMock(), namespace_type: 'single' }; + delete payload.namespace_type; + const decoded = deleteEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getDeleteEndpointListItemSchemaMock()); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: DeleteEndpointListItemSchema & { + extraKey?: string; + } = { ...getDeleteEndpointListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = deleteEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index 4b42f79a89329..042f62a8d129b 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -26,17 +26,6 @@ describe('delete_exception_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - // TODO It does allow an id of undefined, is this wanted behavior? - test.skip('it should NOT accept an undefined for an "id"', () => { - const payload = getDeleteExceptionListItemSchemaMock(); - delete payload.id; - const decoded = deleteExceptionListItemSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); - expect(message.schema).toEqual({}); - }); - test('it should accept an undefined for "namespace_type" but default to "single"', () => { const payload = getDeleteExceptionListItemSchemaMock(); delete payload.namespace_type; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index d925de8013c13..2bb0a23173bd6 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -26,17 +26,6 @@ describe('delete_exception_list_schema', () => { expect(message.schema).toEqual(payload); }); - // TODO It does allow an id of undefined, is this wanted behavior? - test.skip('it should NOT accept an undefined for an id', () => { - const payload = getDeleteExceptionListSchemaMock(); - delete payload.id; - const decoded = deleteExceptionListSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); - expect(message.schema).toEqual({}); - }); - test('it should accept an undefined for "namespace_type" but default to "single"', () => { const payload = getDeleteExceptionListSchemaMock(); delete payload.namespace_type; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.mock.ts new file mode 100644 index 0000000000000..ea5529c39fa84 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.mock.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 { ID, LIST_ID, VALUE } from '../../constants.mock'; + +import { DeleteListItemSchema } from './delete_list_item_schema'; + +export const getDeleteListItemSchemaMock = (): DeleteListItemSchema => ({ + id: ID, + list_id: LIST_ID, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts new file mode 100644 index 0000000000000..9bc2825d774ed --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; +import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; + +describe('delete_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getDeleteListItemSchemaMock(); + const decoded = deleteListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: DeleteListItemSchema & { + extraKey?: string; + } = { ...getDeleteListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = deleteListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts new file mode 100644 index 0000000000000..bff55dedf3064 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { FILTER } from '../../constants.mock'; + +import { + FindEndpointListItemSchemaPartial, + FindEndpointListItemSchemaPartialDecoded, +} from './find_endpoint_list_item_schema'; + +export const getFindEndpointListItemSchemaMock = (): FindEndpointListItemSchemaPartial => ({ + filter: FILTER, + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindEndpointListItemSchemaDecodedMock = (): FindEndpointListItemSchemaPartialDecoded => ({ + filter: FILTER, + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts new file mode 100644 index 0000000000000..f9eeaa33230f9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + getFindEndpointListItemSchemaDecodedMock, + getFindEndpointListItemSchemaMock, +} from './find_endpoint_list_item_schema.mock'; +import { + FindEndpointListItemSchemaPartial, + findEndpointListItemSchema, +} from './find_endpoint_list_item_schema'; + +describe('find_endpoint_list_item_schema', () => { + test('it should validate a typical find item request', () => { + const payload = getFindEndpointListItemSchemaMock(); + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindEndpointListItemSchemaDecodedMock()); + }); + + test('it should validate and empty object since everything is optional and has defaults', () => { + const payload: FindEndpointListItemSchemaPartial = {}; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate with page missing', () => { + const payload = getFindEndpointListItemSchemaMock(); + delete payload.page; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindEndpointListItemSchemaDecodedMock(); + delete expected.page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with pre_page missing', () => { + const payload = getFindEndpointListItemSchemaMock(); + delete payload.per_page; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindEndpointListItemSchemaDecodedMock(); + delete expected.per_page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with filter missing', () => { + const payload = getFindEndpointListItemSchemaMock(); + delete payload.filter; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindEndpointListItemSchemaDecodedMock(); + delete expected.filter; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_field missing', () => { + const payload = getFindEndpointListItemSchemaMock(); + delete payload.sort_field; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindEndpointListItemSchemaDecodedMock(); + delete expected.sort_field; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_order missing', () => { + const payload = getFindEndpointListItemSchemaMock(); + delete payload.sort_order; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindEndpointListItemSchemaDecodedMock(); + delete expected.sort_order; + expect(message.schema).toEqual(expected); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: FindEndpointListItemSchemaPartial & { + extraKey: string; + } = { ...getFindEndpointListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = findEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f22e6685fe0ac --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.mock.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 { FILTER, LIST_ID, NAMESPACE_TYPE } from '../../constants.mock'; + +import { + FindExceptionListItemSchemaPartial, + FindExceptionListItemSchemaPartialDecoded, +} from './find_exception_list_item_schema'; + +export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchemaPartial => ({ + filter: FILTER, + list_id: LIST_ID, + namespace_type: NAMESPACE_TYPE, + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListItemSchemaPartial => ({ + filter: 'name:Sofia Kovalevskaya,name:Hypatia,name:Sophie Germain', + list_id: 'list-1,list-2,list-3', + namespace_type: 'single,single,agnostic', + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindExceptionListItemSchemaDecodedMock = (): FindExceptionListItemSchemaPartialDecoded => ({ + filter: [FILTER], + list_id: [LIST_ID], + namespace_type: [NAMESPACE_TYPE], + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindExceptionListItemSchemaDecodedMultipleMock = (): FindExceptionListItemSchemaPartialDecoded => ({ + filter: ['name:Sofia Kovalevskaya', 'name:Hypatia', 'name:Sophie Germain'], + list_id: ['list-1', 'list-2', 'list-3'], + namespace_type: ['single', 'single', 'agnostic'], + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts new file mode 100644 index 0000000000000..ba64bb434d50b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { LIST_ID } from '../../constants.mock'; + +import { + getFindExceptionListItemSchemaDecodedMock, + getFindExceptionListItemSchemaDecodedMultipleMock, + getFindExceptionListItemSchemaMock, + getFindExceptionListItemSchemaMultipleMock, +} from './find_exception_list_item_schema.mock'; +import { + FindExceptionListItemSchemaPartial, + FindExceptionListItemSchemaPartialDecoded, + findExceptionListItemSchema, +} from './find_exception_list_item_schema'; + +describe('find_list_item_schema', () => { + test('it should validate a typical find item request', () => { + const payload = getFindExceptionListItemSchemaMock(); + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindExceptionListItemSchemaDecodedMock()); + }); + + test('it should validate a typical find item request with multiple input strings turned into array elements', () => { + const payload = getFindExceptionListItemSchemaMultipleMock(); + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindExceptionListItemSchemaDecodedMultipleMock()); + }); + + test('it should validate just a list_id where it decodes into an array for list_id and adds a namespace_type of "single" as an array', () => { + const payload: FindExceptionListItemSchemaPartial = { list_id: LIST_ID }; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: FindExceptionListItemSchemaPartialDecoded = { + filter: [], + list_id: [LIST_ID], + namespace_type: ['single'], + }; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with page missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.page; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with pre_page missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.per_page; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.per_page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with filter missing and add filter as an empty array', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.filter; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: FindExceptionListItemSchemaPartialDecoded = { + ...getFindExceptionListItemSchemaDecodedMock(), + filter: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_field missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.sort_field; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.sort_field; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_order missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.sort_order; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.sort_order; + expect(message.schema).toEqual(expected); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: FindExceptionListItemSchemaPartial & { + extraKey: string; + } = { ...getFindExceptionListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.ts new file mode 100644 index 0000000000000..8080d10ca451c --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.mock.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 { FILTER, NAMESPACE_TYPE } from '../../constants.mock'; + +import { + FindExceptionListSchemaPartial, + FindExceptionListSchemaPartialDecoded, +} from './find_exception_list_schema'; + +export const getFindExceptionListSchemaMock = (): FindExceptionListSchemaPartial => ({ + filter: FILTER, + namespace_type: NAMESPACE_TYPE, + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaPartialDecoded => ({ + filter: FILTER, + namespace_type: NAMESPACE_TYPE, + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts new file mode 100644 index 0000000000000..42356066176d5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + getFindExceptionListSchemaDecodedMock, + getFindExceptionListSchemaMock, +} from './find_exception_list_schema.mock'; +import { + FindExceptionListSchemaPartial, + FindExceptionListSchemaPartialDecoded, + findExceptionListSchema, +} from './find_exception_list_schema'; + +describe('find_exception_list_schema', () => { + test('it should validate a typical find item request', () => { + const payload = getFindExceptionListSchemaMock(); + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindExceptionListSchemaDecodedMock()); + }); + + test('it should validate and empty object since everything is optional and will respond only with namespace_type filled out to be "single"', () => { + const payload: FindExceptionListSchemaPartial = {}; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: FindExceptionListSchemaPartialDecoded = { + namespace_type: 'single', + }; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with page missing', () => { + const payload = getFindExceptionListSchemaMock(); + delete payload.page; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListSchemaDecodedMock(); + delete expected.page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with pre_page missing', () => { + const payload = getFindExceptionListSchemaMock(); + delete payload.per_page; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListSchemaDecodedMock(); + delete expected.per_page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with filter missing', () => { + const payload = getFindExceptionListSchemaMock(); + delete payload.filter; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListSchemaDecodedMock(); + delete expected.filter; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_field missing', () => { + const payload = getFindExceptionListSchemaMock(); + delete payload.sort_field; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListSchemaDecodedMock(); + delete expected.sort_field; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_order missing', () => { + const payload = getFindExceptionListSchemaMock(); + delete payload.sort_order; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListSchemaDecodedMock(); + delete expected.sort_order; + expect(message.schema).toEqual(expected); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: FindExceptionListSchemaPartial & { + extraKey: string; + } = { ...getFindExceptionListSchemaMock(), extraKey: 'some new value' }; + const decoded = findExceptionListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index 8b9b08ed387b1..4fa9d2e42c5d1 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -24,11 +24,11 @@ export const findExceptionListSchema = t.exact( }) ); -export type FindExceptionListSchemaPartial = t.TypeOf; +export type FindExceptionListSchemaPartial = t.OutputOf; // This type is used after a decode since some things are defaults after a decode. export type FindExceptionListSchemaPartialDecoded = Omit< - FindExceptionListSchemaPartial, + t.TypeOf, 'namespace_type' > & { namespace_type: NamespaceType; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.ts new file mode 100644 index 0000000000000..a1e91f6acd264 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.mock.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 { CURSOR, FILTER, LIST_ID } from '../../constants.mock'; + +import { + FindListItemSchemaPartial, + FindListItemSchemaPartialDecoded, +} from './find_list_item_schema'; + +export const getFindListItemSchemaMock = (): FindListItemSchemaPartial => ({ + cursor: CURSOR, + filter: FILTER, + list_id: LIST_ID, + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindListItemSchemaDecodedMock = (): FindListItemSchemaPartialDecoded => ({ + cursor: CURSOR, + filter: FILTER, + list_id: LIST_ID, + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts new file mode 100644 index 0000000000000..42803fffc53c2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { LIST_ID } from '../../constants.mock'; + +import { + getFindListItemSchemaDecodedMock, + getFindListItemSchemaMock, +} from './find_list_item_schema.mock'; +import { + FindListItemSchemaPartial, + FindListItemSchemaPartialDecoded, + findListItemSchema, +} from './find_list_item_schema'; + +describe('find_list_item_schema', () => { + test('it should validate a typical find item request', () => { + const payload = getFindListItemSchemaMock(); + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindListItemSchemaDecodedMock()); + }); + + test('it should validate just a list_id where it decodes into an array for list_id and adds a namespace_type of "single"', () => { + const payload: FindListItemSchemaPartial = { list_id: LIST_ID }; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: FindListItemSchemaPartialDecoded = { + cursor: undefined, + filter: undefined, + list_id: LIST_ID, + page: undefined, + per_page: undefined, + sort_field: undefined, + sort_order: undefined, + }; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with page missing', () => { + const payload = getFindListItemSchemaMock(); + delete payload.page; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListItemSchemaDecodedMock(); + delete expected.page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with pre_page missing', () => { + const payload = getFindListItemSchemaMock(); + delete payload.per_page; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListItemSchemaDecodedMock(); + delete expected.per_page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_field missing', () => { + const payload = getFindListItemSchemaMock(); + delete payload.sort_field; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListItemSchemaDecodedMock(); + delete expected.sort_field; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_order missing', () => { + const payload = getFindListItemSchemaMock(); + delete payload.sort_order; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListItemSchemaDecodedMock(); + delete expected.sort_order; + expect(message.schema).toEqual(expected); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: FindListItemSchemaPartial & { + extraKey: string; + } = { ...getFindListItemSchemaMock(), extraKey: 'some new value' }; + const decoded = findListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index c9ece4224c4ce..bbd3c7b5ec642 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -26,6 +26,9 @@ export const findListItemSchema = t.intersection([ ), ]); -export type FindListItemSchemaPartial = Identity>; +export type FindListItemSchemaPartial = Identity>; -export type FindListItemSchema = RequiredKeepUndefined>; +// This type is used after a decode since some things are defaults after a decode. +export type FindListItemSchemaPartialDecoded = RequiredKeepUndefined< + t.TypeOf +>; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts new file mode 100644 index 0000000000000..dcb18dac8cfb6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FILTER } from '../../constants.mock'; + +import { FindListSchema, FindListSchemaEncoded } from './find_list_schema'; + +export const getFindListSchemaMock = (): FindListSchemaEncoded => ({ + filter: FILTER, + page: '1', + per_page: '25', + sort_field: undefined, + sort_order: undefined, +}); + +export const getFindListSchemaDecodedMock = (): FindListSchema => ({ + filter: FILTER, + page: 1, + per_page: 25, + sort_field: undefined, + sort_order: undefined, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts new file mode 100644 index 0000000000000..a343fb4b08bfc --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; +import { FindListSchema, FindListSchemaEncoded, findListSchema } from './find_list_schema'; + +describe('find_list_schema', () => { + test('it should validate a typical find item request', () => { + const payload = getFindListSchemaMock(); + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindListSchemaDecodedMock()); + }); + + test('it should validate and empty object since everything is optional and will respond with an empty object', () => { + const payload: FindListSchemaEncoded = {}; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: FindListSchema = {}; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with page missing', () => { + const payload = getFindListSchemaMock(); + delete payload.page; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListSchemaDecodedMock(); + delete expected.page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with pre_page missing', () => { + const payload = getFindListSchemaMock(); + delete payload.per_page; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListSchemaDecodedMock(); + delete expected.per_page; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with filter missing', () => { + const payload = getFindListSchemaMock(); + delete payload.filter; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListSchemaDecodedMock(); + delete expected.filter; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_field missing', () => { + const payload = getFindListSchemaMock(); + delete payload.sort_field; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListSchemaDecodedMock(); + delete expected.sort_field; + expect(message.schema).toEqual(expected); + }); + + test('it should validate with sort_order missing', () => { + const payload = getFindListSchemaMock(); + delete payload.sort_order; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindListSchemaDecodedMock(); + delete expected.sort_order; + expect(message.schema).toEqual(expected); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: FindListSchemaEncoded & { + extraKey: string; + } = { ...getFindListSchemaMock(), extraKey: 'some new value' }; + const decoded = findListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.mock.ts new file mode 100644 index 0000000000000..82baee2bf848f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.mock.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 { ID, ITEM_ID } from '../../constants.mock'; + +import { ReadEndpointListItemSchema } from './read_endpoint_list_item_schema'; + +export const getReadEndpointListItemSchemaMock = (): ReadEndpointListItemSchema => ({ + id: ID, + item_id: ITEM_ID, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts new file mode 100644 index 0000000000000..70a1d783c87d6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; +import { + ReadEndpointListItemSchema, + readEndpointListItemSchema, +} from './read_endpoint_list_item_schema'; + +describe('read_endpoint_list_item_schema', () => { + test('it should validate a typical list request', () => { + const payload = getReadEndpointListItemSchemaMock(); + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "id"', () => { + const payload = getReadEndpointListItemSchemaMock(); + delete payload.id; + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "item_id"', () => { + const payload = getReadEndpointListItemSchemaMock(); + delete payload.item_id; + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT accept "namespace_type" since endpoint list items do not need it', () => { + const payload: ReadEndpointListItemSchema & { + namespace_type: string; + } = { ...getReadEndpointListItemSchemaMock(), namespace_type: 'single' }; + delete payload.namespace_type; + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getReadEndpointListItemSchemaMock()); + }); + + test('it should accept an undefined for "id", "item_id"', () => { + const payload = getReadEndpointListItemSchemaMock(); + delete payload.id; + delete payload.item_id; + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ReadEndpointListItemSchema & { + extraKey?: string; + } = getReadEndpointListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = readEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.ts new file mode 100644 index 0000000000000..30bbbe2d22ea4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.mock.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 { + COMMENTS, + DESCRIPTION, + ENTRIES, + ID, + ITEM_TYPE, + LIST_ITEM_ID, + META, + NAME, + TAGS, + _TAGS, +} from '../../constants.mock'; + +import { UpdateEndpointListItemSchema } from './update_endpoint_list_item_schema'; + +export const getUpdateEndpointListItemSchemaMock = (): UpdateEndpointListItemSchema => ({ + _tags: _TAGS, + comments: COMMENTS, + description: DESCRIPTION, + entries: ENTRIES, + id: ID, + item_id: LIST_ITEM_ID, + meta: META, + name: NAME, + tags: TAGS, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts new file mode 100644 index 0000000000000..838cb81d84c1d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + UpdateEndpointListItemSchema, + updateEndpointListItemSchema, +} from './update_endpoint_list_item_schema'; +import { getUpdateEndpointListItemSchemaMock } from './update_endpoint_list_item_schema.mock'; + +describe('update_endpoint_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getUpdateEndpointListItemSchemaMock(); + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not accept an undefined for "description"', () => { + const payload = getUpdateEndpointListItemSchemaMock(); + delete payload.description; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "name"', () => { + const payload = getUpdateEndpointListItemSchemaMock(); + delete payload.name; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "type"', () => { + const payload = getUpdateEndpointListItemSchemaMock(); + delete payload.type; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept a value for "list_id"', () => { + const payload: UpdateEndpointListItemSchema & { + list_id?: string; + } = getUpdateEndpointListItemSchemaMock(); + payload.list_id = 'some new list_id'; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "list_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta" but strip it out', () => { + const payload = getUpdateEndpointListItemSchemaMock(); + const outputPayload = getUpdateEndpointListItemSchemaMock(); + delete payload.meta; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete outputPayload.meta; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "comments" but return an array', () => { + const inputPayload = getUpdateEndpointListItemSchemaMock(); + const outputPayload = getUpdateEndpointListItemSchemaMock(); + delete inputPayload.comments; + outputPayload.comments = []; + const decoded = updateEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "entries" but return an array', () => { + const inputPayload = getUpdateEndpointListItemSchemaMock(); + const outputPayload = getUpdateEndpointListItemSchemaMock(); + delete inputPayload.entries; + outputPayload.entries = []; + const decoded = updateEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "tags" but return an array', () => { + const inputPayload = getUpdateEndpointListItemSchemaMock(); + const outputPayload = getUpdateEndpointListItemSchemaMock(); + delete inputPayload.tags; + outputPayload.tags = []; + const decoded = updateEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "_tags" but return an array', () => { + const inputPayload = getUpdateEndpointListItemSchemaMock(); + const outputPayload = getUpdateEndpointListItemSchemaMock(); + delete inputPayload._tags; + outputPayload._tags = []; + const decoded = updateEndpointListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: UpdateEndpointListItemSchema & { + extraKey?: string; + } = getUpdateEndpointListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = updateEndpointListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index 69702a5e8a4f9..2592e44888ff6 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -145,20 +145,6 @@ describe('update_exception_list_item_schema', () => { expect(message.schema).toEqual(outputPayload); }); - // TODO: Is it expected behavior for it not to auto-generate a uui or throw - // error if item_id is not passed in? - test.skip('it should accept an undefined for "item_id" and auto generate a uuid', () => { - const inputPayload = getUpdateExceptionListItemSchemaMock(); - delete inputPayload.item_id; - const decoded = updateExceptionListItemSchema.decode(inputPayload); - const checked = exactCheck(inputPayload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect((message.schema as UpdateExceptionListItemSchema).item_id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); - }); - test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => { const inputPayload = getUpdateExceptionListItemSchemaMock(); delete inputPayload.item_id; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index 71cb9786b8185..892f277045a69 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -109,20 +109,6 @@ describe('update_exception_list_schema', () => { expect(message.schema).toEqual(outputPayload); }); - // TODO: Is it expected behavior for it not to auto-generate a uui or throw - // error if list_id is not passed in? - test.skip('it should accept an undefined for "list_id" and auto generate a uuid', () => { - const inputPayload = getUpdateExceptionListSchemaMock(); - delete inputPayload.list_id; - const decoded = updateExceptionListSchema.decode(inputPayload); - const checked = exactCheck(inputPayload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect((message.schema as UpdateExceptionListSchema).list_id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); - }); - test('it should accept an undefined for "list_id" and generate a correct body not counting the uuid', () => { const inputPayload = getUpdateExceptionListSchemaMock(); delete inputPayload.list_id; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.mock.ts new file mode 100644 index 0000000000000..c95de1e99b4f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.mock.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 { ID, META, VALUE } from '../../constants.mock'; + +import { UpdateListItemSchema } from './update_list_item_schema'; + +export const getUpdateListItemSchemaMock = (): UpdateListItemSchema => ({ + id: ID, + meta: META, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts new file mode 100644 index 0000000000000..6127e20343834 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; +import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; + +describe('update_list_item_schema', () => { + test('it should validate a typical list item request', () => { + const payload = getUpdateListItemSchemaMock(); + const decoded = updateListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an undefined for "meta" but strip it out', () => { + const payload = getUpdateListItemSchemaMock(); + const outputPayload = getUpdateListItemSchemaMock(); + delete payload.meta; + const decoded = updateListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete outputPayload.meta; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: UpdateListItemSchema & { + extraKey?: string; + } = getUpdateListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = updateListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 1f51140005e59..d346ea72ca310 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -24,7 +24,7 @@ describe('create_endpoint_list_schema', () => { }); test('it should accept an empty object when an endpoint list already exists', () => { - const payload = {}; + const payload: CreateEndpointListSchema = {}; const decoded = createEndpointListSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -33,6 +33,19 @@ describe('create_endpoint_list_schema', () => { expect(message.schema).toEqual(payload); }); + test('it should NOT accept an undefined for "list_id"', () => { + const payload = getExceptionListSchemaMock(); + delete payload.list_id; + const decoded = createEndpointListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'invalid keys "_tags,["endpoint","process","malware","os:linux"],created_at,created_by,description,id,meta,{},name,namespace_type,tags,["user added string for a tag","malware"],tie_breaker_id,type,updated_at,updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + test('it should NOT allow missing fields', () => { const payload = getExceptionListSchemaMock(); delete payload.list_id; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index ff900104251b7..c8bf73cf842e1 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -99,19 +99,15 @@ describe('exception_list_item_schema', () => { expect(message.schema).toEqual({}); }); - // TODO: Should this throw an error? "namespace_type" gets auto-populated - // with default "single", is that desired behavior? - test.skip('it should NOT accept an undefined for "namespace_type"', () => { + test('it should accept an undefined for "namespace_type" and return "single" as a default value for "namespace_type"', () => { const payload = getExceptionListItemSchemaMock(); delete payload.namespace_type; const decoded = exceptionListItemSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "namespace_type"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as ExceptionListItemSchema).namespace_type).toEqual('single'); }); test('it should NOT accept an undefined for "description"', () => { diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 0de8fd72900af..c8440e9d3f3d0 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -27,7 +27,6 @@ import { } from '../common/schemas'; import { commentsArray, entriesArray } from '../types'; -// TODO: Should we use a partial here to reflect that this can JSON serialize meta, comment as non existent? export const exceptionListItemSchema = t.exact( t.type({ _tags, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index 99b3b778388ea..b773dd498ed01 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -60,19 +60,15 @@ describe('exception_list_schema', () => { expect(message.schema).toEqual({}); }); - // TODO: Should this throw an error? "namespace_type" gets auto-populated - // with default "single", is that desired behavior? - test.skip('it should NOT accept an undefined for "namespace_type"', () => { + test('it should accept an undefined for "namespace_type" and make "namespace_type" that of "single"', () => { const payload = getExceptionListSchemaMock(); delete payload.namespace_type; const decoded = exceptionListSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "namespace_type"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as ExceptionListSchema).namespace_type).toEqual('single'); }); test('it should NOT accept an undefined for "description"', () => { diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 120ed31f87d0d..0fb2bfca4a48f 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -25,7 +25,6 @@ import { updated_by, } from '../common/schemas'; -// TODO: Should we use a partial here to reflect that this can JSON serialize meta as non existent? export const exceptionListSchema = t.exact( t.type({ _tags, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index 5d0f3599729b3..cac69ce65623f 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -5,12 +5,11 @@ */ import { IRouter } from 'kibana/server'; -import * as t from 'io-ts'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; import { validate } from '../../common/siem_common_deps'; -import { exceptionListSchema } from '../../common/schemas'; +import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; @@ -35,21 +34,19 @@ export const createEndpointListRoute = (router: IRouter): void => { async (context, _, response) => { const siemResponse = buildSiemResponse(response); try { - // Our goal is be fast as possible and block the least amount of const exceptionLists = getExceptionListClient(context); const createdList = await exceptionLists.createEndpointList(); - if (createdList != null) { - const [validated, errors] = validate(createdList, t.union([exceptionListSchema, t.null])); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); - } else { - return response.ok({ body: validated ?? {} }); - } + // We always return ok on a create endpoint list route but with an empty body as + // an additional fetch of the full list would be slower and the UI has everything hard coded + // within it to get the list if it needs details about it. Our goal is to be as fast as possible + // and block the least amount of time with this route since it could end up in various parts of the + // stack at some point such as repeatedly being called by endpoint agents. + const body = createdList ?? {}; + const [validated, errors] = validate(body, createEndpointListSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); } else { - // We always return ok on a create endpoint list route but with an empty body as - // an additional fetch of the full list would be slower and the UI has everything hard coded - // within it to get the list if it needs details about it. - return response.ok({ body: {} }); + return response.ok({ body: validated ?? {} }); } } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts index 8b50f4666085a..8148c9b1ed824 100644 --- a/x-pack/plugins/lists/server/routes/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -23,7 +23,6 @@ export const exportListItemRoute = (router: IRouter): void => { path: `${LIST_ITEM_URL}/_export`, validate: { query: buildRouteValidation(exportListItemQuerySchema), - // TODO: Do we want to add a body here like export_rules_route and allow a size limit? }, }, async (context, request, response) => { diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 1ccb948d0ad21..52d534b08df2b 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -9,7 +9,11 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { validate } from '../../common/siem_common_deps'; -import { findListItemSchema, foundListItemSchema } from '../../common/schemas'; +import { + FindListItemSchemaPartialDecoded, + findListItemSchema, + foundListItemSchema, +} from '../../common/schemas'; import { decodeCursor } from '../services/utils'; import { getListClient } from './utils'; @@ -22,7 +26,9 @@ export const findListItemRoute = (router: IRouter): void => { }, path: `${LIST_ITEM_URL}/_find`, validate: { - query: buildRouteValidation(findListItemSchema), + query: buildRouteValidation( + findListItemSchema + ), }, }, async (context, request, response) => { diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index e21a54c09a873..f706559dffdbd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -29,7 +29,6 @@ export const patchListItemRoute = (router: IRouter): void => { try { const { value, id, meta } = request.body; const lists = getListClient(context); - // TODO: This looks like just a regular update, implement a patchListItem API and add plumbing for that. const listItem = await lists.updateListItem({ id, meta, diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 9443ac2ed2eea..3a0d8714a14cd 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -29,7 +29,6 @@ export const patchListRoute = (router: IRouter): void => { try { const { name, description, id, meta } = request.body; const lists = getListClient(context); - // TODO: This looks like just a regular update, implement a patchListItem API and add plumbing for that. const list = await lists.updateList({ description, id, meta, name }); if (list == null) { return siemResponse.error({ diff --git a/x-pack/plugins/lists/server/scripts/post_x_exception_list_items.sh b/x-pack/plugins/lists/server/scripts/post_x_exception_list_items.sh new file mode 100755 index 0000000000000..8e29e96f884a5 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_x_exception_list_items.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +LIST_ID=${1:-simple_list} +COUNT=${2:-100} +LIST_FILE=${3:-exception_lists/new/exception_list_item.json} + +# You can get the first exception list_id like so: +# ./find_exception_lists.sh | jq '.data[0].list_id' + +# Example useage +# ./post_x_exception_list_items.sh +# ./post_x_exception_list_items.sh 5886f4dd-84e5-441b-a7d7-0712f072f9d1 100 + +cat ${LIST_FILE} | jq --arg LIST_ID "$LIST_ID" '.list_id = $LIST_ID' > $TMPDIR/file_to_post.json + +for i in $(seq 1 $COUNT); do + ITEM_ID=`cat /dev/urandom | base64 | tr -dc '0-9a-zA-Z' | head -c100` + cat ${LIST_FILE} | jq --arg LIST_ID "$LIST_ID" --arg ITEM_ID "$ITEM_ID" '.list_id = $LIST_ID | .item_id = $ITEM_ID' > $TMPDIR/file_to_post_${ITEM_ID}.json + ./post_exception_list_item.sh $TMPDIR/file_to_post_${ITEM_ID}.json & +done diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 91e9587aa676a..07bdb933bcc2a 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -74,7 +74,7 @@ export const createListItemsBulk = async ({ const createBody: CreateEsBulkTypeSchema = { create: { _index: listItemIndex } }; return [...accum, createBody, elasticBody]; } else { - // TODO: Report errors with return values from the bulk insert + // TODO: Report errors with return values from the bulk insert into another index or saved object return accum; } }, From 401ad352e958ff4a0c8a006054875e1e381383e4 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 16 Jul 2020 21:13:55 +0200 Subject: [PATCH 15/76] [SIEM] Bring Cypress tests back to life (#71871) * fixes and unskips 'Detection rules, custom' * fixes 'Export rules' test * unksips 'Deleting prebuilt rules' test * unskips Alerts timeline * unskips 'Events viewer' tests * unskips 'toggle column in timeline' tests * unskips 'url timeline' test * fixes Overview test * skips flaky test --- .../cypress/fixtures/overview.json | 5 + .../cypress/integration/alerts.spec.ts | 3 +- .../alerts_detection_rules_custom.spec.ts | 11 +- .../alerts_detection_rules_export.spec.ts | 3 +- .../alerts_detection_rules_prebuilt.spec.ts | 3 +- .../integration/alerts_timeline.spec.ts | 3 +- .../cypress/integration/events_viewer.spec.ts | 3 +- .../cypress/integration/overview.spec.ts | 2 +- .../timeline_toggle_column.spec.ts | 3 +- .../security_solution/cypress/objects/rule.ts | 4 +- .../test_files/expected_rules_export.ndjson | 2 +- .../custom_rule_with_timeline/data.json.gz | Bin 74563 -> 0 bytes .../custom_rule_with_timeline/mappings.json | 6418 ----------------- .../es_archives/export_rule/data.json.gz | Bin 28975 -> 1911 bytes .../es_archives/export_rule/mappings.json | 1421 +--- 15 files changed, 412 insertions(+), 7469 deletions(-) delete mode 100644 x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz delete mode 100644 x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/fixtures/overview.json b/x-pack/plugins/security_solution/cypress/fixtures/overview.json index 893661a693043..69594b88b7515 100644 --- a/x-pack/plugins/security_solution/cypress/fixtures/overview.json +++ b/x-pack/plugins/security_solution/cypress/fixtures/overview.json @@ -33,6 +33,11 @@ "winlogbeatMWSysmonOperational": 30, "__typename": "OverviewHostData" }, + "status": { + "indicesExist": true, + "indexFields": [], + "__typename": "SourceStatus" + }, "__typename": "Source" } } diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 67186e1087d44..3de57b085a9c6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// Flaky: https://github.com/elastic/kibana/issues/70727 -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index a51ad4388c428..ba1de0e40e270 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule, totalNumberOfPrebuiltRulesInEsArchiveCustomRule } from '../objects/rule'; +import { newRule } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -64,14 +64,13 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// Flaky: https://github.com/elastic/kibana/issues/67814 -describe.skip('Detection rules, custom', () => { +describe('Detection rules, custom', () => { before(() => { - esArchiverLoad('custom_rule_with_timeline'); + esArchiverLoad('timeline'); }); after(() => { - esArchiverUnload('custom_rule_with_timeline'); + esArchiverUnload('timeline'); }); it('Creates and activates a new custom rule', () => { @@ -90,7 +89,7 @@ describe.skip('Detection rules, custom', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchiveCustomRule + 1; + const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index a7e6652613493..eb8448233c624 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,8 +17,7 @@ import { DETECTIONS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -// Flakky: https://github.com/elastic/kibana/issues/69849 -describe.skip('Export rules', () => { +describe('Export rules', () => { before(() => { esArchiverLoad('export_rule'); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts index 17f905b091e08..986a7c7177a79 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts @@ -67,8 +67,7 @@ describe('Alerts rules, prebuilt rules', () => { }); }); -// FLAKY: https://github.com/elastic/kibana/issues/71814 -describe.skip('Deleting prebuilt rules', () => { +describe('Deleting prebuilt rules', () => { beforeEach(() => { const expectedNumberOfRules = totalNumberOfPrebuiltRules; const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index b37aabf4825fc..2fed23755963b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -17,8 +17,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// Flakky: https://github.com/elastic/kibana/issues/71220 -describe.skip('Alerts timeline', () => { +describe('Alerts timeline', () => { beforeEach(() => { esArchiverLoad('timeline_alerts'); loginAndWaitForPage(DETECTIONS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 843d99cf06cab..84ca1e20e9576 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -46,8 +46,7 @@ const defaultHeadersInDefaultEcsCategory = [ { id: 'destination.ip' }, ]; -// Flakky: https://github.com/elastic/kibana/issues/70757 -describe.skip('Events Viewer', () => { +describe('Events Viewer', () => { context('Fields rendering', () => { before(() => { loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 6fb3840d89764..b799d487acd08 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -11,7 +11,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; -describe.skip('Overview Page', () => { +describe('Overview Page', () => { before(() => { cy.stubSecurityApi('overview'); loginAndWaitForPage(OVERVIEW_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 759eec69bc022..12e6f3db9b61e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -24,8 +24,7 @@ import { import { HOSTS_URL } from '../urls/navigation'; -// Flaky: https://github.com/elastic/kibana/issues/71361 -describe.skip('toggle column in timeline', () => { +describe('toggle column in timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index c9d3af57e5e59..a30fddc3c3a69 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -59,7 +59,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'host.name: * ', + customQuery: 'host.name:*', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -69,7 +69,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', - timelineId: '3270f530-bc84-11ea-b73f-89980a6a1ce7', + timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson index 7baa59fb3d8c0..b0ef52fc0b223 100644 --- a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson @@ -1,2 +1,2 @@ -{"author":[],"actions":[],"created_at":"2020-07-03T10:44:10.567Z","updated_at":"2020-07-03T10:44:10.941Z","created_by":"elastic","description":"Export rule","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"ad65b1b6-be18-4e41-9d0a-89d8576053d8","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"50a3776b-144d-4cff-9f1f-1173e0d5d4a4","language":"kuery","license":"","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"risk_score_mapping":[],"rule_name_override":"","name":"Export rule","query":"host.name: * ","references":[],"meta":{"from":"1m","kibana_siem_app_url":"http://localhost:5620/app/security"},"severity":"low","severity_mapping":[],"updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[],"throttle":"no_actions","timestamp_override":"","version":1,"exceptions_list":[]} +{"author":[],"actions":[],"created_at":"2020-07-15T10:45:15.954Z","updated_at":"2020-07-15T10:45:16.874Z","created_by":"elastic","description":"Export rule","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"a4772daa-a3ef-44a3-8035-6abd1bf2893a","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","logs-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"1b08a0c2-8243-463e-92c2-b464a8c97cd4","language":"kuery","license":"","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"risk_score_mapping":[],"name":"Export rule","query":"host.name: *","references":[],"meta":{"from":"1m","kibana_siem_app_url":"http://localhost:5620/app/security/detections"},"severity":"low","severity_mapping":[],"updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1,"exceptions_list":[]} {"exported_count":1,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz deleted file mode 100644 index b3a94c77c11846e2e6d34e2908022b2e943f551b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74563 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 Z58M!_J$|-S>Np0>xWf^WZDC*LP&cj<=>%APGva zAd(?KpOyc<4T@4oiL_-osmZGB2cZvie|?|;b%lg|2cfP?a=Gsl3(v#^-mt$tJATV zWJjlC)6n9*)2;pv(?5+)Pu%D(L|fOHPW+Cwb-rVs)7!5s3v~UalNb^IY#ROH`E7r% zb9=sTWNh!;LWgI!kL>FuH^a-b%i&*tEqi%-ACXkLN*LyX`ehCuu*@b?GADkPQt{Z2 zStdcQ@TCL`5^Tz7w7RH^RAj4~d7+`6VfHk$?V83-jlkAp!^R}S9wRbFBF#vNFv$ej zWDE!)#B7lg97J5;$cOMY=RkxfdFt*yBylQ~G-IT^b%Iko;gz(KSY=`qrxl$Qt-#`C zlm#>zZn4-OUrg&G${4FK1%&%4O>zJmZIA#w_$24zN0f^f_sxMgi#Zkaj_{XRDH9}p zg(rZF?3-phQ(j4A^3J-EZT_TYv z7(&FB3E&K=7h&;`FRv!pACrbn?r8)<1+5VphNEGIhk5plGm<`5v0>CF5znbr!UR9` zizd@kG=!=|BJe`Cm7<_c7ELYsqHJ9QoFs!RmDSK136b#`fCo5v<$3i%cAQkRoq)C z2}w$zTwl(}l=5-kk5g%wiRwwm;3?GJ%X*;7rhQ-Ykb{xmRh_P(tA?c$)$PJp&#(;a z_^#~-Mr%p-1bjHXBWX&5T8ucpgANqbk_gB_4CLl4zHikMSMfoNWaoUDg-!^g5vjLi zM!ledktPOcqLbt$S4CX3h^i;Eso?8!aey*DZF<^9P3pC{KO#ZK$YSHA5p*mVE~Hv> z(t1%gxWIfojj&-Fj_VVo`j+FUnnqyyFx{?-bR1$ov@H#V<*qOAa4`v6gF(JIr1(IF ze?WuY+Y_gxWZpY_dFO1^o=yI7Ua$(s3H=O@g+p*#N&)L6i$p3E}8_%nck@Le)WpEg(co@af`5qh% zNeu7~9`wYCM;b(D1m~tq)g}2J?%xdN6yWhnL z#KoY%tOlF!JTu%EiSyZL^*vuiM^G~Gz~3=R3%fKe2t3>J)h>2D)$kmodRCyTfvxG- z#wO7{qoTgBbmmWMxgr*jTsNtkl40dEhfv$-9cd2Jc1VXktBu-7#Vi?NZJnf^s;TC1 zKAWCoa-?t!%PuDyxb!hjCs_2vlicC*V*9OKWxnG(+e)ElZ2&opc%)aIM&1P7%WmfC zr6#Lwt>NWN{JF8xFGiFr@Fzw@DZEf}see`41LgVU=p(d|EG4gBbao0rmJFVuI<;(l z*ySuZA1+lA4Pm}e<&mit_i-o>V%lzZmE-tL0ecPdS1nuT`JnNHmWnqe%X9{L!@I{^ zyrdk)s{yXg>ouW!-NTjpm;KITy69hy>|Z~jak&WDn(KB=!#0rXYX5ALS`AelfQ13z zJ*Z|>g)3qT58!5jXV;|1Pn2-Lg9KguQ6j)GB`^q(3 zIR~0CuzpuIlZ25g;vG`aF?GAGTgq>T`xnPY9~4H%MA;+$IR0JPh9sPjj-x{)4GrmS zO|z6fRBB2Kv7b<@Gc@M@mLQ%lEkrbnMCn5DZMB#fy)HCx!+b|f;GzbX{XEwO8HZR5 zN{<_u^}3En^icod?e3L*7n)g;6tA2MFL%AtLqPcfjYgRu{50yrgi2)fKppF@X_Y?Z z$sx)n9!XEbZ7^vpV~Q$+UvZ194}jBlE&XQw`^PIdtegmCJAO(c*rxC4)-RF>BdF@v zBCffXQI5FtgCbroxqmX^(hloqM%*NUUJpQaVntB-W?-< xLDFU@fKNkh2h~6E&12KBLryNdYc_wKD?zdIsy70;m8n%L=YRStDGUA`001%rx*-4n literal 28975 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 Date: Thu, 16 Jul 2020 12:17:39 -0700 Subject: [PATCH 16/76] Add plugin list to docs that is generated from the repo (#71731) Co-authored-by: Stacey Gammon Co-authored-by: spalger Co-authored-by: Elastic Machine --- .../src/test/prChanges.groovy | 13 + .../architecture/code-exploration.asciidoc | 586 ++++++++++++++++++ docs/developer/architecture/index.asciidoc | 3 + packages/kbn-dev-utils/package.json | 1 + packages/kbn-dev-utils/src/index.ts | 2 + .../src/plugin_list/discover_plugins.ts | 69 +++ .../src/plugin_list/generate_plugin_list.ts | 84 +++ .../kbn-dev-utils/src/plugin_list/index.ts | 20 + .../src/plugin_list/run_plugin_list_cli.ts | 49 ++ ...simple_kibana_platform_plugin_discovery.ts | 86 +++ .../src/optimizer/kibana_platform_plugins.ts | 87 +-- scripts/build_plugin_list_docs.js | 21 + src/dev/ci_setup/setup.sh | 16 + vars/prChanges.groovy | 15 +- 14 files changed, 986 insertions(+), 66 deletions(-) create mode 100644 docs/developer/architecture/code-exploration.asciidoc create mode 100644 packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts create mode 100644 packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts create mode 100644 packages/kbn-dev-utils/src/plugin_list/index.ts create mode 100644 packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts create mode 100644 packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts create mode 100644 scripts/build_plugin_list_docs.js diff --git a/.ci/pipeline-library/src/test/prChanges.groovy b/.ci/pipeline-library/src/test/prChanges.groovy index 0fb750d6ff64e..f149340517ff0 100644 --- a/.ci/pipeline-library/src/test/prChanges.groovy +++ b/.ci/pipeline-library/src/test/prChanges.groovy @@ -84,4 +84,17 @@ class PrChangesTest extends KibanaBasePipelineTest { assertFalse(prChanges.areChangesSkippable()) } + + @Test + void 'areChangesSkippable() with skippable changes that are in notSkippablePaths'() { + props([ + githubPrs: [ + getChanges: { [ + [filename: 'docs/developer/architecture/code-exploration.asciidoc'], + ] }, + ], + ]) + + assertFalse(prChanges.areChangesSkippable()) + } } diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc new file mode 100644 index 0000000000000..23ba1c54d27d3 --- /dev/null +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -0,0 +1,586 @@ +//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within packages/kbn-dev-utils/target/plugin_list/generate_plugin_list.js + +//// + +[[code-exploration]] +=== Exploring Kibana code + +The goals of our folder heirarchy are: + +- Easy for developers to know where to add new services, plugins and applications. +- Easy for developers to know where to find the code from services, plugins and applications. +- Easy to browse and understand our folder structure. + +To that aim, we strive to: + +- Avoid too many files in any given folder. +- Choose clear, unambigious folder names. +- Organize by domain. +- Every folder should contain a README that describes the contents of that folder. + +[discrete] +[[kibana-services-applications]] +==== Services and Applications + +[discrete] +===== src/plugins + +- {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/bfetch/README.md[bfetch] + +bfetch allows to batch HTTP requests and streams responses back. + + +- {kib-repo}blob/{branch}/src/plugins/charts/README.md[charts] + +The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. + + +- {kib-repo}blob/{branch}/src/plugins/console[console] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/dashboard[dashboard] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/data/README.md[data] + +data plugin provides common data access services. + + +- {kib-repo}blob/{branch}/src/plugins/dev_tools/README.md[devTools] + +The ui/registry/dev_tools is removed in favor of the devTools plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches /app/dev_tools#/. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + + +- {kib-repo}blob/{branch}/src/plugins/discover[discover] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/embeddable/README.md[embeddable] + +Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers. + + +- {kib-repo}blob/{branch}/src/plugins/es_ui_shared[esUiShared] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions] + +This plugin provides methods which will parse & execute an expression pipeline +string for you, as well as a series of registries for advanced users who might +want to incorporate their own functions, types, and renderers into the service +for use in their own application. + + +- {kib-repo}blob/{branch}/src/plugins/home/README.md[home] + +Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. + + +- {kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/input_control_vis[inputControlVis] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/inspector/README.md[inspector] + +The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] + +This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] + +Tools for building React applications in Kibana. + + +- {kib-repo}blob/{branch}/src/plugins/kibana_usage_collection/README.md[kibanaUsageCollection] + +This plugin registers the basic usage collectors from Kibana: + + +- {kib-repo}blob/{branch}/src/plugins/kibana_utils/README.md[kibanaUtils] + +Utilities for building Kibana plugins. + + +- {kib-repo}blob/{branch}/src/plugins/legacy_export[legacyExport] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/management[management] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] + +The navigation plugins exports the TopNavMenu component. +It also provides a stateful version of it on the start contract. + + +- {kib-repo}blob/{branch}/src/plugins/newsfeed[newsfeed] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/region_map[regionMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects[savedObjects] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/saved_objects_management[savedObjectsManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/share/README.md[share] + +Replaces the legacy ui/share module for registering share context menus. + + +- {kib-repo}blob/{branch}/src/plugins/status_page[statusPage] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_collection_manager/README.md[telemetryCollectionManager] + +Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting. + + +- {kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] + +This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). + + +- {kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/timelion[timelion] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/ui_actions/README.md[uiActions] + +An API for: + + +- {kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_markdown[visTypeMarkdown] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_metric[visTypeMetric] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_table[visTypeTable] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_tagcloud[visTypeTagcloud] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timelion/README.md[visTypeTimelion] + +If your grammar was changed in public/chain.peg you need to re-generate the static parser. You could use a grunt task: + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_timeseries[visTypeTimeseries] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vega[visTypeVega] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_vislib[visTypeVislib] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/vis_type_xy[visTypeXy] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/visualizations[visualizations] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/src/plugins/visualize[visualize] + +WARNING: Missing README. + + +[discrete] +===== x-pack/plugins + +- {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] + +The Kibana actions plugin provides a framework to create executable actions. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] + +This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the built-in alertTypes with the alerting plugin, register associated HTTP +routes, etc. + + +- {kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] + +The Kibana alerting plugin provides a common place to set up alerts. You can: + + +- {kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] + +To access an elasticsearch instance that has live data you have two options: + + +- {kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/beats_management[beats_management] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] + +"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun + + +- {kib-repo}blob/{branch}/x-pack/plugins/case/README.md[case] + +Experimental Feature + + +- {kib-repo}blob/{branch}/x-pack/plugins/cloud[cloud] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/code[code] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] + +You can run a local cluster and simulate a remote cluster within a single Kibana directory. + + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_enhanced/README.md[dashboardEnhanced] + +- {kib-repo}blob/{branch}/x-pack/plugins/dashboard_mode[dashboardMode] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced[discoverEnhanced] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced/README.md[embeddableEnhanced] + +- {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] + +The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with +security and spaces filtering as well as performing audit logging. + + +- {kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] + +This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: + + +- {kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] + +The purpose of this plugin is to provide a way to persist a history of events +occuring in Kibana, initially just for the Make It Action project - alerts +and actions. + + +- {kib-repo}blob/{branch}/x-pack/plugins/features[features] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] + +The GlobalSearch plugin provides an easy way to search for various objects, such as applications +or dashboards from the Kibana instance, from both server and client-side plugins + + +- {kib-repo}blob/{branch}/x-pack/plugins/global_search_providers[globalSearchProviders] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/graph/README.md[graph] + +This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. + + +- {kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] + +- {kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] + +You can test that the Frozen badge, phase filtering, and lifecycle information is surfaced in +Index Management by running this series of requests in Console: + + +- {kib-repo}blob/{branch}/x-pack/plugins/index_management[indexManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] + +This is the home of the infra plugin, which aims to provide a solution for +the infrastructure monitoring use-case within Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] + +Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) + + +- {kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] + +The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] + +Run all tests from the x-pack root directory + + +- {kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] + +The licensing plugin retrieves license data from Elasticsearch at regular configurable intervals. + + +- {kib-repo}blob/{branch}/x-pack/plugins/lists/README.md[lists] + +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + + +- {kib-repo}blob/{branch}/x-pack/plugins/logstash[logstash] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps/README.md[maps] + +Visualize geo data from Elasticsearch or 3rd party geo-services. + + +- {kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] + +This plugin provides access to the detailed tile map services from Elastic. + + +- {kib-repo}blob/{branch}/x-pack/plugins/ml[ml] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability] + +This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. + + +- {kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] + +An awesome Kibana reporting plugin + + +- {kib-repo}blob/{branch}/x-pack/plugins/rollup/README.md[rollup] + +Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + + +- {kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] + +See Configuring security in Kibana. + + +- {kib-repo}blob/{branch}/x-pack/plugins/security_solution[securitySolution] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/snapshot_restore/README.md[snapshotRestore] + +or + + +- {kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] + +Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. + + +- {kib-repo}blob/{branch}/x-pack/plugins/transform[transform] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/translations[translations] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] + +The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. +As a developer you can reuse and extend built-in alerts and actions UI functionality: + + +- {kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] + +- {kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] + +WARNING: Missing README. + + +- {kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] + +The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening +in their infrastructure. + + +- {kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] + +This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): + diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index b7ca1cb352b0b..2e6ab1a4ad6ac 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -17,9 +17,12 @@ A few notable services are called out below. * <> * <> * <> +* <> include::add-data-tutorials.asciidoc[leveloffset=+1] include::development-visualize-index.asciidoc[leveloffset=+1] include::security/index.asciidoc[leveloffset=+1] + +include::code-exploration.asciidoc[leveloffset=+1] diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index d95cd1d404a1b..b307bd41bb4dd 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -17,6 +17,7 @@ "exit-hook": "^2.2.0", "getopts": "^2.2.5", "load-json-file": "^6.2.0", + "normalize-path": "^3.0.0", "moment": "^2.24.0", "rxjs": "^6.5.5", "tree-kill": "^1.2.2", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index fad5f85ab5890..3e9e6238df9dc 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -39,3 +39,5 @@ export { KbnClient } from './kbn_client'; export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; +export * from './plugin_list'; +export * from './simple_kibana_platform_plugin_discovery'; diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts new file mode 100644 index 0000000000000..733b9f23a5394 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -0,0 +1,69 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; + +import MarkdownIt from 'markdown-it'; +import cheerio from 'cheerio'; + +import { REPO_ROOT } from '../repo_root'; +import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; + +export interface Plugin { + id: string; + relativeDir?: string; + relativeReadmePath?: string; + readmeSnippet?: string; +} + +export type Plugins = Plugin[]; + +const getReadmeName = (directory: string) => + Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.md'); + +export const discoverPlugins = (pluginsRootDir: string): Plugins => + simpleKibanaPlatformPluginDiscovery([pluginsRootDir], []).map( + ({ directory, manifest: { id } }): Plugin => { + const readmeName = getReadmeName(directory); + + let relativeReadmePath: string | undefined; + let readmeSnippet: string | undefined; + if (readmeName) { + const readmePath = Path.resolve(directory, readmeName); + relativeReadmePath = Path.relative(REPO_ROOT, readmePath); + + const md = new MarkdownIt(); + const parsed = md.render(Fs.readFileSync(readmePath, 'utf8')); + const $ = cheerio.load(parsed); + + const firstParagraph = $('p')[0]; + if (firstParagraph) { + readmeSnippet = $(firstParagraph).text(); + } + } + + return { + id, + relativeReadmePath, + relativeDir: relativeReadmePath || Path.relative(REPO_ROOT, directory), + readmeSnippet, + }; + } + ); diff --git a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts new file mode 100644 index 0000000000000..f3f8817299bb1 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts @@ -0,0 +1,84 @@ +/* + * 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 Path from 'path'; + +import normalizePath from 'normalize-path'; + +import { REPO_ROOT } from '../repo_root'; +import { Plugins } from './discover_plugins'; + +function* printPlugins(plugins: Plugins) { + for (const plugin of plugins) { + const path = plugin.relativeReadmePath || plugin.relativeDir; + yield ''; + yield `- {kib-repo}blob/{branch}/${path}[${plugin.id}]`; + + if (!plugin.relativeReadmePath || plugin.readmeSnippet) { + yield ''; + yield plugin.readmeSnippet || 'WARNING: Missing README.'; + yield ''; + } + } +} + +export function generatePluginList(ossPlugins: Plugins, xpackPlugins: Plugins) { + return `//// + +NOTE: + This is an automatically generated file. Please do not edit directly. Instead, run the + following from within the kibana repository: + + node scripts/build_plugin_list_docs + + You can update the template within ${normalizePath( + Path.relative(REPO_ROOT, Path.resolve(__dirname, __filename)) + )} + +//// + +[[code-exploration]] +=== Exploring Kibana code + +The goals of our folder heirarchy are: + +- Easy for developers to know where to add new services, plugins and applications. +- Easy for developers to know where to find the code from services, plugins and applications. +- Easy to browse and understand our folder structure. + +To that aim, we strive to: + +- Avoid too many files in any given folder. +- Choose clear, unambigious folder names. +- Organize by domain. +- Every folder should contain a README that describes the contents of that folder. + +[discrete] +[[kibana-services-applications]] +==== Services and Applications + +[discrete] +===== src/plugins +${Array.from(printPlugins(ossPlugins)).join('\n')} + +[discrete] +===== x-pack/plugins +${Array.from(printPlugins(xpackPlugins)).join('\n')} +`; +} diff --git a/packages/kbn-dev-utils/src/plugin_list/index.ts b/packages/kbn-dev-utils/src/plugin_list/index.ts new file mode 100644 index 0000000000000..7e964f92b089b --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/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 * from './run_plugin_list_cli'; diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts new file mode 100644 index 0000000000000..817534ba5b154 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts @@ -0,0 +1,49 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; + +import { run } from '../run'; +import { REPO_ROOT } from '../repo_root'; + +import { discoverPlugins } from './discover_plugins'; +import { generatePluginList } from './generate_plugin_list'; + +const OSS_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'src/plugins'); +const XPACK_PLUGIN_DIR = Path.resolve(REPO_ROOT, 'x-pack/plugins'); +const OUTPUT_PATH = Path.resolve( + REPO_ROOT, + 'docs/developer/architecture/code-exploration.asciidoc' +); + +export function runPluginListCli() { + run(async ({ log }) => { + log.info('looking for oss plugins'); + const ossPlugins = discoverPlugins(OSS_PLUGIN_DIR); + log.success(`found ${ossPlugins.length} plugins`); + + log.info('looking for x-pack plugins'); + const xpackPlugins = discoverPlugins(XPACK_PLUGIN_DIR); + log.success(`found ${xpackPlugins.length} plugins`); + + log.info('writing plugin list to', OUTPUT_PATH); + Fs.writeFileSync(OUTPUT_PATH, generatePluginList(ossPlugins, xpackPlugins)); + }); +} diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts new file mode 100644 index 0000000000000..c7155b2b3c51b --- /dev/null +++ b/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts @@ -0,0 +1,86 @@ +/* + * 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 Path from 'path'; + +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly manifestPath: string; + readonly manifest: { + id: string; + [key: string]: unknown; + }; +} + +/** + * Helper to find the new platform plugins. + */ +export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], paths: string[]) { + const patterns = Array.from( + new Set([ + // find kibana.json files up to 5 levels within the scan dir + ...scanDirs.reduce( + (acc: string[], dir) => [ + ...acc, + `${dir}/*/kibana.json`, + `${dir}/*/*/kibana.json`, + `${dir}/*/*/*/kibana.json`, + `${dir}/*/*/*/*/kibana.json`, + `${dir}/*/*/*/*/*/kibana.json`, + ], + [] + ), + ...paths.map((path) => `${path}/kibana.json`), + ]) + ); + + const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => + // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + Path.resolve(path) + ); + + return manifestPaths.map( + (manifestPath): KibanaPlatformPlugin => { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + manifestPath, + manifest: { + ...manifest, + id: manifest.id, + }, + }; + } + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index b489c53be47b9..a848d779dc9a2 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -17,10 +17,7 @@ * under the License. */ -import Path from 'path'; - -import globby from 'globby'; -import loadJsonFile from 'load-json-file'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; export interface KibanaPlatformPlugin { readonly directory: string; @@ -30,72 +27,32 @@ export interface KibanaPlatformPlugin { readonly extraPublicDirs: string[]; } +const isArrayOfStrings = (input: any): input is string[] => + Array.isArray(input) && input.every((p) => typeof p === 'string'); + /** * Helper to find the new platform plugins. */ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { - return globby - .sync( - Array.from( - new Set([ - ...scanDirs.map(nestedScanDirPaths).reduce((dirs, current) => [...dirs, ...current], []), - ...paths.map((path) => `${path}/kibana.json`), - ]) - ), - { - absolute: true, + return simpleKibanaPlatformPluginDiscovery(scanDirs, paths).map( + ({ directory, manifestPath, manifest }): KibanaPlatformPlugin => { + let extraPublicDirs: string[] | undefined; + if (manifest.extraPublicDirs) { + if (!isArrayOfStrings(manifest.extraPublicDirs)) { + throw new TypeError( + 'expected new platform plugin manifest to have an array of strings `extraPublicDirs` property' + ); + } + extraPublicDirs = manifest.extraPublicDirs; } - ) - .map((path) => - // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this - readKibanaPlatformPlugin(Path.resolve(path)) - ); -} - -function nestedScanDirPaths(dir: string): string[] { - // down to 5 level max - return [ - `${dir}/*/kibana.json`, - `${dir}/*/*/kibana.json`, - `${dir}/*/*/*/kibana.json`, - `${dir}/*/*/*/*/kibana.json`, - `${dir}/*/*/*/*/*/kibana.json`, - ]; -} -function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { - if (!Path.isAbsolute(manifestPath)) { - throw new TypeError('expected new platform manifest path to be absolute'); - } - - const manifest = loadJsonFile.sync(manifestPath); - if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { - throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); - } - - if (typeof manifest.id !== 'string') { - throw new TypeError('expected new platform plugin manifest to have a string id'); - } - - let extraPublicDirs: string[] | undefined; - if (manifest.extraPublicDirs) { - if ( - !Array.isArray(manifest.extraPublicDirs) || - !manifest.extraPublicDirs.every((p) => typeof p === 'string') - ) { - throw new TypeError( - 'expected new platform plugin manifest to have an array of strings `extraPublicDirs` property' - ); + return { + directory, + manifestPath, + id: manifest.id, + isUiPlugin: !!manifest.ui, + extraPublicDirs: extraPublicDirs || [], + }; } - - extraPublicDirs = manifest.extraPublicDirs as string[]; - } - - return { - directory: Path.dirname(manifestPath), - manifestPath, - id: manifest.id, - isUiPlugin: !!manifest.ui, - extraPublicDirs: extraPublicDirs || [], - }; + ); } diff --git a/scripts/build_plugin_list_docs.js b/scripts/build_plugin_list_docs.js new file mode 100644 index 0000000000000..54821a1b10ee8 --- /dev/null +++ b/scripts/build_plugin_list_docs.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env/prebuilt_dev_only_entry'); +require('@kbn/dev-utils').runPluginListCli(); diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index dc91d1cf23a37..25d2afb00cd87 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -65,3 +65,19 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi + +### +### rebuild plugin list to ensure it's not out of date +### +echo " -- building plugin list docs" +node scripts/build_plugin_list_docs + +### +### verify no git modifications +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'node scripts/build_plugin_list_docs' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy index 414e651177277..adaacf952b5b6 100644 --- a/vars/prChanges.groovy +++ b/vars/prChanges.groovy @@ -2,6 +2,8 @@ import groovy.transform.Field public static @Field PR_CHANGES_CACHE = [] +// if all the changed files in a PR match one of these regular +// expressions then CI will be skipped for that PR def getSkippablePaths() { return [ /^docs\//, @@ -14,6 +16,15 @@ def getSkippablePaths() { ] } +// exclusion regular expressions that will invalidate paths that +// match one of the skippable path regular expressions +def getNotSkippablePaths() { + return [ + // this file is auto-generated and changes to it need to be validated with CI + /^docs\/developer\/architecture\/code-exploration.asciidoc$/, + ] +} + def areChangesSkippable() { if (!githubPr.isPr()) { return false @@ -21,6 +32,7 @@ def areChangesSkippable() { try { def skippablePaths = getSkippablePaths() + def notSkippablePaths = getNotSkippablePaths() def files = getChangedFiles() // 3000 is the max files GH API will return @@ -29,7 +41,8 @@ def areChangesSkippable() { } files = files.findAll { file -> - return !skippablePaths.find { regex -> file =~ regex} + def skippable = skippablePaths.find { regex -> file =~ regex} && !notSkippablePaths.find { regex -> file =~ regex } + return !skippable } return files.size() < 1 From 52597b203b1da74403a782bdc1ca9817b5f950bf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 16 Jul 2020 13:31:43 -0600 Subject: [PATCH 17/76] [Maps] typescript vector style phase 1 (#69994) * [Maps] typescript vector style phase 1 * tslint * unify Ordinal and Categorical field meta since they are mixed in real data * field formatter type * review feedback Co-authored-by: Elastic Machine --- .../data_request_descriptor_types.ts | 16 ++ .../maps/public/classes/joins/inner_join.d.ts | 2 + .../plugins/maps/public/classes/joins/join.ts | 2 + .../blended_vector_layer.ts | 96 +++++----- .../sources/es_agg_source/es_agg_source.d.ts | 2 + .../classes/sources/es_source/es_source.d.ts | 9 +- .../categorical_field_meta_popover.tsx | 7 +- .../field_meta/ordinal_field_meta_popover.tsx | 16 +- .../symbol/icon_map_select.test.tsx | 5 +- .../components/symbol/icon_map_select.tsx | 4 +- .../properties/dynamic_color_property.test.js | 2 +- .../properties/dynamic_style_property.d.ts | 37 ---- ...property.js => dynamic_style_property.tsx} | 167 +++++++++++------- .../vector/properties/style_property.ts | 22 +-- .../classes/styles/vector/vector_style.d.ts | 10 +- .../classes/styles/vector/vector_style.js | 9 +- 16 files changed, 228 insertions(+), 178 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts rename x-pack/plugins/maps/public/classes/styles/vector/properties/{dynamic_style_property.js => dynamic_style_property.tsx} (54%) diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index e73126ef10d3d..cd7d2d5d0f461 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -64,6 +64,22 @@ export type DataMeta = Partial & Partial & Partial; +type NumericalStyleFieldData = { + avg: number; + max: number; + min: number; + std_deviation: number; +}; + +type CategoricalStyleFieldData = { + buckets: Array<{ key: string; doc_count: number }>; +}; + +export type StyleMetaData = { + // key is field name for field requiring style meta + [key: string]: NumericalStyleFieldData | CategoricalStyleFieldData; +}; + export type DataRequestDescriptor = { dataId: string; dataMetaAtStart?: DataMeta | null; diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts index 6da72fbbd8227..452a3fdc3a19f 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.d.ts @@ -15,4 +15,6 @@ export class InnerJoin implements IJoin { getRightJoinSource(): IESTermSource; toDescriptor(): JoinDescriptor; + + getSourceMetaDataRequestId(): string; } diff --git a/x-pack/plugins/maps/public/classes/joins/join.ts b/x-pack/plugins/maps/public/classes/joins/join.ts index 6554f7581d99c..4a551c5bcc485 100644 --- a/x-pack/plugins/maps/public/classes/joins/join.ts +++ b/x-pack/plugins/maps/public/classes/joins/join.ts @@ -11,4 +11,6 @@ export interface IJoin { getRightJoinSource(): IESTermSource; toDescriptor(): JoinDescriptor; + + getSourceMetaDataRequestId(): string; } diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 5388a82e5924d..aefa2beede7d1 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -33,6 +33,7 @@ import { VectorStyleDescriptor, SizeDynamicOptions, DynamicStylePropertyOptions, + StylePropertyOptions, VectorLayerDescriptor, } from '../../../../common/descriptor_types'; import { IStyle } from '../../styles/style'; @@ -44,7 +45,7 @@ interface CountData { isSyncClustered: boolean; } -function getAggType(dynamicProperty: IDynamicStyleProperty): AGG_TYPE { +function getAggType(dynamicProperty: IDynamicStyleProperty): AGG_TYPE { return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS; } @@ -100,52 +101,57 @@ function getClusterStyleDescriptor( }, }, }; - 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; - } + 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 (styleName === VECTOR_STYLES.SYMBOLIZE_AS || styleName === VECTOR_STYLES.LABEL_BORDER_SIZE) { - // copy none static/dynamic styles to cluster style - clusterStyleDescriptor.properties[styleName] = { + if ( + styleName === VECTOR_STYLES.SYMBOLIZE_AS || + styleName === VECTOR_STYLES.LABEL_BORDER_SIZE + ) { + // copy none static/dynamic styles to cluster style + clusterStyleDescriptor.properties[styleName] = { + // @ts-expect-error + options: { ...styleProperty.getOptions() }, + }; + } else if (styleProperty.isDynamic()) { + // copy dynamic styles to cluster style + const options = styleProperty.getOptions() as DynamicStylePropertyOptions; + const field = + options && options.field && options.field.name + ? { + ...options.field, + name: clusterSource.getAggKey( + getAggType(styleProperty as IDynamicStyleProperty), + options.field.name + ), + } + : undefined; // @ts-expect-error - options: { ...styleProperty.getOptions() }, - }; - } else if (styleProperty.isDynamic()) { - // copy dynamic styles to cluster style - const options = styleProperty.getOptions() as DynamicStylePropertyOptions; - const field = - options && options.field && options.field.name - ? { - ...options.field, - name: clusterSource.getAggKey( - getAggType(styleProperty as IDynamicStyleProperty), - options.field.name - ), - } - : undefined; - // @ts-expect-error - clusterStyleDescriptor.properties[styleName] = { - type: STYLE_TYPE.DYNAMIC, - options: { - ...options, - field, - }, - }; - } else { - // copy static styles to cluster style - // @ts-expect-error - clusterStyleDescriptor.properties[styleName] = { - type: STYLE_TYPE.STATIC, - options: { ...styleProperty.getOptions() }, - }; - } - }); + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.DYNAMIC, + options: { + ...options, + field, + }, + }; + } else { + // copy static styles to cluster style + // @ts-expect-error + clusterStyleDescriptor.properties[styleName] = { + type: STYLE_TYPE.STATIC, + options: { ...styleProperty.getOptions() }, + }; + } + }); return clusterStyleDescriptor; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts index a93f9121d1e62..91ed0ab52b08e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts @@ -14,6 +14,7 @@ export interface IESAggSource extends IESSource { getAggKey(aggType: AGG_TYPE, fieldName: string): string; getAggLabel(aggType: AGG_TYPE, fieldName: string): string; getMetricFields(): IESAggField[]; + hasMatchingMetricField(fieldName: string): boolean; } export class AbstractESAggSource extends AbstractESSource implements IESAggSource { @@ -22,4 +23,5 @@ export class AbstractESAggSource extends AbstractESSource implements IESAggSourc getAggKey(aggType: AGG_TYPE, fieldName: string): string; getAggLabel(aggType: AGG_TYPE, fieldName: string): string; getMetricFields(): IESAggField[]; + hasMatchingMetricField(fieldName: string): boolean; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts index d95ec5a64e6c3..1f2985ffcc27c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts @@ -7,7 +7,10 @@ import { AbstractVectorSource } from '../vector_source'; import { IVectorSource } from '../vector_source'; import { IndexPattern, ISearchSource } from '../../../../../../../src/plugins/data/public'; -import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { + DynamicStylePropertyOptions, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; import { VectorStyle } from '../../styles/vector/vector_style'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; @@ -25,7 +28,7 @@ export interface IESSource extends IVectorSource { loadStylePropsMeta( layerName: string, style: VectorStyle, - dynamicStyleProps: IDynamicStyleProperty[], + dynamicStyleProps: Array>, registerCancelCallback: (requestToken: symbol, callback: () => void) => void, searchFilters: VectorSourceRequestMeta ): Promise; @@ -45,7 +48,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource loadStylePropsMeta( layerName: string, style: VectorStyle, - dynamicStyleProps: IDynamicStyleProperty[], + dynamicStyleProps: Array>, registerCancelCallback: (requestToken: symbol, callback: () => void) => void, searchFilters: VectorSourceRequestMeta ): Promise; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx index fac002d0a877d..e49c15c68b8db 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx @@ -9,18 +9,17 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FieldMetaPopover } from './field_meta_popover'; -import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; import { FieldMetaOptions } from '../../../../../../common/descriptor_types'; type Props = { - styleProperty: IDynamicStyleProperty; + fieldMetaOptions: FieldMetaOptions; onChange: (fieldMetaOptions: FieldMetaOptions) => void; }; export function CategoricalFieldMetaPopover(props: Props) { const onIsEnabledChange = (event: EuiSwitchEvent) => { props.onChange({ - ...props.styleProperty.getFieldMetaOptions(), + ...props.fieldMetaOptions, isEnabled: event.target.checked, }); }; @@ -32,7 +31,7 @@ export function CategoricalFieldMetaPopover(props: Props) { label={i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel', { defaultMessage: 'Get categories from indices', })} - checked={props.styleProperty.getFieldMetaOptions().isEnabled} + checked={props.fieldMetaOptions.isEnabled} onChange={onIsEnabledChange} compressed /> diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx index 32a54a45ed4d7..9086c4df31596 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx @@ -11,7 +11,6 @@ import { EuiFormRow, EuiRange, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DEFAULT_SIGMA } from '../../vector_style_defaults'; import { FieldMetaPopover } from './field_meta_popover'; -import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; import { FieldMetaOptions } from '../../../../../../common/descriptor_types'; import { VECTOR_STYLES } from '../../../../../../common/constants'; @@ -38,21 +37,22 @@ function getIsEnableToggleLabel(styleName: string) { } type Props = { - styleProperty: IDynamicStyleProperty; + fieldMetaOptions: FieldMetaOptions; + styleName: VECTOR_STYLES; onChange: (fieldMetaOptions: FieldMetaOptions) => void; }; export function OrdinalFieldMetaPopover(props: Props) { const onIsEnabledChange = (event: EuiSwitchEvent) => { props.onChange({ - ...props.styleProperty.getFieldMetaOptions(), + ...props.fieldMetaOptions, isEnabled: event.target.checked, }); }; const onSigmaChange = (event: ChangeEvent | MouseEvent) => { props.onChange({ - ...props.styleProperty.getFieldMetaOptions(), + ...props.fieldMetaOptions, sigma: parseInt(event.currentTarget.value, 10), }); }; @@ -62,8 +62,8 @@ export function OrdinalFieldMetaPopover(props: Props) { @@ -79,9 +79,9 @@ export function OrdinalFieldMetaPopover(props: Props) { min={1} max={5} step={0.25} - value={_.get(props.styleProperty.getFieldMetaOptions(), 'sigma', DEFAULT_SIGMA)} + value={_.get(props.fieldMetaOptions, 'sigma', DEFAULT_SIGMA)} onChange={onSigmaChange} - disabled={!props.styleProperty.getFieldMetaOptions().isEnabled} + disabled={!props.fieldMetaOptions.isEnabled} showTicks tickInterval={1} compressed diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx index 4e68baf0bd7b7..4d9579e9e4c00 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx @@ -29,6 +29,7 @@ import { shallow } from 'enzyme'; import { FIELD_ORIGIN } from '../../../../../../common/constants'; import { AbstractField } from '../../../../fields/field'; import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; +import { IconDynamicOptions } from '../../../../../../common/descriptor_types'; import { IconMapSelect } from './icon_map_select'; class MockField extends AbstractField {} @@ -46,7 +47,9 @@ class MockDynamicStyleProperty { const defaultProps = { iconPaletteId: 'filledShapes', onChange: () => {}, - styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty, + styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty< + IconDynamicOptions + >, isCustomOnly: false, }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx index 1dd55bbb47f78..1a908f4c014e4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { IconStops } from './icon_stops'; // @ts-expect-error import { getIconPaletteOptions, PREFERRED_ICONS } from '../../symbol_utils'; -import { IconStop } from '../../../../../../common/descriptor_types'; +import { IconDynamicOptions, IconStop } from '../../../../../../common/descriptor_types'; import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; const CUSTOM_MAP_ID = 'CUSTOM_MAP_ID'; @@ -32,7 +32,7 @@ interface Props { customIconStops?: IconStop[]; iconPaletteId: string | null; onChange: ({ customIconStops, iconPaletteId, useCustomIconMap }: StyleOptionChanges) => void; - styleProperty: IDynamicStyleProperty; + styleProperty: IDynamicStyleProperty; useCustomIconMap?: boolean; isCustomOnly: boolean; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js index 7992ee5b3aeaf..2183a298a2841 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js @@ -187,7 +187,7 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => { colorCategory: 'palette_0', }); - const meta = colorStyle.pluckCategoricalStyleMetaFromFieldMetaData({ + const meta = colorStyle._pluckCategoricalStyleMetaFromFieldMetaData({ foobar: { buckets: [ { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts deleted file mode 100644 index e153b6e4850f7..0000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { IStyleProperty } from './style_property'; -import { FIELD_ORIGIN } from '../../../../../common/constants'; -import { - CategoryFieldMeta, - DynamicStylePropertyOptions, - FieldMetaOptions, - RangeFieldMeta, -} from '../../../../../common/descriptor_types'; -import { IField } from '../../../fields/field'; - -export interface IDynamicStyleProperty extends IStyleProperty { - getOptions(): DynamicStylePropertyOptions; - getFieldMetaOptions(): FieldMetaOptions; - getField(): IField | undefined; - getFieldName(): string; - getFieldOrigin(): FIELD_ORIGIN | undefined; - getRangeFieldMeta(): RangeFieldMeta; - getCategoryFieldMeta(): CategoryFieldMeta; - getNumberOfCategories(): number; - isFieldMetaEnabled(): boolean; - isOrdinal(): boolean; - supportsFieldMeta(): boolean; - getFieldMetaRequest(): Promise; - supportsMbFeatureState(): boolean; - pluckOrdinalStyleMetaFromFeatures(features: unknown[]): RangeFieldMeta; - pluckCategoricalStyleMetaFromFeatures(features: unknown[]): CategoryFieldMeta; - pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData: unknown): RangeFieldMeta; - pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData: unknown): CategoryFieldMeta; - getValueSuggestions(query: string): string[]; -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx similarity index 54% rename from x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 15d0b3c4bf913..216fde595af32 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -3,42 +3,87 @@ * 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 _ from 'lodash'; -import { AbstractStyleProperty } from './style_property'; +import React from 'react'; +import { Feature } from 'geojson'; +import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { STYLE_TYPE, SOURCE_META_DATA_REQUEST_ID, FIELD_ORIGIN, + VECTOR_STYLES, } from '../../../../../common/constants'; -import React from 'react'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; +import { + CategoryFieldMeta, + FieldMetaOptions, + StyleMetaData, + RangeFieldMeta, +} from '../../../../../common/descriptor_types'; +import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IJoin } from '../../../joins/join'; + +export interface IDynamicStyleProperty extends IStyleProperty { + getFieldMetaOptions(): FieldMetaOptions; + getField(): IField | null; + getFieldName(): string; + getFieldOrigin(): FIELD_ORIGIN | null; + getRangeFieldMeta(): RangeFieldMeta | null; + getCategoryFieldMeta(): CategoryFieldMeta | null; + getNumberOfCategories(): number; + isFieldMetaEnabled(): boolean; + isOrdinal(): boolean; + supportsFieldMeta(): boolean; + getFieldMetaRequest(): Promise; + supportsMbFeatureState(): boolean; + pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; + pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; + getValueSuggestions(query: string): Promise; +} + +type fieldFormatter = (value: string | undefined) => string; -export class DynamicStyleProperty extends AbstractStyleProperty { +export class DynamicStyleProperty extends AbstractStyleProperty + implements IDynamicStyleProperty { static type = STYLE_TYPE.DYNAMIC; - constructor(options, styleName, field, vectorLayer, getFieldFormatter) { + protected readonly _field: IField | null; + protected readonly _layer: IVectorLayer; + protected readonly _getFieldFormatter: (fieldName: string) => null | fieldFormatter; + + constructor( + options: T, + styleName: VECTOR_STYLES, + field: IField | null, + vectorLayer: IVectorLayer, + getFieldFormatter: (fieldName: string) => null | fieldFormatter + ) { super(options, styleName); this._field = field; this._layer = vectorLayer; this._getFieldFormatter = getFieldFormatter; } - getValueSuggestions = (query) => { - const field = this.getField(); - const fieldSource = this._getFieldSource(); - return fieldSource && field ? fieldSource.getValueSuggestions(field, query) : []; + // ignore TS error about "Type '(query: string) => Promise | never[]' is not assignable to type '(query: string) => Promise'." + // @ts-expect-error + getValueSuggestions = (query: string) => { + return this._field === null + ? [] + : this._field.getSource().getValueSuggestions(this._field, query); }; - _getStyleMetaDataRequestId(fieldName) { + _getStyleMetaDataRequestId(fieldName: string) { if (this.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { return SOURCE_META_DATA_REQUEST_ID; } - const join = this._layer.getValidJoins().find((join) => { - return join.getRightJoinSource().hasMatchingMetricField(fieldName); + const join = this._layer.getValidJoins().find((validJoin: IJoin) => { + return validJoin.getRightJoinSource().hasMatchingMetricField(fieldName); }); return join ? join.getSourceMetaDataRequestId() : null; } @@ -63,8 +108,8 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return rangeFieldMetaFromLocalFeatures; } - const data = styleMetaDataRequest.getData(); - const rangeFieldMeta = this.pluckOrdinalStyleMetaFromFieldMetaData(data); + const data = styleMetaDataRequest.getData() as StyleMetaData; + const rangeFieldMeta = this._pluckOrdinalStyleMetaFromFieldMetaData(data); return rangeFieldMeta ? rangeFieldMeta : rangeFieldMetaFromLocalFeatures; } @@ -88,8 +133,8 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return categoryFieldMetaFromLocalFeatures; } - const data = styleMetaDataRequest.getData(); - const rangeFieldMeta = this.pluckCategoricalStyleMetaFromFieldMetaData(data); + const data = styleMetaDataRequest.getData() as StyleMetaData; + const rangeFieldMeta = this._pluckCategoricalStyleMetaFromFieldMetaData(data); return rangeFieldMeta ? rangeFieldMeta : categoryFieldMetaFromLocalFeatures; } @@ -97,10 +142,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return this._field; } - _getFieldSource() { - return this._field ? this._field.getSource() : null; - } - getFieldName() { return this._field ? this._field.getName() : ''; } @@ -126,7 +167,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getFieldOrigin() { - return this._field.getOrigin(); + return this._field ? this._field.getOrigin() : null; } isFieldMetaEnabled() { @@ -135,10 +176,14 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } supportsFieldMeta() { - return this.isComplete() && this._field.supportsFieldMeta(); + return this.isComplete() && !!this._field && this._field.supportsFieldMeta(); } async getFieldMetaRequest() { + if (!this._field) { + return null; + } + if (this.isOrdinal()) { return this._field.getOrdinalFieldMetaRequest(); } else if (this.isCategorical()) { @@ -154,20 +199,20 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getFieldMetaOptions() { - return _.get(this.getOptions(), 'fieldMetaOptions', {}); + return _.get(this.getOptions(), 'fieldMetaOptions', { isEnabled: true }); } - pluckOrdinalStyleMetaFromFeatures(features) { + pluckOrdinalStyleMetaFromFeatures(features: Feature[]) { if (!this.isOrdinal()) { return null; } - const name = this.getField().getName(); + const name = this.getFieldName(); let min = Infinity; let max = -Infinity; for (let i = 0; i < features.length; i++) { const feature = features[i]; - const newValue = parseFloat(feature.properties[name]); + const newValue = parseFloat(feature.properties ? feature.properties[name] : null); if (!isNaN(newValue)) { min = Math.min(min, newValue); max = Math.max(max, newValue); @@ -176,25 +221,24 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return min === Infinity || max === -Infinity ? null - : { - min: min, - max: max, + : ({ + min, + max, delta: max - min, - }; + } as RangeFieldMeta); } - pluckCategoricalStyleMetaFromFeatures(features) { + pluckCategoricalStyleMetaFromFeatures(features: Feature[]) { const size = this.getNumberOfCategories(); if (!this.isCategorical() || size <= 0) { return null; } - const fieldName = this.getField().getName(); const counts = new Map(); for (let i = 0; i < features.length; i++) { const feature = features[i]; - const term = feature.properties[fieldName]; - //properties object may be sparse, so need to check if the field is effectively present + const term = feature.properties ? feature.properties[this.getFieldName()] : undefined; + // properties object may be sparse, so need to check if the field is effectively present if (typeof term !== undefined) { if (counts.has(term)) { counts.set(term, counts.get(term) + 1); @@ -215,16 +259,16 @@ export class DynamicStyleProperty extends AbstractStyleProperty { const truncated = ordered.slice(0, size); return { categories: truncated, - }; + } as CategoryFieldMeta; } - pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) { - if (!this.isOrdinal()) { + _pluckOrdinalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData) { + if (!this.isOrdinal() || !this._field) { return null; } - const stats = fieldMetaData[this._field.getRootName()]; - if (!stats) { + const stats = styleMetaData[this._field.getRootName()]; + if (!stats || !('avg' in stats)) { return null; } @@ -242,55 +286,56 @@ export class DynamicStyleProperty extends AbstractStyleProperty { }; } - pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) { - if (!this.isCategorical()) { + _pluckCategoricalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData) { + if (!this.isCategorical() || !this._field) { return null; } - const rootFieldName = this._field.getRootName(); - if (!fieldMetaData[rootFieldName] || !fieldMetaData[rootFieldName].buckets) { + const fieldMeta = styleMetaData[this._field.getRootName()]; + if (!fieldMeta || !('buckets' in fieldMeta)) { return null; } - const ordered = fieldMetaData[rootFieldName].buckets.map((bucket) => { - return { - key: bucket.key, - count: bucket.doc_count, - }; - }); return { - categories: ordered, + categories: fieldMeta.buckets.map((bucket) => { + return { + key: bucket.key, + count: bucket.doc_count, + }; + }), }; } - formatField(value) { + formatField(value: string | undefined): string { if (this.getField()) { - const fieldName = this.getField().getName(); + const fieldName = this.getFieldName(); const fieldFormatter = this._getFieldFormatter(fieldName); - return fieldFormatter ? fieldFormatter(value) : value; + return fieldFormatter ? fieldFormatter(value) : super.formatField(value); } else { - return value; + return super.formatField(value); } } - getNumericalMbFeatureStateValue(value) { - const valueAsFloat = parseFloat(value); - return isNaN(valueAsFloat) ? null : valueAsFloat; - } - renderLegendDetailRow() { return null; } - renderFieldMetaPopover(onFieldMetaOptionsChange) { + renderFieldMetaPopover(onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void) { if (!this.supportsFieldMeta()) { return null; } return this.isCategorical() ? ( - + ) : ( - + ); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts index b704e4bd56970..7a0ed4fb3e962 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts @@ -8,7 +8,7 @@ import { ReactElement } from 'react'; // @ts-ignore import { getVectorStyleLabel } from '../components/get_vector_style_label'; -import { FieldMetaOptions, StylePropertyOptions } from '../../../../../common/descriptor_types'; +import { FieldMetaOptions } from '../../../../../common/descriptor_types'; import { VECTOR_STYLES } from '../../../../../common/constants'; type LegendProps = { @@ -17,12 +17,12 @@ type LegendProps = { symbolId?: string; }; -export interface IStyleProperty { +export interface IStyleProperty { isDynamic(): boolean; isComplete(): boolean; formatField(value: string | undefined): string; getStyleName(): VECTOR_STYLES; - getOptions(): StylePropertyOptions; + getOptions(): T; renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; renderFieldMetaPopover( onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void @@ -30,11 +30,11 @@ export interface IStyleProperty { getDisplayStyleName(): string; } -export class AbstractStyleProperty implements IStyleProperty { - private readonly _options: StylePropertyOptions; - private readonly _styleName: VECTOR_STYLES; +export class AbstractStyleProperty implements IStyleProperty { + protected readonly _options: T; + protected readonly _styleName: VECTOR_STYLES; - constructor(options: StylePropertyOptions, styleName: VECTOR_STYLES) { + constructor(options: T, styleName: VECTOR_STYLES) { this._options = options; this._styleName = styleName; } @@ -62,15 +62,17 @@ export class AbstractStyleProperty implements IStyleProperty { return this._styleName; } - getOptions(): StylePropertyOptions { - return this._options || {}; + getOptions(): T { + return this._options; } renderLegendDetailRow() { return null; } - renderFieldMetaPopover() { + renderFieldMetaPopover( + onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void + ): ReactElement | null { return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts index ea0736c4837d8..d48d075288a28 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts @@ -9,14 +9,16 @@ import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; import { IVectorSource } from '../../sources/vector_source'; import { AbstractStyle, IStyle } from '../style'; import { + DynamicStylePropertyOptions, + StylePropertyOptions, VectorStyleDescriptor, VectorStylePropertiesDescriptor, } from '../../../../common/descriptor_types'; import { StyleMeta } from './style_meta'; export interface IVectorStyle extends IStyle { - getAllStyleProperties(): IStyleProperty[]; - getDynamicPropertiesArray(): IDynamicStyleProperty[]; + getAllStyleProperties(): Array>; + getDynamicPropertiesArray(): Array>; getSourceFieldNames(): string[]; getStyleMeta(): StyleMeta; } @@ -26,7 +28,7 @@ export class VectorStyle extends AbstractStyle implements IVectorStyle { static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); getSourceFieldNames(): string[]; - getAllStyleProperties(): IStyleProperty[]; - getDynamicPropertiesArray(): IDynamicStyleProperty[]; + getAllStyleProperties(): Array>; + getDynamicPropertiesArray(): Array>; getStyleMeta(): StyleMeta; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index 3cff48e4d682e..907e16a6a8427 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -48,6 +48,11 @@ const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; +function getNumericalMbFeatureStateValue(value) { + const valueAsFloat = parseFloat(value); + return isNaN(valueAsFloat) ? null : valueAsFloat; +} + export class VectorStyle extends AbstractStyle { static type = LAYER_STYLE_TYPE.VECTOR; @@ -518,14 +523,14 @@ export class VectorStyle extends AbstractStyle { const computedName = getComputedFieldName(dynamicStyleProp.getStyleName(), name); const rawValue = feature.properties[name]; if (dynamicStyleProp.supportsMbFeatureState()) { - tmpFeatureState[name] = dynamicStyleProp.getNumericalMbFeatureStateValue(rawValue); //the same value will be potentially overridden multiple times, if the name remains identical + tmpFeatureState[name] = getNumericalMbFeatureStateValue(rawValue); //the same value will be potentially overridden multiple times, if the name remains identical } else { //in practice, a new system property will only be created for: // - label text: this requires the value to be formatted first. // - icon orientation: this is a lay-out property which do not support feature-state (but we're still coercing to a number) const formattedValue = dynamicStyleProp.isOrdinal() - ? dynamicStyleProp.getNumericalMbFeatureStateValue(rawValue) + ? getNumericalMbFeatureStateValue(rawValue) : dynamicStyleProp.formatField(rawValue); feature.properties[computedName] = formattedValue; From 7868a569eba1195e689ca1cf490c7e97238c0349 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 16 Jul 2020 14:42:34 -0500 Subject: [PATCH 18/76] [ML] Fix datafeed start time is incorrect when the job has trailing empty buckets (#71976) Co-authored-by: Elastic Machine --- .../anomaly_detection_jobs/summary_job.ts | 1 + .../plugins/ml/common/util/job_utils.test.ts | 20 +++++++++++++++++++ x-pack/plugins/ml/common/util/job_utils.ts | 19 +++++++++++++++++- .../start_datafeed_modal.js | 2 +- .../ml/server/models/job_service/jobs.ts | 12 ++++++++--- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 6cf109dc553ae..2102673060273 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -30,6 +30,7 @@ export interface MlSummaryJob { isSingleMetricViewerJob: boolean; deleting?: boolean; latestTimestampSortValue?: number; + earliestStartTimestampMs?: number; } export interface AuditMessage { diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index 233e2c2cd19ac..a56ccd5208bab 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -18,8 +18,10 @@ import { prefixDatafeedId, getSafeAggregationName, getLatestDataOrBucketTimestamp, + getEarliestDatafeedStartTime, } from './job_utils'; import { CombinedJob, Job } from '../types/anomaly_detection_jobs'; +import moment from 'moment'; describe('ML - job utils', () => { describe('calculateDatafeedFrequencyDefaultSeconds', () => { @@ -581,4 +583,22 @@ describe('ML - job utils', () => { expect(getLatestDataOrBucketTimestamp(undefined, undefined)).toBe(undefined); }); }); + + describe('getEarliestDatafeedStartTime', () => { + test('returns expected value when no gap in data at end of bucket processing', () => { + expect(getEarliestDatafeedStartTime(1549929594000, 1549928700000)).toBe(1549929594000); + }); + test('returns expected value when there is a gap in data at end of bucket processing', () => { + expect(getEarliestDatafeedStartTime(1549929594000, 1562256600000)).toBe(1562256600000); + }); + test('returns expected value when bucket span is provided', () => { + expect( + getEarliestDatafeedStartTime(1549929594000, 1562256600000, moment.duration(1, 'h')) + ).toBe(1562260200000); + }); + + test('returns expected value when job has not run', () => { + expect(getLatestDataOrBucketTimestamp(undefined, undefined)).toBe(undefined); + }); + }); }); diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 7ea4ceccf578d..bb0e351ebfec8 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import semver from 'semver'; -import { Duration } from 'moment'; +import moment, { Duration } from 'moment'; // @ts-ignore import numeral from '@elastic/numeral'; @@ -621,6 +621,23 @@ function isValidTimeInterval(value: string | undefined): boolean { return parseTimeIntervalForJob(value) !== null; } +// The earliest start time for the datafeed should be the max(latest_record_timestamp, latest_bucket.timestamp + bucket_span). +export function getEarliestDatafeedStartTime( + latestRecordTimestamp: number | undefined, + latestBucketTimestamp: number | undefined, + bucketSpan?: Duration | null | undefined +): number | undefined { + if (latestRecordTimestamp !== undefined && latestBucketTimestamp !== undefined) { + // if bucket span is available (e.g. 15m) add it to the latest bucket timestamp in ms + const adjustedBucketStartTime = bucketSpan + ? moment(latestBucketTimestamp).add(bucketSpan).valueOf() + : latestBucketTimestamp; + return Math.max(latestRecordTimestamp, adjustedBucketStartTime); + } else { + return latestRecordTimestamp !== undefined ? latestRecordTimestamp : latestBucketTimestamp; + } +} + // Returns the latest of the last source data and last processed bucket timestamp, // as used for example in setting the end time of results views for cases where // anomalies might have been raised after the point at which data ingest has stopped. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index 9ce15fb881bd8..d0d3dc56ababf 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -222,6 +222,6 @@ StartDatafeedModal.propTypes = { }; function getLowestLatestTime(jobs) { - const times = jobs.map((j) => j.latestTimestampSortValue); + const times = jobs.map((j) => j.earliestStartTimestampMs || 0); return moment(Math.min(...times)); } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index aca0c5d72a9f5..e9ed2d0941d96 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import Boom from 'boom'; import { ILegacyScopedClusterClient } from 'kibana/server'; +import { parseTimeIntervalForJob } from '../../../common/util/job_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { MlSummaryJob, @@ -24,11 +25,11 @@ import { resultsServiceProvider } from '../results_service'; import { CalendarManager, Calendar } from '../calendar'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { + getEarliestDatafeedStartTime, getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, } from '../../../common/util/job_utils'; import { groupsProvider } from './groups'; - export interface MlJobsResponse { jobs: Job[]; count: number; @@ -171,6 +172,11 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { description: job.description || '', groups: Array.isArray(job.groups) ? job.groups.sort() : [], processed_record_count: job.data_counts?.processed_record_count, + earliestStartTimestampMs: getEarliestDatafeedStartTime( + dataCounts?.latest_record_timestamp, + dataCounts?.latest_bucket_timestamp, + parseTimeIntervalForJob(job.analysis_config?.bucket_span) + ), memory_status: job.model_size_stats ? job.model_size_stats.memory_status : '', jobState: job.deleting === true ? deletingStr : job.state, hasDatafeed, @@ -182,8 +188,8 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { latestTimestampMs: dataCounts?.latest_record_timestamp, earliestTimestampMs: dataCounts?.earliest_record_timestamp, latestResultsTimestampMs: getLatestDataOrBucketTimestamp( - dataCounts?.latest_record_timestamp as number, - dataCounts?.latest_bucket_timestamp as number + dataCounts?.latest_record_timestamp, + dataCounts?.latest_bucket_timestamp ), isSingleMetricViewerJob: isTimeSeriesViewJob(job), nodeName: job.node ? job.node.name : undefined, From 28189c274b9d758993ac57058fba578df5dc3fc5 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 16 Jul 2020 15:48:58 -0400 Subject: [PATCH 19/76] [Monitoring] Out of the box alert tweaks (#71942) * Tweaks to thresholds and throttle periods * Fixes * Type fix, and more defensive against no alerts * Remove unnecessary restrictions --- .../plugins/monitoring/public/alerts/callout.tsx | 2 +- .../flyout_expressions/alert_param_duration.tsx | 2 +- .../public/alerts/lib/should_show_alert_badge.ts | 3 +++ .../monitoring/server/alerts/base_alert.test.ts | 2 +- .../monitoring/server/alerts/base_alert.ts | 2 +- .../server/alerts/cluster_health_alert.test.ts | 2 +- .../server/alerts/cpu_usage_alert.test.ts | 4 ++-- .../monitoring/server/alerts/cpu_usage_alert.ts | 13 +++++++++++-- .../elasticsearch_version_mismatch_alert.test.ts | 2 +- .../alerts/kibana_version_mismatch_alert.test.ts | 2 +- .../alerts/license_expiration_alert.test.ts | 2 +- .../logstash_version_mismatch_alert.test.ts | 2 +- .../server/alerts/nodes_changed_alert.test.ts | 2 +- .../server/alerts/nodes_changed_alert.ts | 15 +++++++++++++++ .../server/routes/api/v1/alerts/enable.ts | 1 - .../server/routes/api/v1/alerts/status.ts | 1 - 16 files changed, 41 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index 748ec257ea765..d000f470da334 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -24,7 +24,7 @@ const TYPES = [ severity: AlertSeverity.Danger, color: 'danger', label: i18n.translate('xpack.monitoring.alerts.callout.dangerLabel', { - defaultMessage: 'DAnger alert(s)', + defaultMessage: 'Danger alert(s)', }), }, ]; diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx index 23a9ea1facbc9..862f32efd7361 100644 --- a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx @@ -49,7 +49,7 @@ interface Props { setAlertParams: (property: string, value: any) => void; } -const parseRegex = /(\d+)(\smhd)/; +const parseRegex = /(\d+)([smhd]{1})/; export const AlertParamDuration: React.FC = (props: Props) => { const { name, label, setAlertParams, errors } = props; const parsed = parseRegex.exec(props.duration); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts index c6773e9ca0156..6de371b53ef9b 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts +++ b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts @@ -10,6 +10,9 @@ export function shouldShowAlertBadge( alerts: { [alertTypeId: string]: CommonAlertStatus }, alertTypeIds: string[] ) { + if (!alerts) { + return false; + } const inSetupMode = isInSetupMode(); return inSetupMode || alertTypeIds.find((name) => alerts[name] && alerts[name].states.length); } diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts index 8fd31db421a30..c256cce362ff8 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts @@ -63,7 +63,7 @@ describe('BaseAlert', () => { interval: '1m', }, tags: [], - throttle: '1m', + throttle: '1d', }, }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 622ee7dc51af1..cac57f599633d 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -40,7 +40,7 @@ import { MonitoringLicenseService } from '../types'; export class BaseAlert { public type!: string; public label!: string; - public defaultThrottle: string = '1m'; + public defaultThrottle: string = '1d'; public defaultInterval: string = '1m'; public rawAlert: Alert | undefined; public isLegacy: boolean = false; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 10b75c43ac879..f25179fa63c2f 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -22,7 +22,7 @@ describe('ClusterHealthAlert', () => { const alert = new ClusterHealthAlert(); expect(alert.type).toBe(ALERT_CLUSTER_HEALTH); expect(alert.label).toBe('Cluster health'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index f0d11abab1492..1a66560ae124a 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -22,9 +22,9 @@ describe('CpuUsageAlert', () => { const alert = new CpuUsageAlert(); expect(alert.type).toBe(ALERT_CPU_USAGE); expect(alert.label).toBe('CPU Usage'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore - expect(alert.defaultParams).toStrictEqual({ threshold: 90, duration: '5m' }); + expect(alert.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 9171745fba747..b543a4c976377 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -39,7 +39,7 @@ const FIRING = i18n.translate('xpack.monitoring.alerts.cpuUsage.firing', { defaultMessage: 'firing', }); -const DEFAULT_THRESHOLD = 90; +const DEFAULT_THRESHOLD = 85; const DEFAULT_DURATION = '5m'; interface CpuUsageParams { @@ -393,7 +393,16 @@ export class CpuUsageAlert extends BaseAlert { continue; } - const instance = services.alertInstanceFactory(`${this.type}:${cluster.clusterUuid}`); + const firingNodeUuids = nodes.reduce((list: string[], node) => { + const stat = node.meta as AlertCpuUsageNodeStats; + if (node.shouldFire) { + list.push(stat.nodeId); + } + return list; + }, [] as string[]); + firingNodeUuids.sort(); // It doesn't matter how we sort, but keep the order consistent + const instanceId = `${this.type}:${cluster.clusterUuid}:${firingNodeUuids.join(',')}`; + const instance = services.alertInstanceFactory(instanceId); const state = (instance.getState() as unknown) as AlertInstanceState; const alertInstanceState: AlertInstanceState = { alertStates: state?.alertStates || [] }; let shouldExecuteActions = false; diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 44684939ca261..50bf40825c515 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -22,7 +22,7 @@ describe('ElasticsearchVersionMismatchAlert', () => { const alert = new ElasticsearchVersionMismatchAlert(); expect(alert.type).toBe(ALERT_ELASTICSEARCH_VERSION_MISMATCH); expect(alert.label).toBe('Elasticsearch version mismatch'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 6c56c7aa08d71..1a76fae9fc420 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -22,7 +22,7 @@ describe('KibanaVersionMismatchAlert', () => { const alert = new KibanaVersionMismatchAlert(); expect(alert.type).toBe(ALERT_KIBANA_VERSION_MISMATCH); expect(alert.label).toBe('Kibana version mismatch'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 09173df1d88b1..0f677dcc9c120 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -29,7 +29,7 @@ describe('LicenseExpirationAlert', () => { const alert = new LicenseExpirationAlert(); expect(alert.type).toBe(ALERT_LICENSE_EXPIRATION); expect(alert.label).toBe('License expiration'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 3f6d38809a949..f29c199b3f1e1 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -22,7 +22,7 @@ describe('LogstashVersionMismatchAlert', () => { const alert = new LogstashVersionMismatchAlert(); expect(alert.type).toBe(ALERT_LOGSTASH_VERSION_MISMATCH); expect(alert.label).toBe('Logstash version mismatch'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 13c3dbbbe6e8a..d45d404b38304 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -29,7 +29,7 @@ describe('NodesChangedAlert', () => { const alert = new NodesChangedAlert(); expect(alert.type).toBe(ALERT_NODES_CHANGED); expect(alert.label).toBe('Nodes changed'); - expect(alert.defaultThrottle).toBe('1m'); + expect(alert.defaultThrottle).toBe('1d'); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 5b38503c7ece4..73f3ee055c928 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -155,6 +155,21 @@ export class NodesChangedAlert extends BaseAlert { }; } + if ( + Object.values(states.added).length === 0 && + Object.values(states.removed).length === 0 && + Object.values(states.restarted).length === 0 + ) { + return { + text: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage', + { + defaultMessage: `Elasticsearch nodes have changed`, + } + ), + }; + } + const addedText = Object.values(states.added).length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 1d83644fce756..b7cc088d2716c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -18,7 +18,6 @@ export function enableAlertsRoute(server: any, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alerts/enable', - options: { tags: ['access:monitoring'] }, validate: false, }, async (context, request, response) => { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index eef99bbc4ac68..78daa5e47c49f 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -15,7 +15,6 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alert/{clusterUuid}/status', - options: { tags: ['access:monitoring'] }, validate: { params: schema.object({ clusterUuid: schema.string(), From 4278e560ef75e0b33204f2ca384a27138a3bad3b Mon Sep 17 00:00:00 2001 From: Fabien Baligand Date: Thu, 16 Jul 2020 22:13:35 +0200 Subject: [PATCH 20/76] [Visualizations] Pass 'aggs' parameter to custom request handlers (#71423) --- .../public/expressions/visualization_function.ts | 14 +++++++++++++- .../visualizations/public/legacy/build_pipeline.ts | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/plugins/visualizations/public/expressions/visualization_function.ts b/src/plugins/visualizations/public/expressions/visualization_function.ts index 222479158934b..68a153f4272a3 100644 --- a/src/plugins/visualizations/public/expressions/visualization_function.ts +++ b/src/plugins/visualizations/public/expressions/visualization_function.ts @@ -21,7 +21,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { VisResponseValue, PersistedState } from '../../../../plugins/visualizations/public'; import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; -import { getTypes, getIndexPatterns, getFilterManager } from '../services'; +import { getTypes, getIndexPatterns, getFilterManager, getSearch } from '../services'; interface Arguments { index?: string | null; @@ -31,6 +31,7 @@ interface Arguments { schemas?: string; visConfig?: string; uiState?: string; + aggConfigs?: string; } export type ExpressionFunctionVisualization = ExpressionFunctionDefinition< @@ -84,6 +85,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ default: '"{}"', help: 'User interface state', }, + aggConfigs: { + types: ['string'], + default: '"{}"', + help: 'Aggregation configurations', + }, }, async fn(input, args, { inspectorAdapters }) { const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {}; @@ -94,6 +100,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ const uiStateParams = args.uiState ? JSON.parse(args.uiState) : {}; const uiState = new PersistedState(uiStateParams); + const aggConfigsState = args.aggConfigs ? JSON.parse(args.aggConfigs) : []; + const aggs = indexPattern + ? getSearch().aggs.createAggConfigs(indexPattern, aggConfigsState) + : undefined; + if (typeof visType.requestHandler === 'function') { input = await visType.requestHandler({ partialRows: args.partialRows, @@ -107,6 +118,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({ inspectorAdapters, queryFilter: getFilterManager(), forceFetch: true, + aggs, }); } diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 2ef07bf18c91c..e74a83d91fabf 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -535,7 +535,10 @@ export const buildPipeline = async ( metricsAtAllLevels=${vis.isHierarchical()} partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `; if (indexPattern) { - pipeline += `${prepareString('index', indexPattern.id)}`; + pipeline += `${prepareString('index', indexPattern.id)} `; + if (vis.data.aggs) { + pipeline += `${prepareJson('aggConfigs', vis.data.aggs!.aggs)}`; + } } } From d510263666aa8636c79ff9bc4083c69df22f4174 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 16 Jul 2020 14:25:25 -0600 Subject: [PATCH 21/76] [Maps] fix 'New Map' from getting added to recently accessed (#72125) --- .../public/routing/routes/maps_app/load_map_and_render.js | 6 +++++- .../maps/public/routing/routes/maps_app/maps_app_view.js | 5 ----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js index c87f6eb330531..3eea5b00d324e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js @@ -7,7 +7,7 @@ import React from 'react'; import { MapsAppView } from '.'; import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { getToasts } from '../../../kibana_services'; +import { getCoreChrome, getToasts } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; @@ -30,6 +30,10 @@ export const LoadMapAndRender = class extends React.Component { try { const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); if (this._isMounted) { + getCoreChrome().docTitle.change(savedMap.title); + if (this.props.savedMapId) { + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); + } this.setState({ savedMap }); } } catch (err) { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 29fbb5f46e29b..aa7f24155ab43 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -56,11 +56,6 @@ export class MapsAppView extends React.Component { } componentDidMount() { - const { savedMap } = this.props; - - getCoreChrome().docTitle.change(savedMap.title); - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); - // Init sync utils // eslint-disable-next-line react-hooks/rules-of-hooks this._globalSyncUnsubscribe = useGlobalStateSyncing(); From ba76476110fdcb0dff3d760e82be46a31d48aae0 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 16 Jul 2020 16:54:09 -0400 Subject: [PATCH 22/76] [Ingest Manager] Do not show enrolling and unenrolling agents as online in agent counters (#71921) --- .../common/services/agent_status.ts | 20 +++++++++++-------- .../common/types/rest_spec/agent.ts | 1 + .../sections/fleet/components/donut_chart.tsx | 2 +- .../sections/fleet/components/list_layout.tsx | 1 + .../server/services/agents/status.ts | 6 ++++-- .../policy/store/policy_details/reducer.ts | 1 + .../policy/store/policy_list/index.test.ts | 1 + .../pages/policy/store/policy_list/reducer.ts | 1 + 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index 6489c30308771..536003b0f743d 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AGENT_POLLING_THRESHOLD_MS, - AGENT_TYPE_PERMANENT, - AGENT_SAVED_OBJECT_TYPE, -} from '../constants'; +import { AGENT_POLLING_THRESHOLD_MS, AGENT_SAVED_OBJECT_TYPE } from '../constants'; import { Agent, AgentStatus } from '../types'; export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { @@ -41,8 +37,16 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta return 'online'; } +export function buildKueryForEnrollingAgents() { + return `not ${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*`; +} + +export function buildKueryForUnenrollingAgents() { + return `${AGENT_SAVED_OBJECT_TYPE}.unenrollment_started_at:*`; +} + export function buildKueryForOnlineAgents() { - return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`; + return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()}) AND not (${buildKueryForEnrollingAgents()}) AND not (${buildKueryForUnenrollingAgents()})`; } export function buildKueryForErrorAgents() { @@ -50,7 +54,7 @@ export function buildKueryForErrorAgents() { } export function buildKueryForOfflineAgents() { - return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s) AND not ( ${buildKueryForErrorAgents()} ))`; + }s AND not (${buildKueryForErrorAgents()})`; } 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 ed7d73ab0b719..7ec5a8d68311f 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 @@ -173,5 +173,6 @@ export interface GetAgentStatusResponse { online: number; error: number; offline: number; + other: number; }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx index bfa9c80f12851..99a4f27b428fe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx @@ -31,7 +31,7 @@ export const DonutChart = ({ height, width, data }: DonutChartProps) => { .ordinal() // @ts-ignore .domain(data) - .range(['#017D73', '#98A2B3', '#BD271E']); + .range(['#017D73', '#98A2B3', '#BD271E', '#F5A700']); const pieGenerator = d3.layout .pie() .value(({ value }: any) => value) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 46190033d4d6b..16acda9dc4afd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -66,6 +66,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { online: agentStatus?.online || 0, offline: agentStatus?.offline || 0, error: agentStatus?.error || 0, + other: agentStatus?.other || 0, }} /> 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 016a2344cf532..86336714a511e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -25,9 +25,10 @@ export async function getAgentStatusForConfig( soClient: SavedObjectsClientContract, configId?: string ) { - const [all, error, offline] = await Promise.all( + const [all, online, error, offline] = await Promise.all( [ undefined, + AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map((kuery) => @@ -47,9 +48,10 @@ export async function getAgentStatusForConfig( return { events: await getEventsCount(soClient, configId), total: all.total, - online: all.total - error.total - offline.total, + online: online.total, error: error.total, offline: offline.total, + other: all.total - online.total - error.total - offline.total, }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index b3b74c2ca9dae..e7aa2c8893f8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -21,6 +21,7 @@ export const initialPolicyDetailsState: () => Immutable = () offline: 0, online: 0, total: 0, + other: 0, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 0a24c9eea71eb..8203aae244f24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -152,6 +152,7 @@ describe('policy list store concerns', () => { offline: 0, online: 0, total: 0, + other: 0, }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts index 52bed8d850ef4..53954449ab9c3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts @@ -31,6 +31,7 @@ export const initialPolicyListState: () => Immutable = () => ({ offline: 0, online: 0, total: 0, + other: 0, }, }); From 9da8b768bfb5a24053feeaed1c8fb796ac905cba Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 16 Jul 2020 14:43:27 -0700 Subject: [PATCH 23/76] Fixed Webhook connector doesn't retain added HTTP header settings (#71924) * Fixed Webhook connector doesn't retain added HTTP header settings * fixed method --- .../webhook/webhook_connectors.test.tsx | 1 + .../webhook/webhook_connectors.tsx | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 3b7865e59b9e6..3a2afff03c58f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -36,6 +36,7 @@ describe('WebhookActionConnectorFields renders', () => { /> ); expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 57c88607c0884..2321d5b4b5479 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -31,14 +31,16 @@ const HTTP_VERBS = ['post', 'put']; const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { user, password } = action.secrets; + const { method, url, headers } = action.config; + const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); const [hasHeaders, setHasHeaders] = useState(false); - const { user, password } = action.secrets; - const { method, url, headers } = action.config; - - editActionConfig('method', 'post'); // set method to POST by default + if (!method) { + editActionConfig('method', 'post'); // set method to POST by default + } const headerErrors = { keyHeader: new Array(), @@ -80,7 +82,7 @@ const WebhookActionConnectorFields: React.FunctionComponent
- {hasHeaders && Object.keys(headers || {}).length > 0 ? ( - + {Object.keys(headers || {}).length > 0 ? ( + <>
@@ -351,10 +353,10 @@ const WebhookActionConnectorFields: React.FunctionComponent {headersList} - + ) : null} - {headerControl} + {hasHeaders && headerControl}
From 78b39e8b9f8d27cd5f4ba28755a9489688748f3f Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 16 Jul 2020 14:51:56 -0700 Subject: [PATCH 24/76] using test_user with minimum privs (#71988) Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/discover/async_scripted_fields.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 46848b1db0ef4..33a64e4f9cdd3 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -14,9 +14,9 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); const queryBar = getService('queryBar'); + const security = getService('security'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { await esArchiver.load('kibana_scripted_fields_on_logstash'); await esArchiver.loadIfNeeded('logstash_functional'); + await security.testUser.setRoles(['test_logstash_reader', 'global_discover_read']); // changing the timepicker default here saves us from having to set it in Discover (~8s) await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': @@ -36,6 +37,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.update({}); await esArchiver.unload('logstash_functional'); await esArchiver.load('empty_kibana'); + await security.testUser.restoreDefaults(); }); it('query should show failed shards pop up', async function () { From 63e6666b1321ed8d57ec65b0e650249189d160e2 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 16 Jul 2020 17:02:15 -0600 Subject: [PATCH 25/76] [Maps] Fix issue preventing TMS from rendering correctly (#71946) * Ensure getColors selector modifies and returns the same object * Call onSourceConfigChange on CreateSourceEditor mount * Back out selector update Co-authored-by: Elastic Machine --- .../kibana_tilemap_source/create_source_editor.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js index 1cbf4c1a87de3..ee557f6244f49 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; @@ -13,10 +13,12 @@ import { i18n } from '@kbn/i18n'; export function CreateSourceEditor({ onSourceConfigChange }) { const tilemap = getKibanaTileMap(); - - if (tilemap.url) { - onSourceConfigChange(); - } + useEffect(() => { + if (tilemap.url) { + onSourceConfigChange(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( @@ -33,7 +35,7 @@ export function CreateSourceEditor({ onSourceConfigChange }) { }) } > - + ); From fead1f2d1d51febe0772cd966b52e5bc633bef24 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 16 Jul 2020 16:42:41 -0700 Subject: [PATCH 26/76] skip flaky suite (#77207) --- .../security_and_spaces/tests/alerting/alerts.ts | 3 ++- 1 file changed, 2 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 0f339154bd948..db1e59746162b 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 @@ -30,7 +30,8 @@ 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/72207 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); From b695d60516ec9551a99ae6ab547657909be2c987 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 16 Jul 2020 17:13:32 -0700 Subject: [PATCH 27/76] [baseline/capture] use high-memory nodes with ramDisks (#71894) Co-authored-by: spalger --- .ci/Jenkinsfile_baseline_capture | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 7c7cc8d98c306..3f90a6bc05af0 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -8,12 +8,12 @@ kibanaPipeline(timeoutMinutes: 120) { catchError { parallel([ 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) } }, 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) } }, From 678dc309afbf2b30d1175374e2e516ec3f02157c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 16 Jul 2020 19:43:15 -0500 Subject: [PATCH 28/76] [Security Solution][Detections,Lists] Miscellaneous post-FF fixes (#71990) * Overview Alerts Histogram stacking defaults to signal.rule.name Since this is now the default for all AlertsHistograms, I've moved this default upstream into the histogram itself. * Replace magic strings with our constant ENDPOINT_LIST_ID Also replaced a few unintentional uses of this string with the non-reserved 'endpoint_list_id'. Co-authored-by: Elastic Machine --- .../schemas/response/exception_list_item_schema.mock.ts | 2 +- .../common/schemas/response/exception_list_schema.mock.ts | 6 ++++-- x-pack/plugins/lists/public/exceptions/api.test.ts | 2 +- .../detections/components/alerts_histogram_panel/index.tsx | 6 +++++- .../pages/detection_engine/rules/create/helpers.ts | 3 ++- .../detections/pages/detection_engine/rules/helpers.tsx | 3 ++- .../overview/components/signals_by_category/index.tsx | 5 ----- .../server/endpoint/lib/artifacts/lists.ts | 3 ++- .../apis/lists/create_exception_list_item.ts | 5 +++-- 9 files changed, 20 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index f7a6af98c8f0e..9e1a88ceb28bd 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -16,7 +16,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ entries: ENTRIES, id: '1', item_id: 'endpoint_list_item', - list_id: 'endpoint_list', + list_id: 'endpoint_list_id', meta: {}, name: 'Sample Endpoint Exception List', namespace_type: 'single', diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 017b959a2baf3..906dcf6560ee5 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ENDPOINT_LIST_ID } from '../..'; + import { ExceptionListSchema } from './exception_list_schema'; export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ @@ -12,10 +14,10 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ created_by: 'user_name', description: 'This is a sample endpoint type exception', id: '1', - list_id: 'endpoint_list', + list_id: ENDPOINT_LIST_ID, meta: {}, name: 'Sample Endpoint Exception List', - namespace_type: 'single', + namespace_type: 'agnostic', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'endpoint', diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 1414d828fa6d4..455670098307f 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -508,7 +508,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), - listId: 'endpoint_list', + listId: 'endpoint_list_id', namespaceType: 'single', pagination: { page: 1, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 560c092d12076..3bc84bb7c32ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -80,10 +80,14 @@ const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ const NO_LEGEND_DATA: LegendItem[] = []; +const DEFAULT_STACK_BY = 'signal.rule.name'; +const getDefaultStackByOption = (): AlertsHistogramOption => + alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; + export const AlertsHistogramPanel = memo( ({ chartHeight, - defaultStackByOption = alertsHistogramOptions[8], // signal.rule.name + defaultStackByOption = getDefaultStackByOption(), deleteQuery, filters, headerChildren, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 226fa5313e34f..38f7836f678f9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -12,6 +12,7 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { NewRule } from '../../../../containers/detection_engine/rules'; import { @@ -167,7 +168,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule ...(isAssociatedToEndpointList ? { exceptions_list: [ - { id: 'endpoint_list', namespace_type: 'agnostic', type: 'endpoint' }, + { id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' }, ] as AboutStepRuleJson['exceptions_list'], } : {}), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 3de508dcbb3be..11b779e71b9b2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -13,6 +13,7 @@ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engin import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { ENDPOINT_LIST_ID } from '../../../../shared_imports'; import { Rule } from '../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -137,7 +138,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu return { isNew: false, author, - isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === 'endpoint_list') ?? false, + isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === ENDPOINT_LIST_ID) ?? false, isBuildingBlock: buildingBlockType !== undefined, license: license ?? '', ruleNameOverride: ruleNameOverride ?? '', diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index fbfdefa13d738..0ac136044c06d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -17,7 +17,6 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; const NO_FILTERS: Filter[] = []; interface Props extends Pick { @@ -62,13 +61,9 @@ const SignalsByCategoryComponent: React.FC = ({ [setAbsoluteRangeDatePicker] ); - const defaultStackByOption = - alertsHistogramOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; - return ( Date: Thu, 16 Jul 2020 20:49:55 -0500 Subject: [PATCH 29/76] [Security Solution][Detections] Disable exceptions for Threshold and ML rules (#72137) * Move isThresholdRule predicate into our common folder This is very similar to isMlRule, which is already used extensively and lives at this level. * Disable endpoint association checkbox for ML and Threshold rules The fullWidth and isDisabled props were not used; what we want is disabled. * Fix react warning about nesting buttons This removes the AdvancedSettingsAccordion in favor of a plain EuiAccordion with buttonContent, as that seems to be all that's needed here. * Disable Exceptions tab on Details for ML or Threshold rules These rule types do not currently support exceptions. * Fix type error Unused import --- .../common/detection_engine/utils.ts | 3 ++ .../rules/select_rule_type/index.tsx | 3 +- .../rules/step_about_rule/index.tsx | 40 ++++++------------ .../detection_engine/rules/details/index.tsx | 42 +++++++++++-------- 4 files changed, 40 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index fa1812235f897..153130fc16d60 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,6 +5,7 @@ */ import { EntriesArray } from '../shared_imports'; +import { RuleType } from './types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -15,3 +16,5 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'nested'); return found.length > 0; }; + +export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 6546c1ba59d84..c6ea269e1a355 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -8,14 +8,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; -const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; - interface SelectRuleTypeProps { describedByIds?: string[]; field: FieldHook; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 1fc88da5fd7c7..ec812fa63eadf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, RuleStep, @@ -58,26 +60,6 @@ const TagContainer = styled.div` TagContainer.displayName = 'TagContainer'; -const AdvancedSettingsAccordion = styled(EuiAccordion)` - .euiAccordion__iconWrapper { - display: none; - } - - .euiAccordion__childWrapper { - transition-duration: 1ms; /* hack to fire Step accordion to set proper content's height */ - } - - &.euiAccordion-isOpen .euiButtonEmpty__content > svg { - transform: rotate(90deg); - } -`; - -const AdvancedSettingsAccordionButton = ( - - {I18n.ADVANCED_SETTINGS} - -); - const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, @@ -94,6 +76,10 @@ const StepAboutRuleComponent: FC = ({ const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( defineRuleData?.index ?? [] ); + const canUseExceptions = + defineRuleData?.ruleType && + !isMlRule(defineRuleData.ruleType) && + !isThresholdRule(defineRuleData.ruleType); const { form } = useForm({ defaultValue: initialState, @@ -193,10 +179,10 @@ const StepAboutRuleComponent: FC = ({ /> - = ({ idAria: 'detectionEngineStepAboutRuleAssociatedToEndpointList', 'data-test-subj': 'detectionEngineStepAboutRuleAssociatedToEndpointList', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading || !canUseExceptions, }, }} /> @@ -287,8 +272,7 @@ const StepAboutRuleComponent: FC = ({ idAria: 'detectionEngineStepAboutRuleBuildingBlock', 'data-test-subj': 'detectionEngineStepAboutRuleBuildingBlock', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading, }, }} /> @@ -319,7 +303,7 @@ const StepAboutRuleComponent: FC = ({ placeholder: '', }} /> - + {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 7eb5c3a535377..484c28b4b428c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -37,7 +37,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { useRule } from '../../../../containers/detection_engine/rules'; +import { useRule, Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { useWithSource } from '../../../../../common/containers/source'; @@ -90,6 +90,8 @@ import { MIN_EVENTS_VIEWER_BODY_HEIGHT, } from '../../../../../timelines/components/timeline/body/helpers'; import { footerHeight } from '../../../../../timelines/components/timeline/footer'; +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; enum RuleDetailTabs { alerts = 'alerts', @@ -97,23 +99,26 @@ enum RuleDetailTabs { exceptions = 'exceptions', } -const ruleDetailTabs = [ - { - id: RuleDetailTabs.alerts, - name: detectionI18n.ALERT, - disabled: false, - }, - { - id: RuleDetailTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - }, - { - id: RuleDetailTabs.failures, - name: i18n.FAILURE_HISTORY_TAB, - disabled: false, - }, -]; +const getRuleDetailsTabs = (rule: Rule | null) => { + const canUseExceptions = rule && !isMlRule(rule.type) && !isThresholdRule(rule.type); + return [ + { + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, + disabled: false, + }, + { + id: RuleDetailTabs.exceptions, + name: i18n.EXCEPTIONS_TAB, + disabled: !canUseExceptions, + }, + { + id: RuleDetailTabs.failures, + name: i18n.FAILURE_HISTORY_TAB, + disabled: false, + }, + ]; +}; export const RuleDetailsPageComponent: FC = ({ filters, @@ -160,6 +165,7 @@ export const RuleDetailsPageComponent: FC = ({ // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const ruleDetailTabs = getRuleDetailsTabs(rule); const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( From 55da30d17a7489a95c158b5cea01b5ddb785dbc9 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 16 Jul 2020 19:11:08 -0700 Subject: [PATCH 30/76] skip flaky suite (#64696) --- x-pack/test/api_integration/apis/fleet/unenroll_agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index bc6c44e590cc4..4e1443ad1fc68 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,7 +16,8 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - describe('fleet_unenroll_agent', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64696 + describe.skip('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { From effab78a94524a6ab428b57f407a045f27ae4954 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 16 Jul 2020 23:00:22 -0500 Subject: [PATCH 31/76] [Security Solution][Detections] Better toast errors (#72205) * Add new hook to wrap the toasts service When receiving error responses from our APIs, this gives us better toast messages. * Replace useToasts with useAppToasts in trivial case * WIP: prevent infinite polling when server is unresponsive The crux of this issue was that we had no steady state when the server returned a non-API error (!isApiError), as it would if the server was throwing 500s or just generally misbehaving. The solution, then, is to addresse these non-API errors in our underlying useListsIndex and useListsPrivileges hooks. This also refactors those hooks to: * collapse multiple error states into one (that's all we currently need) * use useAppToasts for better UI TODO: I don't think I need the changes in useListsConfig's useEffect. * Slightly more legible variables The only task in this hook is our readPrivileges task right now, so I'm shortening the variable until we have a need to disambiguate it further. * Remove unnecessary conditions around creating our index If the index hook has an error needsIndex will not be true. * Better toast errors for Kibana API errors Our isApiError predicate does not work for errors coming back from Kibana platform itself, e.g. for a request payload error. I've added a separate predicate for that case, isKibanaError, and then a wrapping isAppError predicate since most of our use cases just care about error.body.message, which is common to both. * Use new toasts hook on our exceptions modals This fixes two issues: * toast appears above modal overlay * Error message from response is now presented in the toast * Fix bug with toasts dependencies Because of the way some of the exception modal's hooks are written, a change to one of its callbacks means that the request will be canceled. Because the toasts service exports instance methods, the context within the function (and thus the function itself) can change leading to a mutable ref. Because we don't want/need this behavior, we store our exported functions in refs to 'freeze' them for react. With our bound functions, we should now be able to declare e.g. `toast.addError` as a dependency, however react cannot determine that it is bound (and thus that toast.addError() is equivalent to addError()), and so we must destructure our functions in order to use them as dependencies. * Alert clipboard toasts through new Toasts service This fixes the z-index issue between modals and toasts. * Fix type errors * Mock external dependency These tests now call out to the Notifications service (in a context) instead of our redux implementation. --- .../exceptions/add_exception_modal/index.tsx | 12 ++-- .../exceptions/edit_exception_modal/index.tsx | 12 ++-- .../viewer/exception_item/index.test.tsx | 2 + .../common/components/toasters/utils.ts | 4 +- .../common/hooks/use_app_toasts.test.ts | 63 +++++++++++++++++ .../public/common/hooks/use_app_toasts.ts | 48 +++++++++++++ .../public/common/lib/clipboard/clipboard.tsx | 23 ++----- .../common/lib/kibana/__mocks__/index.ts | 2 + .../public/common/utils/api/index.ts | 23 ++++++- .../value_lists_management_modal/modal.tsx | 13 ++-- .../alerts/use_signal_index.tsx | 6 +- .../lists/use_lists_config.tsx | 6 +- .../lists/use_lists_index.tsx | 69 ++++++++----------- .../lists/use_lists_privileges.tsx | 29 ++++---- 14 files changed, 211 insertions(+), 101 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index d5eeef0f1e768..79383676266f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -29,8 +29,8 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; import { ExceptionBuilder } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; @@ -115,7 +115,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ Array >([]); const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -124,15 +124,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const onError = useCallback( (error: Error) => { - errorToToaster({ title: i18n.ADD_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.ADD_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.ADD_EXCEPTION_SUCCESS); onConfirm(shouldCloseAlert); - }, [dispatchToaster, onConfirm, shouldCloseAlert]); + }, [addSuccess, onConfirm, shouldCloseAlert]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 73933d483e2cb..dbc70dfe21dd0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -30,7 +30,7 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { ExceptionBuilder } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; @@ -93,7 +93,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -102,15 +102,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const onError = useCallback( (error) => { - errorToToaster({ title: i18n.EDIT_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.EDIT_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); - }, [dispatchToaster, onConfirm]); + }, [addSuccess, onConfirm]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx index 0e2908fc34232..90752f9450e4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -13,6 +13,8 @@ import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +jest.mock('../../../../lib/kibana'); + describe('ExceptionItem', () => { it('it renders ExceptionDetails and ExceptionEntries', () => { const exceptionItem = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts index e7cc389d4c06b..47c5588a12830 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts @@ -9,7 +9,7 @@ import { isError } from 'lodash/fp'; import { AppToast, ActionToaster } from './'; import { isToasterError } from './errors'; -import { isApiError } from '../../utils/api'; +import { isAppError } from '../../utils/api'; /** * Displays an error toast for the provided title and message @@ -114,7 +114,7 @@ export const errorToToaster = ({ iconType, errors: error.messages, }; - } else if (isApiError(error)) { + } else if (isAppError(error)) { toast = { id, title, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts new file mode 100644 index 0000000000000..e0e629793952a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useToasts } from '../lib/kibana'; +import { useAppToasts } from './use_app_toasts'; + +jest.mock('../lib/kibana'); + +describe('useDeleteList', () => { + let addErrorMock: jest.Mock; + let addSuccessMock: jest.Mock; + + beforeEach(() => { + addErrorMock = jest.fn(); + addSuccessMock = jest.fn(); + (useToasts as jest.Mock).mockImplementation(() => ({ + addError: addErrorMock, + addSuccess: addSuccessMock, + })); + }); + + it('works normally with a regular error', async () => { + const error = new Error('regular error'); + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(error, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(error, { title: 'title' }); + }); + + it("uses a AppError's body.message as the toastMessage", async () => { + const kibanaApiError = { + message: 'Not Found', + body: { status_code: 404, message: 'Detailed Message' }, + }; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(kibanaApiError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(kibanaApiError, { + title: 'title', + toastMessage: 'Detailed Message', + }); + }); + + it('converts an unknown error to an Error', () => { + const unknownError = undefined; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(unknownError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), { + title: 'title', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts new file mode 100644 index 0000000000000..bc59d87100058 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useRef } from 'react'; + +import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public'; +import { useToasts } from '../lib/kibana'; +import { isAppError, AppError } from '../utils/api'; + +export type UseAppToasts = Pick & { + api: ToastsStart; + addError: (error: unknown, options: ErrorToastOptions) => Toast; +}; + +export const useAppToasts = (): UseAppToasts => { + const toasts = useToasts(); + const addError = useRef(toasts.addError.bind(toasts)).current; + const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; + + const addAppError = useCallback( + (error: AppError, options: ErrorToastOptions) => + addError(error, { + ...options, + toastMessage: error.body.message, + }), + [addError] + ); + + const _addError = useCallback( + (error: unknown, options: ErrorToastOptions) => { + if (isAppError(error)) { + return addAppError(error, options); + } else { + if (error instanceof Error) { + return addError(error, options); + } else { + return addError(new Error(String(error)), options); + } + } + }, + [addAppError, addError] + ); + + return { api: toasts, addError: _addError, addSuccess }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx index fdb6ed130a525..75b7308ab61f1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiGlobalToastListToast as Toast, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; import copy from 'copy-to-clipboard'; import React from 'react'; -import uuid from 'uuid'; import * as i18n from './translations'; -import { useStateToaster } from '../../components/toasters'; +import { useAppToasts } from '../../hooks/use_app_toasts'; export type OnCopy = ({ content, @@ -20,17 +19,6 @@ export type OnCopy = ({ isSuccess: boolean; }) => void; -interface GetSuccessToastParams { - titleSummary?: string; -} - -const getSuccessToast = ({ titleSummary }: GetSuccessToastParams): Toast => ({ - id: `copy-success-${uuid.v4()}`, - color: 'success', - iconType: 'copyClipboard', - title: `${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, -}); - interface Props { children?: JSX.Element; content: string | number; @@ -40,7 +28,7 @@ interface Props { } export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => { - const dispatchToaster = useStateToaster()[1]; + const { addSuccess } = useAppToasts(); const onClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -52,10 +40,7 @@ export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTi } if (isSuccess) { - dispatchToaster({ - type: 'addToaster', - toast: { toastLifeTimeMs, ...getSuccessToast({ titleSummary }) }, - }); + addSuccess(`${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, { toastLifeTimeMs }); } }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index c3e1f35f37356..6ada887ece175 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createKibanaContextProviderMock, createUseUiSettingMock, @@ -19,6 +20,7 @@ export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); +export const useToasts = jest.fn(() => notificationServiceMock.createStartContract().toasts); export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/plugins/security_solution/public/common/utils/api/index.ts b/x-pack/plugins/security_solution/public/common/utils/api/index.ts index ab442d0d09cf9..e8934259fe43e 100644 --- a/x-pack/plugins/security_solution/public/common/utils/api/index.ts +++ b/x-pack/plugins/security_solution/public/common/utils/api/index.ts @@ -6,14 +6,33 @@ import { has } from 'lodash/fp'; -export interface KibanaApiError { +export interface AppError { name: string; message: string; + body: { + message: string; + }; +} + +export interface KibanaError extends AppError { + body: { + message: string; + statusCode: number; + }; +} + +export interface SecurityAppError extends AppError { body: { message: string; status_code: number; }; } -export const isApiError = (error: unknown): error is KibanaApiError => +export const isKibanaError = (error: unknown): error is KibanaError => + has('message', error) && has('body.message', error) && has('body.statusCode', error); + +export const isSecurityAppError = (error: unknown): error is SecurityAppError => has('message', error) && has('body.message', error) && has('body.status_code', error); + +export const isAppError = (error: unknown): error is AppError => + isKibanaError(error) || isSecurityAppError(error); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index 0a935a9cdb1c4..d7d4be6d951b8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -23,7 +23,8 @@ import { useDeleteList, useCursor, } from '../../../shared_imports'; -import { useToasts, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations'; import { ValueListsTable } from './table'; @@ -45,7 +46,7 @@ export const ValueListsModalComponent: React.FC = ({ const { start: findLists, ...lists } = useFindLists(); const { start: deleteList, result: deleteResult } = useDeleteList(); const [exportListId, setExportListId] = useState(); - const toasts = useToasts(); + const { addError, addSuccess } = useAppToasts(); const fetchLists = useCallback(() => { findLists({ cursor, http, pageIndex: pageIndex + 1, pageSize }); @@ -82,21 +83,21 @@ export const ValueListsModalComponent: React.FC = ({ const handleUploadError = useCallback( (error: Error) => { if (error.name !== 'AbortError') { - toasts.addError(error, { title: i18n.UPLOAD_ERROR }); + addError(error, { title: i18n.UPLOAD_ERROR }); } }, - [toasts] + [addError] ); const handleUploadSuccess = useCallback( (response: ListSchema) => { - toasts.addSuccess({ + addSuccess({ text: i18n.uploadSuccessMessage(response.name), title: i18n.UPLOAD_SUCCESS_TITLE, }); fetchLists(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [toasts] + [addSuccess] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 65a2721013b5e..14fd9ffa50843 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { isApiError } from '../../../../common/utils/api'; +import { isSecurityAppError } from '../../../../common/utils/api'; type Func = () => void; @@ -59,7 +59,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { signalIndexName: null, createDeSignalIndex: createIndex, }); - if (isApiError(error) && error.body.status_code !== 404) { + if (isSecurityAppError(error) && error.body.status_code !== 404) { errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); } } @@ -81,7 +81,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { } } catch (error) { if (isSubscribed) { - if (isApiError(error) && error.body.status_code === 409) { + if (isSecurityAppError(error) && error.body.status_code === 409) { fetchData(); } else { setSignalIndex({ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx index e21cbceeaef27..71847e7b7d8cb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx @@ -19,16 +19,16 @@ export interface UseListsConfigReturn { } export const useListsConfig = (): UseListsConfigReturn => { - const { createIndex, createIndexError, indexExists, loading: indexLoading } = useListsIndex(); + const { createIndex, indexExists, loading: indexLoading, error: indexError } = useListsIndex(); const { canManageIndex, canWriteIndex, loading: privilegesLoading } = useListsPrivileges(); const { lists } = useKibana().services; const enabled = lists != null; const loading = indexLoading || privilegesLoading; const needsIndex = indexExists === false; - const indexCreationFailed = createIndexError != null; + const hasIndexError = indexError != null; const needsIndexConfiguration = - needsIndex && (canManageIndex === false || (canManageIndex === true && indexCreationFailed)); + needsIndex && (canManageIndex === false || (canManageIndex === true && hasIndexError)); const needsConfiguration = !enabled || canWriteIndex === false || needsIndexConfiguration; useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx index 75f12bd07d3ae..ee1316eb8a1fd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx @@ -7,28 +7,24 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListIndex, useCreateListIndex } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { isSecurityAppError } from '../../../../common/utils/api'; import * as i18n from './translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -export interface UseListsIndexState { +export interface UseListsIndexReturn { + createIndex: () => void; indexExists: boolean | null; -} - -export interface UseListsIndexReturn extends UseListsIndexState { + error: unknown; loading: boolean; - createIndex: () => void; - createIndexError: unknown; - createIndexResult: { acknowledged: boolean } | undefined; } export const useListsIndex = (): UseListsIndexReturn => { - const [state, setState] = useState({ - indexExists: null, - }); + const [indexExists, setIndexExists] = useState(null); + const [error, setError] = useState(null); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); + const { addError } = useAppToasts(); const { loading: readLoading, start: readListIndex, ...readListIndexState } = useReadListIndex(); const { loading: createLoading, @@ -51,18 +47,17 @@ export const useListsIndex = (): UseListsIndexReturn => { // initial read list useEffect(() => { - if (!readLoading && state.indexExists === null) { + if (!readLoading && !error && indexExists === null) { readIndex(); } - }, [readIndex, readLoading, state.indexExists]); + }, [error, indexExists, readIndex, readLoading]); // handle read result useEffect(() => { if (readListIndexState.result != null) { - setState({ - indexExists: - readListIndexState.result.list_index && readListIndexState.result.list_item_index, - }); + setIndexExists( + readListIndexState.result.list_index && readListIndexState.result.list_item_index + ); } }, [readListIndexState.result]); @@ -75,34 +70,30 @@ export const useListsIndex = (): UseListsIndexReturn => { // handle read error useEffect(() => { - const error = readListIndexState.error; - if (isApiError(error)) { - setState({ indexExists: false }); - if (error.body.status_code !== 404) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_FETCH_FAILURE, - toastMessage: error.body.message, - }); + const err = readListIndexState.error; + if (err != null) { + if (isSecurityAppError(err) && err.body.status_code === 404) { + setIndexExists(false); + } else { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_FETCH_FAILURE }); } } - }, [readListIndexState.error, toasts]); + }, [addError, readListIndexState.error]); // handle create error useEffect(() => { - const error = createListIndexState.error; - if (isApiError(error)) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_CREATE_FAILURE, - toastMessage: error.body.message, - }); + const err = createListIndexState.error; + if (err != null) { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_CREATE_FAILURE }); } - }, [createListIndexState.error, toasts]); + }, [addError, createListIndexState.error]); return { - loading, createIndex, - createIndexError: createListIndexState.error, - createIndexResult: createListIndexState.result, - ...state, + error, + indexExists, + loading, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx index fbbcff33402c3..f99f62b1948e6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx @@ -7,8 +7,8 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListPrivileges } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; export interface UseListsPrivilegesState { @@ -79,8 +79,8 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { }); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); - const { loading, start: readListPrivileges, ...privilegesState } = useReadListPrivileges(); + const { addError } = useAppToasts(); + const { loading, start: readListPrivileges, ...readState } = useReadListPrivileges(); const readPrivileges = useCallback(() => { if (lists) { @@ -90,20 +90,20 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { // initRead useEffect(() => { - if (!loading && state.isAuthenticated === null) { + if (!loading && !readState.error && state.isAuthenticated === null) { readPrivileges(); } - }, [loading, readPrivileges, state.isAuthenticated]); + }, [loading, readState.error, readPrivileges, state.isAuthenticated]); // handleReadResult useEffect(() => { - if (privilegesState.result != null) { + if (readState.result != null) { try { const { is_authenticated: isAuthenticated, lists: { index: listsPrivileges }, listItems: { index: listItemsPrivileges }, - } = privilegesState.result as ListPrivileges; + } = readState.result as ListPrivileges; setState({ isAuthenticated, @@ -114,19 +114,18 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); } } - }, [privilegesState.result]); + }, [readState.result]); // handleReadError useEffect(() => { - const error = privilegesState.error; - if (isApiError(error)) { - setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); - toasts.addError(error, { + const error = readState.error; + if (error != null) { + setState({ isAuthenticated: false, canManageIndex: false, canWriteIndex: false }); + addError(error, { title: i18n.LISTS_PRIVILEGES_READ_FAILURE, - toastMessage: error.body.message, }); } - }, [privilegesState.error, toasts]); + }, [addError, readState.error]); return { loading, ...state }; }; From 5cd1f6e562a558a6d725806e6a5bc5eb722c4b57 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 16 Jul 2020 22:42:28 -0700 Subject: [PATCH 32/76] Call setupIngest before fleet_install tests (#72214) --- x-pack/test/api_integration/apis/fleet/install.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/install.ts b/x-pack/test/api_integration/apis/fleet/install.ts index 3a122463fae55..59b040e30fb48 100644 --- a/x-pack/test/api_integration/apis/fleet/install.ts +++ b/x-pack/test/api_integration/apis/fleet/install.ts @@ -6,11 +6,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { setupIngest } from './agents/services'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); describe('fleet_install', () => { + setupIngest(providerContext); + it('should return a 400 if we try download an install script for a not supported OS', async () => { await supertest.get(`/api/ingest_manager/fleet/install/gameboy`).expect(400); }); From 6164037f18b4d715f16bc42fc932576d089692c3 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 17 Jul 2020 09:14:37 +0200 Subject: [PATCH 33/76] Fix value for process.hash.sha256 draggable (#72142) --- .../system/generic_file_details.test.tsx | 18 ++++++++++++++++++ .../renderers/system/generic_file_details.tsx | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx index 2c207ea8a1562..dd5b182c8c124 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -11,6 +11,7 @@ import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import '../../../../../../common/mock/match_media'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { mockEndgameCreationEvent } from '../../../../../../common/mock/mock_endgame_ecs_data'; import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; @@ -61,6 +62,23 @@ describe('SystemGenericFileDetails', () => { 'Evan@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' ); }); + + test('it passes string value for processHashSha256', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('SystemGenericFileLine').prop('processHashSha256')).toEqual( + 'd4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee' + ); + }); }); describe('#SystemGenericFileLine', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx index 8dd513539a96a..3a95c0ea927c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx @@ -240,7 +240,7 @@ export const SystemGenericFileDetails = React.memo( const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); const processHashMd5: string | null | undefined = get('process.hash.md5[0]', data); const processHashSha1: string | null | undefined = get('process.hash.sha1[0]', data); - const processHashSha256: string | null | undefined = get('process.hash.sha256', data); + const processHashSha256: string | null | undefined = get('process.hash.sha256[0]', data); const processPid: number | null | undefined = get('process.pid[0]', data); const processPpid: number | null | undefined = get('process.ppid[0]', data); const processName: string | null | undefined = get('process.name[0]', data); From 99e14aad761ddc5f4db53d8e23815a85241c0bf0 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 17 Jul 2020 05:38:55 -0400 Subject: [PATCH 34/76] Register navLink actions for declared applications (#72109) --- .../feature_privilege_builder/navlink.ts | 5 ++++- .../privileges/privileges.test.ts | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index dd076477a9c11..f25632407be86 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -9,6 +9,9 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { - return feature.navLinkId ? [this.actions.ui.get('navLinks', feature.navLinkId)] : []; + const appNavLinks = feature.app.map((app) => this.actions.ui.get('navLinks', app)); + return feature.navLinkId + ? [this.actions.ui.get('navLinks', feature.navLinkId), ...appNavLinks] + : appNavLinks; } } diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 8a499a3eba8fa..d8ece8f68d425 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -54,8 +54,20 @@ describe('features', () => { const actual = privileges.get(); expect(actual).toHaveProperty('features.foo-feature', { - all: [actions.login, actions.version, actions.ui.get('navLinks', 'kibana:foo')], - read: [actions.login, actions.version, actions.ui.get('navLinks', 'kibana:foo')], + all: [ + actions.login, + actions.version, + actions.ui.get('navLinks', 'kibana:foo'), + actions.ui.get('navLinks', 'app-1'), + actions.ui.get('navLinks', 'app-2'), + ], + read: [ + actions.login, + actions.version, + actions.ui.get('navLinks', 'kibana:foo'), + actions.ui.get('navLinks', 'app-1'), + actions.ui.get('navLinks', 'app-2'), + ], }); }); @@ -635,6 +647,8 @@ describe('reserved', () => { expect(actual).toHaveProperty('reserved.foo', [ actions.version, actions.ui.get('navLinks', 'kibana:foo'), + actions.ui.get('navLinks', 'app-1'), + actions.ui.get('navLinks', 'app-2'), ]); }); From 9296369bfd8cc3325460fd3e761e2a9648436bdb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 17 Jul 2020 11:04:14 +0100 Subject: [PATCH 35/76] chore(NA): teardown dynamic dll plugin (#72096) * chore(NA): teardown dynamic dll plugin * chore(NA): remove missing ts-ignore * chore(NA): remove last mentions to the DLL machinery * chore(NA): update notice file * prevent duplication and searching target/public * remove changes to es-ui code to unblock pr * add node internals override for legacy tests bundle Co-authored-by: spalger Co-authored-by: Elastic Machine --- NOTICE.txt | 3 - package.json | 1 - packages/kbn-babel-code-parser/.babelrc | 3 - packages/kbn-babel-code-parser/README.md | 19 - packages/kbn-babel-code-parser/package.json | 26 -- .../kbn-babel-code-parser/src/can_require.js | 36 -- .../kbn-babel-code-parser/src/code_parser.js | 105 ----- packages/kbn-babel-code-parser/src/index.js | 22 -- .../kbn-babel-code-parser/src/strategies.js | 103 ----- .../src/strategies.test.js | 108 ------ .../kbn-babel-code-parser/src/visitors.js | 124 ------ .../src/visitors.test.js | 68 ---- packages/kbn-babel-code-parser/yarn.lock | 1 - src/dev/build/README.md | 11 - src/dev/build/build_distributables.js | 2 - src/dev/build/tasks/index.js | 1 - .../clean_client_modules_on_dll_task.js | 126 ------ .../tasks/nodejs_modules/get_dependencies.js | 27 -- src/dev/build/tasks/nodejs_modules/index.js | 20 - .../build/tasks/nodejs_modules/webpack_dll.js | 131 ------- .../tasks/nodejs_modules/webpack_dll.test.js | 123 ------ src/dev/build/tasks/optimize_task.js | 1 - src/dev/notice/generate_notice_from_source.ts | 9 +- src/legacy/core_plugins/tests_bundle/index.js | 7 + .../server/logging/log_format_string.js | 2 - src/legacy/ui/ui_render/ui_render_mixin.js | 11 - src/optimize/base_optimizer.js | 8 - .../bundles_route/__tests__/bundles_route.js | 12 - src/optimize/bundles_route/bundles_route.ts | 19 +- .../bundles_route/proxy_bundles_route.ts | 1 - .../dynamic_dll_plugin/dll_allowed_modules.js | 36 -- .../dynamic_dll_plugin/dll_compiler.js | 366 ------------------ .../dynamic_dll_plugin/dll_config_model.js | 278 ------------- .../dynamic_dll_plugin/dll_entry_template.js | 37 -- .../dynamic_dll_plugin/dynamic_dll_plugin.js | 354 ----------------- src/optimize/dynamic_dll_plugin/index.js | 21 - src/optimize/optimize_mixin.ts | 5 +- src/optimize/watch/optmzr_role.js | 10 - src/optimize/watch/watch.js | 2 +- src/optimize/watch/watch_cache.ts | 189 --------- src/optimize/watch/watch_optimizer.js | 6 - tasks/config/karma.js | 12 - 42 files changed, 15 insertions(+), 2431 deletions(-) delete mode 100644 packages/kbn-babel-code-parser/.babelrc delete mode 100755 packages/kbn-babel-code-parser/README.md delete mode 100755 packages/kbn-babel-code-parser/package.json delete mode 100644 packages/kbn-babel-code-parser/src/can_require.js delete mode 100644 packages/kbn-babel-code-parser/src/code_parser.js delete mode 100644 packages/kbn-babel-code-parser/src/index.js delete mode 100644 packages/kbn-babel-code-parser/src/strategies.js delete mode 100644 packages/kbn-babel-code-parser/src/strategies.test.js delete mode 100644 packages/kbn-babel-code-parser/src/visitors.js delete mode 100644 packages/kbn-babel-code-parser/src/visitors.test.js delete mode 120000 packages/kbn-babel-code-parser/yarn.lock delete mode 100644 src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js delete mode 100644 src/dev/build/tasks/nodejs_modules/get_dependencies.js delete mode 100644 src/dev/build/tasks/nodejs_modules/index.js delete mode 100644 src/dev/build/tasks/nodejs_modules/webpack_dll.js delete mode 100644 src/dev/build/tasks/nodejs_modules/webpack_dll.test.js delete mode 100644 src/optimize/dynamic_dll_plugin/dll_allowed_modules.js delete mode 100644 src/optimize/dynamic_dll_plugin/dll_compiler.js delete mode 100644 src/optimize/dynamic_dll_plugin/dll_config_model.js delete mode 100644 src/optimize/dynamic_dll_plugin/dll_entry_template.js delete mode 100644 src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js delete mode 100644 src/optimize/dynamic_dll_plugin/index.js delete mode 100644 src/optimize/watch/watch_cache.ts diff --git a/NOTICE.txt b/NOTICE.txt index 56280e6e3883e..e1552852d0349 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -26,9 +26,6 @@ This module was heavily inspired by the externals plugin that ships with webpack MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra ---- -This product has relied on ASTExplorer that is licensed under MIT. - --- This product includes code that is based on Ace editor, which was available under a "BSD" license. diff --git a/package.json b/package.json index 541fcc46a3d6e..593e8f82f0541 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,6 @@ "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", "@kbn/analytics": "1.0.0", - "@kbn/babel-code-parser": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/packages/kbn-babel-code-parser/.babelrc b/packages/kbn-babel-code-parser/.babelrc deleted file mode 100644 index 7da72d1779128..0000000000000 --- a/packages/kbn-babel-code-parser/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-babel-code-parser/README.md b/packages/kbn-babel-code-parser/README.md deleted file mode 100755 index f99d01541ee22..0000000000000 --- a/packages/kbn-babel-code-parser/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# @kbn/babel-code-parser - -Simple abstraction over the `@babel/parser` and the `@babel/traverse` in order -to build a code parser on top. - -We have two main functions `parseSingleFile` (sync and sync version) and the -`parseEntries` (only async version). The first one just parse one entry file -and the second one parses recursively all the files from a list of -start entry points. - -Then we have `visitors` and `strategies`. The first ones are basically the -`visitors` to use into the ast from the `@babel/traverse`. They are the only -way to collect info when using the `parseSingleFile`. The `strategies` are -meant to be used with the `parseEntries` and configures the info we want -to collect from our parsed code. After each loop, one per entry file, the -`parseEntries` method will call the given `strategy` expecting that -`strategy` would call the desired visitors, assemble the important -information to collect and adds them to the final results. - diff --git a/packages/kbn-babel-code-parser/package.json b/packages/kbn-babel-code-parser/package.json deleted file mode 100755 index 6f42c086ecaab..0000000000000 --- a/packages/kbn-babel-code-parser/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@kbn/babel-code-parser", - "description": "babel code parser for Kibana", - "private": true, - "version": "1.0.0", - "main": "./target/index.js", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/elastic/kibana/tree/master/packages/kbn-babel-code-parser" - }, - "scripts": { - "build": "babel src --out-dir target", - "kbn:bootstrap": "yarn build --quiet", - "kbn:watch": "yarn build --watch" - }, - "devDependencies": { - "@babel/cli": "^7.10.1" - }, - "dependencies": { - "@kbn/babel-preset": "1.0.0", - "@babel/parser": "^7.10.2", - "@babel/traverse": "^7.10.1", - "lodash": "^4.17.15" - } -} diff --git a/packages/kbn-babel-code-parser/src/can_require.js b/packages/kbn-babel-code-parser/src/can_require.js deleted file mode 100644 index b410611237766..0000000000000 --- a/packages/kbn-babel-code-parser/src/can_require.js +++ /dev/null @@ -1,36 +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. - */ - -export function canRequire(entry, cwd = require.resolve.paths(entry) || []) { - try { - // We will try to test if we can resolve - // this entry through the require.resolve - // setting as the start looking path the - // given cwd. That cwd variable could be - // a path or an array of paths - // from where Require.resolve will keep - // looking recursively as normal starting - // from those locations. - return require.resolve(entry, { - paths: [].concat(cwd), - }); - } catch (e) { - return false; - } -} diff --git a/packages/kbn-babel-code-parser/src/code_parser.js b/packages/kbn-babel-code-parser/src/code_parser.js deleted file mode 100644 index 91927780363ac..0000000000000 --- a/packages/kbn-babel-code-parser/src/code_parser.js +++ /dev/null @@ -1,105 +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 { canRequire } from './can_require'; -import { readFile, readFileSync } from 'fs'; -import { extname } from 'path'; -import { promisify } from 'util'; -import * as parser from '@babel/parser'; -import traverse from '@babel/traverse'; -import * as babelParserOptions from '@kbn/babel-preset/common_babel_parser_options'; - -const read = promisify(readFile); - -function _cannotParseFile(filePath) { - return extname(filePath) !== '.js'; -} - -function _parseAndTraverseFileContent(fileContent, visitorsGenerator) { - const results = []; - - // Parse and get the code AST - // All the babel parser plugins - // were enabled - const ast = parser.parse(fileContent, babelParserOptions); - - // Loop through the code AST with - // the defined visitors - traverse(ast, visitorsGenerator(results)); - - return results; -} - -export async function parseSingleFile(filePath, visitorsGenerator) { - // Don't parse any other files than .js ones - if (_cannotParseFile(filePath)) { - return []; - } - - // Read the file - const content = await read(filePath, { encoding: 'utf8' }); - - // return the results found on parse and traverse - // the file content with the given visitors - return _parseAndTraverseFileContent(content, visitorsGenerator); -} - -export function parseSingleFileSync(filePath, visitorsGenerator) { - // Don't parse any other files than .js ones - if (_cannotParseFile(filePath)) { - return []; - } - - // Read the file - const content = readFileSync(filePath, { encoding: 'utf8' }); - - // return the results found on parse and traverse - // the file content with the given visitors - return _parseAndTraverseFileContent(content, visitorsGenerator); -} - -export async function parseEntries(cwd, entries, strategy, results, wasParsed = {}) { - // Assure that we always have a cwd - const sanitizedCwd = cwd || process.cwd(); - - // Test each entry against canRequire function - const entriesQueue = entries.map((entry) => canRequire(entry)); - - while (entriesQueue.length) { - // Get the first element in the queue as - // select it as our current entry to parse - const mainEntry = entriesQueue.shift(); - - // Avoid parse the current entry if it is not valid - // or it was already parsed - if (typeof mainEntry !== 'string' || wasParsed[mainEntry]) { - continue; - } - - // Find new entries and adds them to the end of the queue - entriesQueue.push( - ...(await strategy(sanitizedCwd, parseSingleFile, mainEntry, wasParsed, results)) - ); - - // Mark the current main entry as already parsed - wasParsed[mainEntry] = true; - } - - return results; -} diff --git a/packages/kbn-babel-code-parser/src/index.js b/packages/kbn-babel-code-parser/src/index.js deleted file mode 100644 index 70e4eb18068b0..0000000000000 --- a/packages/kbn-babel-code-parser/src/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { dependenciesParseStrategy } from './strategies'; -export { dependenciesVisitorsGenerator } from './visitors'; -export { parseSingleFile, parseSingleFileSync, parseEntries } from './code_parser'; diff --git a/packages/kbn-babel-code-parser/src/strategies.js b/packages/kbn-babel-code-parser/src/strategies.js deleted file mode 100644 index 2369692ad434b..0000000000000 --- a/packages/kbn-babel-code-parser/src/strategies.js +++ /dev/null @@ -1,103 +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 { canRequire } from './can_require'; -import { dependenciesVisitorsGenerator } from './visitors'; -import { dirname, isAbsolute, resolve } from 'path'; -import { builtinModules } from 'module'; - -export function _calculateTopLevelDependency(inputDep, outputDep = '') { - // The path separator will be always the forward slash - // as at this point we only have the found entries into - // the provided source code entries where we just use it - const pathSeparator = '/'; - const depSplitPaths = inputDep.split(pathSeparator); - const firstPart = depSplitPaths.shift(); - const outputDepFirstArgAppend = outputDep ? pathSeparator : ''; - - outputDep += `${outputDepFirstArgAppend}${firstPart}`; - - // In case our dependency isn't started by @ - // we are already done and we can return the - // dependency value we already have - if (firstPart.charAt(0) !== '@') { - return outputDep; - } - - // Otherwise we need to keep constructing the dependency - // value because dependencies starting with @ points to - // folders of dependencies. For example, in case we found - // dependencies values with '@the-deps/a' and '@the-deps/a/b' - // we don't want to map it to '@the-deps' but also to @'the-deps/a' - // because inside '@the-deps' we can also have '@the-dep/b' - return _calculateTopLevelDependency(depSplitPaths.join(pathSeparator), outputDep); -} - -export async function dependenciesParseStrategy( - cwd, - parseSingleFile, - mainEntry, - wasParsed, - results -) { - // Get dependencies from a single file and filter - // out node native modules from the result - const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)).filter( - (dep) => !builtinModules.includes(dep) - ); - - // Return the list of all the new entries found into - // the current mainEntry that we could use to look for - // new dependencies - return dependencies.reduce((filteredEntries, entry) => { - const absEntryPath = resolve(cwd, dirname(mainEntry), entry); - - // NOTE: cwd for following canRequires is absEntryPath - // because we should start looking from there - const requiredPath = canRequire(absEntryPath, absEntryPath); - const requiredRelativePath = canRequire(entry, absEntryPath); - - const isRelativeFile = !isAbsolute(entry); - const isNodeModuleDep = isRelativeFile && !requiredPath && requiredRelativePath; - const isNewEntry = isRelativeFile && requiredPath; - - // If it is a node_module add it to the results and also - // add the resolved path for the node_module main file - // as an entry point to look for dependencies it was - // not already parsed - if (isNodeModuleDep) { - // Save the result as the top level dependency - results[_calculateTopLevelDependency(entry)] = true; - - if (!wasParsed[requiredRelativePath]) { - filteredEntries.push(requiredRelativePath); - } - } - - // If a new, not yet parsed, relative entry were found - // add it to the list of entries to be parsed - if (isNewEntry && !wasParsed[requiredPath]) { - if (!wasParsed[requiredPath]) { - filteredEntries.push(requiredPath); - } - } - - return filteredEntries; - }, []); -} diff --git a/packages/kbn-babel-code-parser/src/strategies.test.js b/packages/kbn-babel-code-parser/src/strategies.test.js deleted file mode 100644 index e61c784cdcd54..0000000000000 --- a/packages/kbn-babel-code-parser/src/strategies.test.js +++ /dev/null @@ -1,108 +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 { readFile } from 'fs'; -import { canRequire } from './can_require'; -import { parseSingleFile } from './code_parser'; -import { _calculateTopLevelDependency, dependenciesParseStrategy } from './strategies'; - -jest.mock('./can_require', () => ({ - canRequire: jest.fn(), -})); - -jest.mock('fs', () => ({ - readFile: jest.fn(), -})); - -const mockCwd = '/tmp/project/dir/'; - -describe('Code Parser Strategies', () => { - it('should calculate the top level dependencies correctly', () => { - const plainDep = 'dep1/file'; - const foldedDep = '@kbn/es/file'; - const otherFoldedDep = '@kbn/es'; - - expect(_calculateTopLevelDependency(plainDep)).toEqual('dep1'); - expect(_calculateTopLevelDependency(foldedDep)).toEqual('@kbn/es'); - expect(_calculateTopLevelDependency(otherFoldedDep)).toEqual('@kbn/es'); - }); - - it('should exclude native modules', async () => { - readFile.mockImplementationOnce((path, options, cb) => { - cb(null, `require('fs')`); - }); - - const results = []; - await dependenciesParseStrategy(mockCwd, parseSingleFile, 'dep1/file.js', {}, results); - - expect(results.length).toBe(0); - }); - - it('should return a dep from_modules', async () => { - readFile.mockImplementationOnce((path, options, cb) => { - cb(null, `require('dep_from_node_modules')`); - }); - - canRequire.mockImplementation((entry, cwd) => { - if (entry === `${cwd}dep1/dep_from_node_modules`) { - return false; - } - - if (entry === 'dep_from_node_modules') { - return `${mockCwd}node_modules/dep_from_node_modules/index.js`; - } - }); - - const results = await dependenciesParseStrategy( - mockCwd, - parseSingleFile, - 'dep1/file.js', - {}, - {} - ); - expect(results[0]).toBe(`${mockCwd}node_modules/dep_from_node_modules/index.js`); - }); - - it('should return a relative dep file', async () => { - readFile.mockImplementationOnce((path, options, cb) => { - cb(null, `require('./relative_dep')`); - }); - - canRequire.mockImplementation((entry) => { - if (entry === `${mockCwd}dep1/relative_dep`) { - return `${entry}/index.js`; - } - - return false; - }); - - const results = await dependenciesParseStrategy( - mockCwd, - parseSingleFile, - 'dep1/file.js', - {}, - {} - ); - expect(results[0]).toBe(`${mockCwd}dep1/relative_dep/index.js`); - }); - - afterAll(() => { - jest.clearAllMocks(); - }); -}); diff --git a/packages/kbn-babel-code-parser/src/visitors.js b/packages/kbn-babel-code-parser/src/visitors.js deleted file mode 100644 index b159848d424fa..0000000000000 --- a/packages/kbn-babel-code-parser/src/visitors.js +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -import { matches } from 'lodash'; - -/* @notice - * - * This product has relied on ASTExplorer that is licensed under MIT. - */ -export function dependenciesVisitorsGenerator(dependenciesAcc) { - return (() => { - // This was built with help on an ast explorer and some ESTree docs - // like the babel parser ast spec and the main docs for the Esprima - // which is a complete and useful docs for the ESTree spec. - // - // https://astexplorer.net - // https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md - // https://esprima.readthedocs.io/en/latest/syntax-tree-format.html - // https://github.com/estree/estree - return { - // Visitors to traverse and found dependencies - // raw values on require + require.resolve - CallExpression: ({ node }) => { - // AST check for require expressions - const isRequire = (node) => { - return matches({ - callee: { - type: 'Identifier', - name: 'require', - }, - })(node); - }; - - // AST check for require.resolve expressions - const isRequireResolve = (node) => { - return matches({ - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'require', - }, - property: { - type: 'Identifier', - name: 'resolve', - }, - }, - })(node); - }; - - // Get string values inside the expressions - // whether they are require or require.resolve - if (isRequire(node) || isRequireResolve(node)) { - const nodeArguments = node.arguments; - const reqArg = Array.isArray(nodeArguments) ? nodeArguments.shift() : null; - - if (!reqArg) { - return; - } - - if (reqArg.type === 'StringLiteral') { - dependenciesAcc.push(reqArg.value); - } - } - }, - // Visitors to traverse and found dependencies - // raw values on import - ImportDeclaration: ({ node }) => { - // AST check for supported import expressions - const isImport = (node) => { - return matches({ - type: 'ImportDeclaration', - source: { - type: 'StringLiteral', - }, - })(node); - }; - - // Get string values from import expressions - if (isImport(node)) { - const importSource = node.source; - dependenciesAcc.push(importSource.value); - } - }, - // Visitors to traverse and found dependencies - // raw values on export from - ExportNamedDeclaration: ({ node }) => { - // AST check for supported export from expressions - const isExportFrom = (node) => { - return matches({ - type: 'ExportNamedDeclaration', - source: { - type: 'StringLiteral', - }, - })(node); - }; - - // Get string values from export from expressions - if (isExportFrom(node)) { - const exportFromSource = node.source; - dependenciesAcc.push(exportFromSource.value); - } - }, - // Visitors to traverse and found dependencies - // raw values on export * from - ExportAllDeclaration: ({ node }) => { - // AST check for supported export * from expressions - const isExportAllFrom = (node) => { - return matches({ - type: 'ExportAllDeclaration', - source: { - type: 'StringLiteral', - }, - })(node); - }; - - // Get string values from export * from expressions - if (isExportAllFrom(node)) { - const exportAllFromSource = node.source; - dependenciesAcc.push(exportAllFromSource.value); - } - }, - }; - })(); -} diff --git a/packages/kbn-babel-code-parser/src/visitors.test.js b/packages/kbn-babel-code-parser/src/visitors.test.js deleted file mode 100644 index d2704fa9dfb72..0000000000000 --- a/packages/kbn-babel-code-parser/src/visitors.test.js +++ /dev/null @@ -1,68 +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 parser from '@babel/parser'; -import traverse from '@babel/traverse'; -import { dependenciesVisitorsGenerator } from './visitors'; - -const visitorsApplier = (code) => { - const result = []; - traverse( - parser.parse(code, { - sourceType: 'unambiguous', - plugins: ['exportDefaultFrom'], - }), - dependenciesVisitorsGenerator(result) - ); - return result; -}; - -describe('Code Parser Visitors', () => { - it('should get values from require', () => { - const rawCode = `/*foo*/require('dep1'); const bar = 1;`; - const foundDeps = visitorsApplier(rawCode); - expect(foundDeps[0] === 'dep1'); - }); - - it('should get values from require.resolve', () => { - const rawCode = `/*foo*/require.resolve('dep2'); const bar = 1;`; - const foundDeps = visitorsApplier(rawCode); - expect(foundDeps[0] === 'dep2'); - }); - - it('should get values from import', () => { - const rawCode = `/*foo*/import dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`; - const foundDeps = visitorsApplier(rawCode); - expect(foundDeps[0] === 'dep1'); - expect(foundDeps[1] === 'dep2'); - }); - - it('should get values from export from', () => { - const rawCode = `/*foo*/export dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`; - const foundDeps = visitorsApplier(rawCode); - expect(foundDeps[0] === 'dep1'); - }); - - it('should get values from export * from', () => { - const rawCode = `/*foo*/export * from 'dep1'; export dep2 from 'dep2';const bar = 1;`; - const foundDeps = visitorsApplier(rawCode); - expect(foundDeps[0] === 'dep1'); - expect(foundDeps[1] === 'dep2'); - }); -}); diff --git a/packages/kbn-babel-code-parser/yarn.lock b/packages/kbn-babel-code-parser/yarn.lock deleted file mode 120000 index 3f82ebc9cdbae..0000000000000 --- a/packages/kbn-babel-code-parser/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/src/dev/build/README.md b/src/dev/build/README.md index ed8750f6fee56..460ab01794334 100644 --- a/src/dev/build/README.md +++ b/src/dev/build/README.md @@ -39,14 +39,3 @@ The majority of this logic is extracted from the grunt build that has existed fo [lib/build.js]: ./lib/build.js [build_distributables.js]: ./build_distributables.js [../tooling_log/tooling_log.js]: ../tooling_log/tooling_log.js - -# Client Node Modules Cleaning - -We have introduced in our bundle a webpack dll for the client vendor modules in order to improve -the optimization time both in dev and in production. As for those modules we already have the -code into the vendors_${chunk_number}.bundle.dll.js we have decided to delete those bundled modules from the -distributable node_modules folder. However, in order to accomplish this, we need to exclude -every node_module used in the server side code. This logic is performed -under `nodejs_modules/clean_client_modules_on_dll_task.js`. In case we need to add any new cli -or any other piece of server code other than `x-pack` or `core_plugins` we'll need -to update the globs present on `clean_client_modules_on_dll_task.js` accordingly. diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 22a348b78dc0a..39a32fff891c2 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -22,7 +22,6 @@ import { getConfig, createRunner } from './lib'; import { BuildKibanaPlatformPluginsTask, BuildPackagesTask, - CleanClientModulesOnDLLTask, CleanEmptyFoldersTask, CleanExtraBinScriptsTask, CleanExtraFilesFromModulesTask, @@ -127,7 +126,6 @@ export async function buildDistributables(options) { await run(TranspileScssTask); await run(BuildKibanaPlatformPluginsTask); await run(OptimizeBuildTask); - await run(CleanClientModulesOnDLLTask); await run(CleanTypescriptTask); await run(CleanExtraFilesFromModulesTask); await run(CleanEmptyFoldersTask); diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js index d96e745c10776..0a3a67313d6a4 100644 --- a/src/dev/build/tasks/index.js +++ b/src/dev/build/tasks/index.js @@ -30,7 +30,6 @@ export * from './create_readme_task'; export * from './install_chromium'; export * from './install_dependencies_task'; export * from './license_file_task'; -export * from './nodejs_modules'; export * from './nodejs'; export * from './notice_file_task'; export * from './optimize_task'; diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js deleted file mode 100644 index 05bfd3ca31a65..0000000000000 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ /dev/null @@ -1,126 +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 { - getDllEntries, - cleanDllModuleFromEntryPath, - writeEmptyFileForDllEntry, -} from './webpack_dll'; -import { getDependencies } from './get_dependencies'; -import globby from 'globby'; -import normalizePosixPath from 'normalize-path'; - -export const CleanClientModulesOnDLLTask = { - description: 'Cleaning client node_modules bundled into the DLL', - - async run(config, log, build) { - const baseDir = normalizePosixPath(build.resolvePath('.')); - const kbnPkg = config.getKibanaPkg(); - const kbnPkgDependencies = (kbnPkg && kbnPkg.dependencies) || {}; - const kbnWebpackLoaders = Object.keys(kbnPkgDependencies).filter( - (dep) => !!dep.includes('-loader') - ); - - // Define the entry points for the server code in order to - // start here later looking for the server side dependencies - const mainCodeEntries = [ - `${baseDir}/src/cli`, - `${baseDir}/src/cli_keystore`, - `${baseDir}/src/cli_plugin`, - `${baseDir}/x-pack`, - ...kbnWebpackLoaders.map((loader) => `${baseDir}/node_modules/${loader}`), - ]; - const discoveredLegacyCorePluginEntries = await globby([ - `${baseDir}/src/legacy/core_plugins/*/index.js`, - `!${baseDir}/src/legacy/core_plugins/**/public`, - ]); - const discoveredPluginEntries = await globby([ - `${baseDir}/src/plugins/*/server/index.js`, - // Small exception to load dynamically discovered functions for timelion plugin - `${baseDir}/src/plugins/vis_type_timelion/server/*_functions/**/*.js`, - `!${baseDir}/src/plugins/**/public`, - ]); - const discoveredNewPlatformXpackPlugins = await globby([ - `${baseDir}/x-pack/plugins/*/server/index.js`, - `!${baseDir}/x-pack/plugins/**/public`, - ]); - - // Compose all the needed entries - const serverEntries = [ - ...mainCodeEntries, - ...discoveredLegacyCorePluginEntries, - ...discoveredPluginEntries, - ...discoveredNewPlatformXpackPlugins, - ]; - - // Get the dependencies found searching through the server - // side code entries that were provided - const serverDependencies = await getDependencies(baseDir, serverEntries); - - // This fulfill a particular exceptional case where - // we need to keep loading a file from a node_module - // only used in the front-end like we do when using the file-loader - // in https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js - // - // manual list of exception modules - const manualExceptionModules = ['mapbox-gl']; - - // consider the top modules as exceptions as the entry points - // to look for other exceptions dependent on that one - const manualExceptionEntries = [ - ...manualExceptionModules.map((module) => `${baseDir}/node_modules/${module}`), - ]; - - // dependencies for declared exception modules - const manualExceptionModulesDependencies = await getDependencies(baseDir, [ - ...manualExceptionEntries, - ]); - - // final list of manual exceptions to add - const manualExceptions = [...manualExceptionModules, ...manualExceptionModulesDependencies]; - - // Consider this as our whiteList for the modules we can't delete - const whiteListedModules = [...serverDependencies, ...kbnWebpackLoaders, ...manualExceptions]; - - // Resolve the client vendors dll manifest paths - // excluding the runtime one - const dllManifestPaths = await globby([ - `${baseDir}/built_assets/dlls/vendors_*.manifest.dll.json`, - `!${baseDir}/built_assets/dlls/vendors_runtime.manifest.dll.json`, - ]); - - // Get dll entries filtering out the ones - // from any whitelisted module - const dllEntries = await getDllEntries(dllManifestPaths, whiteListedModules, baseDir); - - for (const relativeEntryPath of dllEntries) { - const entryPath = `${baseDir}/${relativeEntryPath}`; - - if (entryPath.endsWith('package.json')) { - continue; - } - - // Clean a module included into the dll - // and then write a blank file for each - // entry file present into the dll - await cleanDllModuleFromEntryPath(log, entryPath); - await writeEmptyFileForDllEntry(entryPath); - } - }, -}; diff --git a/src/dev/build/tasks/nodejs_modules/get_dependencies.js b/src/dev/build/tasks/nodejs_modules/get_dependencies.js deleted file mode 100644 index 5a39448f57d14..0000000000000 --- a/src/dev/build/tasks/nodejs_modules/get_dependencies.js +++ /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 { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser'; - -export async function getDependencies(cwd, entries) { - // Return the dependencies retrieve from the - // provided code entries (sanitized) and - // parseStrategy (dependencies one) - return Object.keys(await parseEntries(cwd, entries, dependenciesParseStrategy, {})); -} diff --git a/src/dev/build/tasks/nodejs_modules/index.js b/src/dev/build/tasks/nodejs_modules/index.js deleted file mode 100644 index 1c5392662ad13..0000000000000 --- a/src/dev/build/tasks/nodejs_modules/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. - */ - -export { CleanClientModulesOnDLLTask } from './clean_client_modules_on_dll_task'; diff --git a/src/dev/build/tasks/nodejs_modules/webpack_dll.js b/src/dev/build/tasks/nodejs_modules/webpack_dll.js deleted file mode 100644 index 8de5e582c3d36..0000000000000 --- a/src/dev/build/tasks/nodejs_modules/webpack_dll.js +++ /dev/null @@ -1,131 +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 { deleteAll, isFileAccessible, read, write } from '../../lib'; -import { dirname, relative, resolve } from 'path'; -import pkgUp from 'pkg-up'; -import globby from 'globby'; -import normalizePosixPath from 'normalize-path'; - -function checkDllEntryAccess(entry, baseDir = '') { - const resolvedPath = baseDir ? resolve(baseDir, entry) : entry; - return isFileAccessible(resolvedPath); -} - -export async function getDllEntries(manifestPaths, whiteListedModules, baseDir = '') { - // Read and parse all manifests - const manifests = await Promise.all( - manifestPaths.map(async (manifestPath) => JSON.parse(await read(manifestPath))) - ); - - // Process and group modules from all manifests - const manifestsModules = manifests.flatMap((manifest, idx) => { - if (!manifest || !manifest.content) { - // It should fails because if we don't have the manifest file - // or it is malformed something wrong is happening and we - // should stop - throw new Error(`The following dll manifest doesn't exists: ${manifestPaths[idx]}`); - } - - const modules = Object.keys(manifest.content); - if (!modules.length) { - // It should fails because if we don't have any - // module inside the client vendors dll something - // wrong is happening and we should stop too - throw new Error( - `The following dll manifest is reporting an empty dll: ${manifestPaths[idx]}` - ); - } - - return modules; - }); - - // Only includes modules who are not in the white list of modules - // and that are node_modules - return manifestsModules.filter((entry) => { - const isWhiteListed = whiteListedModules.some((nonEntry) => - normalizePosixPath(entry).includes(`node_modules/${nonEntry}`) - ); - const isNodeModule = entry.includes('node_modules'); - - // NOTE: when using dynamic imports on webpack the entry paths could be created - // with special context module (ex: lazy recursive) values over directories that are not real files - // and only exists in runtime, so we need to check if the entry is a real file. - // We found that problem through the issue https://github.com/elastic/kibana/issues/38481 - // - // More info: - // https://github.com/webpack/webpack/blob/master/examples/code-splitting-harmony/README.md - // https://webpack.js.org/guides/dependency-management/#require-with-expression - const isAccessible = checkDllEntryAccess(entry, baseDir); - - return !isWhiteListed && isNodeModule && isAccessible; - }); -} - -export async function cleanDllModuleFromEntryPath(logger, entryPath) { - const modulePkgPath = await pkgUp(entryPath); - const modulePkg = JSON.parse(await read(modulePkgPath)); - const moduleDir = dirname(modulePkgPath); - const normalizedModuleDir = normalizePosixPath(moduleDir); - - // Cancel the cleanup for this module as it - // was already done. - if (modulePkg.cleaned) { - return; - } - - // Clear dependencies from dll module package.json - if (modulePkg.dependencies) { - modulePkg.dependencies = {}; - } - - // Clear devDependencies from dll module package.json - if (modulePkg.devDependencies) { - modulePkg.devDependencies = {}; - } - - // Delete module contents. It will delete everything - // excepts package.json, images and css - // - // NOTE: We can't use cwd option with globby - // until the following issue gets closed - // https://github.com/sindresorhus/globby/issues/87 - const filesToDelete = await globby([ - `${normalizedModuleDir}/**`, - `!${normalizedModuleDir}/**/*.+(css)`, - `!${normalizedModuleDir}/**/*.+(gif|ico|jpeg|jpg|tiff|tif|svg|png|webp)`, - ]); - - await deleteAll( - filesToDelete.filter((path) => { - const relativePath = relative(moduleDir, path); - return !relativePath.endsWith('package.json') || relativePath.includes('node_modules'); - }) - ); - - // Mark this module as cleaned - modulePkg.cleaned = true; - - // Rewrite modified package.json - await write(modulePkgPath, JSON.stringify(modulePkg, null, 2)); -} - -export async function writeEmptyFileForDllEntry(entryPath) { - await write(entryPath, ''); -} diff --git a/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js b/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js deleted file mode 100644 index ce305169a777b..0000000000000 --- a/src/dev/build/tasks/nodejs_modules/webpack_dll.test.js +++ /dev/null @@ -1,123 +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 { isFileAccessible, read } from '../../lib'; -import { getDllEntries } from './webpack_dll'; - -jest.mock('../../lib', () => ({ - read: jest.fn(), - isFileAccessible: jest.fn(), -})); - -const manifestContentMock = JSON.stringify({ - name: 'vendors', - content: { - '/mock/node_modules/dep1': {}, - '/mock/node_modules/dep2': {}, - '/mock/node_modules/dep3': {}, - '/mock/tmp/dep2': {}, - }, -}); - -const emptyManifestContentMock = JSON.stringify({ - name: 'vendors', - content: {}, -}); - -const noManifestMock = JSON.stringify(null); - -const noContentFieldManifestMock = JSON.stringify({ - name: 'vendors', -}); - -describe('Webpack DLL Build Tasks Utils', () => { - it('should get dll entries correctly', async () => { - read.mockImplementationOnce(async () => manifestContentMock); - - isFileAccessible.mockImplementation(() => true); - - const mockManifestPath = ['/mock/mock_dll_manifest.json']; - const mockModulesWhitelist = ['dep1']; - const dllEntries = await getDllEntries(mockManifestPath, mockModulesWhitelist); - - expect(dllEntries).toEqual( - expect.arrayContaining(['/mock/node_modules/dep2', '/mock/node_modules/dep3']) - ); - }); - - it('should only include accessible files', async () => { - read.mockImplementationOnce(async () => manifestContentMock); - - isFileAccessible.mockImplementation(() => false); - - const mockManifestPath = ['/mock/mock_dll_manifest.json']; - const mockModulesWhitelist = ['dep1']; - const dllEntries = await getDllEntries(mockManifestPath, mockModulesWhitelist); - - isFileAccessible.mockRestore(); - - expect(dllEntries.length).toEqual(0); - }); - - it('should throw an error for no manifest file', async () => { - read.mockImplementationOnce(async () => noManifestMock); - - const mockManifestPath = ['/mock/mock_dll_manifest.json']; - - try { - await getDllEntries(mockManifestPath, []); - } catch (error) { - expect(error.message).toEqual( - `The following dll manifest doesn't exists: /mock/mock_dll_manifest.json` - ); - } - }); - - it('should throw an error for no manifest content field', async () => { - read.mockImplementation(async () => noContentFieldManifestMock); - - const mockManifestPath = ['/mock/mock_dll_manifest.json']; - - try { - await getDllEntries(mockManifestPath, []); - } catch (error) { - expect(error.message).toEqual( - `The following dll manifest doesn't exists: /mock/mock_dll_manifest.json` - ); - } - }); - - it('should throw an error for manifest file without any content', async () => { - read.mockImplementation(async () => emptyManifestContentMock); - - const mockManifestPath = ['/mock/mock_dll_manifest.json']; - - try { - await getDllEntries(mockManifestPath, []); - } catch (error) { - expect(error.message).toEqual( - `The following dll manifest is reporting an empty dll: /mock/mock_dll_manifest.json` - ); - } - }); - - afterAll(() => { - jest.clearAllMocks(); - }); -}); diff --git a/src/dev/build/tasks/optimize_task.js b/src/dev/build/tasks/optimize_task.js index 4034092273239..16a7537b8ac9e 100644 --- a/src/dev/build/tasks/optimize_task.js +++ b/src/dev/build/tasks/optimize_task.js @@ -41,7 +41,6 @@ export const OptimizeBuildTask = { await exec(log, kibanaScript, kibanaArgs, { cwd: build.resolvePath('.'), env: { - FORCE_DLL_CREATION: 'true', KBN_CACHE_LOADER_WRITABLE: 'true', NODE_OPTIONS: '--max-old-space-size=4096', }, diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index fb74bed0f26f4..37bbcce72e497 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -47,10 +47,11 @@ export async function generateNoticeFromSource({ productName, directory, log }: cwd: directory, nodir: true, ignore: [ - '{node_modules,build,target,dist,data,built_assets}/**', - 'packages/*/{node_modules,build,target,dist}/**', - 'x-pack/{node_modules,build,target,dist,data}/**', - 'x-pack/packages/*/{node_modules,build,target,dist}/**', + '{node_modules,build,dist,data,built_assets}/**', + 'packages/*/{node_modules,build,dist}/**', + 'x-pack/{node_modules,build,dist,data}/**', + 'x-pack/packages/*/{node_modules,build,dist}/**', + '**/target/**', ], }; diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index 431c161585fe0..da7c1c4f4527e 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -108,6 +108,13 @@ export default (kibana) => { resolve: { extensions: ['.karma_mock.js', '.karma_mock.tsx', '.karma_mock.ts'], }, + node: { + fs: 'empty', + child_process: 'empty', + dns: 'empty', + net: 'empty', + tls: 'empty', + }, }, webpackConfig ); diff --git a/src/legacy/server/logging/log_format_string.js b/src/legacy/server/logging/log_format_string.js index 3c18aab2e3d09..cbbf71dd894ac 100644 --- a/src/legacy/server/logging/log_format_string.js +++ b/src/legacy/server/logging/log_format_string.js @@ -41,8 +41,6 @@ const typeColors = { optmzr: 'white', manager: 'green', optimize: 'magentaBright', - 'optimize:dynamic_dll_plugin': 'magentaBright', - 'optimize:watch_cache': 'magentaBright', listening: 'magentaBright', scss: 'magentaBright', }; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 168dddf0253d9..7788aeaee72e5 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -24,7 +24,6 @@ import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; -import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; /** * @typedef {import('../../server/kbn_server').default} KbnServer @@ -106,17 +105,8 @@ export function uiRenderMixin(kbnServer, server, config) { const basePath = config.get('server.basePath'); const regularBundlePath = `${basePath}/${buildHash}/bundles`; - const dllBundlePath = `${basePath}/${buildHash}/built_assets/dlls`; - - const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map( - (chunk) => `${dllBundlePath}/vendors${chunk}.style.dll.css` - ); - const dllJsChunks = DllCompiler.getRawDllConfig().chunks.map( - (chunk) => `${dllBundlePath}/vendors${chunk}.bundle.dll.js` - ); const styleSheetPaths = [ - ...(isCore ? [] : dllStyleChunks), `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, ...(darkMode ? [ @@ -173,7 +163,6 @@ export function uiRenderMixin(kbnServer, server, config) { (filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` ), `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - ...(isCore ? [] : [`${dllBundlePath}/vendors_runtime.bundle.dll.js`, ...dllJsChunks]), `${regularBundlePath}/core/core.entry.js`, ...kpPluginBundlePaths, diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 55752db55e28a..41628a2264193 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -30,8 +30,6 @@ import webpackMerge from 'webpack-merge'; import WrapperPlugin from 'wrapper-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { DynamicDllPlugin } from './dynamic_dll_plugin'; - import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; @@ -286,12 +284,6 @@ export default class BaseOptimizer { }, plugins: [ - new DynamicDllPlugin({ - uiBundles: this.uiBundles, - threadLoaderPoolConfig: this.getThreadLoaderPoolConfig(), - logWithMetadata: this.logWithMetadata, - }), - new MiniCssExtractPlugin({ filename: '[name].style.css', }), diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 902fa59b20569..5b42b658300fe 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -56,7 +56,6 @@ describe('optimizer/bundle route', () => { function createServer(options = {}) { const { regularBundlesPath = outputFixture, - dllBundlesPath = outputFixture, basePublicPath = '', builtCssPath = outputFixture, npUiPluginPublicDirs = [], @@ -70,7 +69,6 @@ describe('optimizer/bundle route', () => { server.route( createBundlesRoute({ regularBundlesPath, - dllBundlesPath, basePublicPath, builtCssPath, npUiPluginPublicDirs, @@ -89,28 +87,24 @@ describe('optimizer/bundle route', () => { expect(() => { createBundlesRoute({ regularBundlesPath: null, - dllBundlesPath: '/absolute/path', basePublicPath: '', }); }).to.throwError(/absolute path/); expect(() => { createBundlesRoute({ regularBundlesPath: './relative', - dllBundlesPath: '/absolute/path', basePublicPath: '', }); }).to.throwError(/absolute path/); expect(() => { createBundlesRoute({ regularBundlesPath: 1234, - dllBundlesPath: '/absolute/path', basePublicPath: '', }); }).to.throwError(/absolute path/); expect(() => { createBundlesRoute({ regularBundlesPath: '/absolute/path', - dllBundlesPath: '/absolute/path', basePublicPath: '', }); }).to.not.throwError(); @@ -119,42 +113,36 @@ describe('optimizer/bundle route', () => { expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: 123, }); }).to.throwError(/string/); expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: {}, }); }).to.throwError(/string/); expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: '/a/', }); }).to.throwError(/start and not end with a \//); expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: 'a/', }); }).to.throwError(/start and not end with a \//); expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: '/a', }); }).to.not.throwError(); expect(() => { createBundlesRoute({ regularBundlesPath: '/bundles', - dllBundlesPath: '/absolute/path', basePublicPath: '', }); }).to.not.throwError(); diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts index e9cfba0130d95..8c1092efed252 100644 --- a/src/optimize/bundles_route/bundles_route.ts +++ b/src/optimize/bundles_route/bundles_route.ts @@ -28,22 +28,19 @@ import { assertIsNpUiPluginPublicDirs, NpUiPluginPublicDirs } from '../np_ui_plu import { fromRoot } from '../../core/server/utils'; /** - * Creates the routes that serves files from `bundlesPath` or from - * `dllBundlesPath` (if they are dll bundle's related files). If the + * Creates the routes that serves files from `bundlesPath`. If the * file is js or css then it is searched for instances of * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. * * @param {Object} options * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins * @property {string} options.regularBundlesPath - * @property {string} options.dllBundlesPath * @property {string} options.basePublicPath * * @return Array.of({Hapi.Route}) */ export function createBundlesRoute({ regularBundlesPath, - dllBundlesPath, basePublicPath, builtCssPath, npUiPluginPublicDirs = [], @@ -51,7 +48,6 @@ export function createBundlesRoute({ isDist = false, }: { regularBundlesPath: string; - dllBundlesPath: string; basePublicPath: string; builtCssPath: string; npUiPluginPublicDirs?: NpUiPluginPublicDirs; @@ -70,12 +66,6 @@ export function createBundlesRoute({ ); } - if (typeof dllBundlesPath !== 'string' || !isAbsolute(dllBundlesPath)) { - throw new TypeError( - 'dllBundlesPath must be an absolute path to the directory containing the dll bundles' - ); - } - if (typeof basePublicPath !== 'string') { throw new TypeError('basePublicPath must be a string'); } @@ -118,13 +108,6 @@ export function createBundlesRoute({ fileHashCache, isDist, }), - buildRouteForBundles({ - publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`, - routePath: `/${buildHash}/built_assets/dlls/`, - bundlesPath: dllBundlesPath, - fileHashCache, - isDist, - }), buildRouteForBundles({ publicPath: `${basePublicPath}/`, routePath: `/${buildHash}/built_assets/css/`, diff --git a/src/optimize/bundles_route/proxy_bundles_route.ts b/src/optimize/bundles_route/proxy_bundles_route.ts index 1d189054324a1..108d253d45202 100644 --- a/src/optimize/bundles_route/proxy_bundles_route.ts +++ b/src/optimize/bundles_route/proxy_bundles_route.ts @@ -28,7 +28,6 @@ export function createProxyBundlesRoute({ }) { return [ buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port), - buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port), buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port), ]; } diff --git a/src/optimize/dynamic_dll_plugin/dll_allowed_modules.js b/src/optimize/dynamic_dll_plugin/dll_allowed_modules.js deleted file mode 100644 index 4314cebd3b7e9..0000000000000 --- a/src/optimize/dynamic_dll_plugin/dll_allowed_modules.js +++ /dev/null @@ -1,36 +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 path from 'path'; - -export function notInNodeModules(checkPath) { - return !checkPath.includes(`${path.sep}node_modules${path.sep}`); -} - -export function notInNodeModulesOrWebpackShims(checkPath) { - return notInNodeModules(checkPath) && !checkPath.includes(`${path.sep}webpackShims${path.sep}`); -} - -export function inPluginNodeModules(checkPath) { - return checkPath.match(/[\/\\]plugins.*[\/\\]node_modules/); -} - -export function inDllPluginPublic(checkPath) { - return checkPath.includes(`${path.sep}dynamic_dll_plugin${path.sep}public${path.sep}`); -} diff --git a/src/optimize/dynamic_dll_plugin/dll_compiler.js b/src/optimize/dynamic_dll_plugin/dll_compiler.js deleted file mode 100644 index 9ab21ee0e9076..0000000000000 --- a/src/optimize/dynamic_dll_plugin/dll_compiler.js +++ /dev/null @@ -1,366 +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 { configModel } from './dll_config_model'; -import { - notInNodeModulesOrWebpackShims, - notInNodeModules, - inDllPluginPublic, -} from './dll_allowed_modules'; -import { - dllEntryFileContentArrayToString, - dllEntryFileContentStringToArray, - dllMergeAllEntryFilesContent, -} from './dll_entry_template'; -import { fromRoot } from '../../core/server/utils'; -import { PUBLIC_PATH_PLACEHOLDER } from '../public_path_placeholder'; -import fs from 'fs'; -import webpack from 'webpack'; -import { promisify } from 'util'; -import path from 'path'; -import del from 'del'; -import { chunk } from 'lodash'; -import seedrandom from 'seedrandom'; - -const readFileAsync = promisify(fs.readFile); -const mkdirAsync = promisify(fs.mkdir); -const accessAsync = promisify(fs.access); -const writeFileAsync = promisify(fs.writeFile); - -export class DllCompiler { - static getRawDllConfig( - uiBundles = {}, - babelLoaderCacheDir = '', - threadLoaderPoolConfig = {}, - chunks = Array.from(Array(4).keys()).map((chunkN) => `_${chunkN}`) - ) { - return { - uiBundles, - babelLoaderCacheDir, - threadLoaderPoolConfig, - chunks, - context: fromRoot('.'), - entryName: 'vendors', - dllName: '[name]', - manifestName: '[name]', - styleName: '[name]', - entryExt: '.entry.dll.js', - dllExt: '.bundle.dll.js', - manifestExt: '.manifest.dll.json', - styleExt: '.style.dll.css', - outputPath: fromRoot('built_assets/dlls'), - publicPath: PUBLIC_PATH_PLACEHOLDER, - }; - } - - constructor(uiBundles, threadLoaderPoolConfig, logWithMetadata) { - this.rawDllConfig = DllCompiler.getRawDllConfig( - uiBundles, - uiBundles.getCacheDirectory('babel'), - threadLoaderPoolConfig - ); - this.logWithMetadata = logWithMetadata || (() => null); - } - - async init() { - await this.ensureEntryFilesExists(); - await this.ensureManifestFilesExists(); - await this.ensureOutputPathExists(); - } - - seededShuffle(array) { - // Implementation based on https://github.com/TimothyGu/knuth-shuffle-seeded/blob/gh-pages/index.js#L46 - let currentIndex; - let temporaryValue; - let randomIndex; - const rand = seedrandom('predictable', { global: false }); - - if (array.constructor !== Array) throw new Error('Input is not an array'); - currentIndex = array.length; - - // While there remain elements to shuffle... - while (0 !== currentIndex) { - // Pick a remaining element... - randomIndex = Math.floor(rand() * currentIndex--); - - // And swap it with the current element. - temporaryValue = array[currentIndex]; - array[currentIndex] = array[randomIndex]; - array[randomIndex] = temporaryValue; - } - - return array; - } - - async upsertEntryFiles(content) { - const arrayContent = this.seededShuffle(dllEntryFileContentStringToArray(content)); - const chunks = chunk( - arrayContent, - Math.ceil(arrayContent.length / this.rawDllConfig.chunks.length) - ); - const entryPaths = this.getEntryPaths(); - - await Promise.all( - entryPaths.map( - async (entryPath, idx) => - await this.upsertFile(entryPath, dllEntryFileContentArrayToString(chunks[idx])) - ) - ); - } - - async upsertFile(filePath, content = '') { - await this.ensurePathExists(filePath); - await writeFileAsync(filePath, content, 'utf8'); - } - - getDllPaths() { - return this.rawDllConfig.chunks.map((chunk) => - this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.dllExt}`) - ); - } - - getEntryPaths() { - return this.rawDllConfig.chunks.map((chunk) => - this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.entryExt}`) - ); - } - - getManifestPaths() { - return this.rawDllConfig.chunks.map((chunk) => - this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.manifestExt}`) - ); - } - - getStylePaths() { - return this.rawDllConfig.chunks.map((chunk) => - this.resolvePath(`${this.rawDllConfig.entryName}${chunk}${this.rawDllConfig.styleExt}`) - ); - } - - async ensureEntryFilesExists() { - const entryPaths = this.getEntryPaths(); - - await Promise.all(entryPaths.map(async (entryPath) => await this.ensureFileExists(entryPath))); - } - - async ensureManifestFilesExists() { - const manifestPaths = this.getManifestPaths(); - - await Promise.all( - manifestPaths.map( - async (manifestPath, idx) => - await this.ensureFileExists( - manifestPath, - JSON.stringify({ - name: `${this.rawDllConfig.entryName}${this.rawDllConfig.chunks[idx]}`, - content: {}, - }) - ) - ) - ); - } - - async ensureStyleFileExists() { - const stylePaths = this.getStylePaths(); - - await Promise.all(stylePaths.map(async (stylePath) => await this.ensureFileExists(stylePath))); - } - - async ensureFileExists(filePath, content) { - const exists = await this.ensurePathExists(filePath); - - if (!exists) { - await this.upsertFile(filePath, content); - } - } - - async ensurePathExists(filePath) { - try { - await accessAsync(filePath); - } catch (e) { - await mkdirAsync(path.dirname(filePath), { recursive: true }); - return false; - } - - return true; - } - - async ensureOutputPathExists() { - await this.ensurePathExists(this.rawDllConfig.outputPath); - } - - dllsExistsSync() { - const dllPaths = this.getDllPaths(); - - return dllPaths.every((dllPath) => this.existsSync(dllPath)); - } - - existsSync(filePath) { - return fs.existsSync(filePath); - } - - resolvePath() { - return path.resolve(this.rawDllConfig.outputPath, ...arguments); - } - - async readEntryFiles() { - const entryPaths = this.getEntryPaths(); - - const entryFilesContent = await Promise.all( - entryPaths.map(async (entryPath) => await this.readFile(entryPath)) - ); - - // merge all the module contents from entry files again into - // sorted single one - return dllMergeAllEntryFilesContent(entryFilesContent); - } - - async readFile(filePath, content) { - await this.ensureFileExists(filePath, content); - return await readFileAsync(filePath, 'utf8'); - } - - async run(dllEntries) { - const dllConfig = this.dllConfigGenerator(this.rawDllConfig); - await this.upsertEntryFiles(dllEntries); - - try { - this.logWithMetadata( - ['info', 'optimize:dynamic_dll_plugin'], - 'Client vendors dll compilation started' - ); - - await this.runWebpack(dllConfig()); - - this.logWithMetadata( - ['info', 'optimize:dynamic_dll_plugin'], - `Client vendors dll compilation finished with success` - ); - } catch (e) { - this.logWithMetadata( - ['fatal', 'optimize:dynamic_dll_plugin'], - `Client vendors dll compilation failed` - ); - - // Still throw the original error has here we just want - // log the fail message - throw e; - } - - // Style dll file isn't always created but we are - // expecting it to exist always as we are referencing - // it from the bootstrap template - // - // NOTE: We should review the way we deal with the css extraction - // in ours webpack builds. The industry standard is about to - // only extract css for production but we are extracting it - // in every single compilation. - await this.ensureStyleFileExists(); - } - - dllConfigGenerator(dllConfig) { - return configModel.bind(this, dllConfig); - } - - async runWebpack(config) { - return new Promise((resolve, reject) => { - webpack(config, async (err, stats) => { - // If a critical error occurs or we have - // errors in the stats compilation, - // reject the promise and logs the errors - const webpackErrors = - err || - (stats.hasErrors() && - stats.toString({ - all: false, - colors: true, - errors: true, - errorDetails: true, - moduleTrace: true, - })); - - if (webpackErrors) { - // Reject with webpack fatal errors - return reject(webpackErrors); - } - - // Identify if we have not allowed modules - // bundled inside the dll bundle - const notAllowedModules = []; - - stats.compilation.modules.forEach((module) => { - // ignore if no module or userRequest are defined - if (!module || !module.resource) { - return; - } - - // ignore if this module represents the - // dll entry file - if (this.getEntryPaths().includes(module.resource)) { - return; - } - - // ignore if this module is part of the - // files inside dynamic dll plugin public folder - if (inDllPluginPublic(module.resource)) { - return; - } - - // A module is not allowed if it's not a node_module, a webpackShim - // or the reasons from being bundled into the dll are not node_modules - if (notInNodeModulesOrWebpackShims(module.resource)) { - const reasons = module.reasons || []; - - reasons.forEach((reason) => { - // Skip if we can't read the reason info - if (!reason || !reason.module || !reason.module.resource) { - return; - } - - // Is the reason for this module being bundle a - // node_module or no? - if (notInNodeModules(reason.module.resource)) { - notAllowedModules.push(module.resource); - } - }); - } - }); - - if (notAllowedModules.length) { - // Delete the built dll, as it contains invalid modules, and reject listing - // all the not allowed modules - try { - await del(this.rawDllConfig.outputPath); - } catch (e) { - return reject(e); - } - - return reject( - `The following modules are not allowed to be bundled into the dll: \n${notAllowedModules.join( - '\n' - )}` - ); - } - - // Otherwise it has succeed - return resolve(stats); - }); - }); - } -} diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js deleted file mode 100644 index eec369b194fef..0000000000000 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ /dev/null @@ -1,278 +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 { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; -import { fromRoot } from '../../core/server/utils'; -import webpack from 'webpack'; -import webpackMerge from 'webpack-merge'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import TerserPlugin from 'terser-webpack-plugin'; -import * as UiSharedDeps from '@kbn/ui-shared-deps'; - -function generateDLL(config) { - const { - dllAlias, - dllValidateSyntax, - dllNoParseRules, - dllContext, - dllEntry, - dllOutputPath, - dllPublicPath, - dllBundleName, - dllBundleFilename, - dllStyleFilename, - dllManifestPath, - babelLoaderCacheDir, - threadLoaderPoolConfig, - } = config; - - const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); - const BABEL_EXCLUDE_RE = [/[\/\\](webpackShims|node_modules|bower_components)[\/\\]/]; - - /** - * Wrap plugin loading in a function so that we can require - * `@kbn/optimizer` only when absolutely necessary since we - * don't ship this package in the distributable but this code - * is still shipped, though it's not used. - */ - const getValidateSyntaxPlugins = () => { - if (!dllValidateSyntax) { - return []; - } - - // only require @kbn/optimizer - const { DisallowedSyntaxPlugin } = require('@kbn/optimizer'); - return [new DisallowedSyntaxPlugin()]; - }; - - return { - entry: dllEntry, - context: dllContext, - output: { - futureEmitAssets: true, // TODO: remove on webpack 5 - filename: dllBundleFilename, - path: dllOutputPath, - publicPath: dllPublicPath, - library: dllBundleName, - }, - node: { fs: 'empty', child_process: 'empty', dns: 'empty', net: 'empty', tls: 'empty' }, - resolve: { - extensions: ['.js', '.json'], - mainFields: ['browser', 'browserify', 'main'], - alias: dllAlias, - modules: ['webpackShims', fromRoot('webpackShims'), 'node_modules', fromRoot('node_modules')], - }, - module: { - rules: [ - { - resource: [ - { - test: /\.js$/, - exclude: BABEL_EXCLUDE_RE.concat(dllNoParseRules), - }, - { - test: /\.js$/, - include: /[\/\\]node_modules[\/\\]x-pack[\/\\]/, - exclude: /[\/\\]node_modules[\/\\]x-pack[\/\\](.+?[\/\\])*node_modules[\/\\]/, - }, - // TODO: remove when we drop support for IE11 - // We need because normalize-url is distributed without - // any kind of transpilation - // More info: https://github.com/elastic/kibana/pull/35804 - { - test: /\.js$/, - include: /[\/\\]node_modules[\/\\]normalize-url[\/\\]/, - exclude: /[\/\\]node_modules[\/\\]normalize-url[\/\\](.+?[\/\\])*node_modules[\/\\]/, - }, - ], - // Self calling function with the equivalent logic - // from maybeAddCacheLoader one from base optimizer - use: ((babelLoaderCacheDirPath, loaders) => { - return [ - { - loader: 'cache-loader', - options: { - cacheContext: fromRoot('.'), - cacheDirectory: babelLoaderCacheDirPath, - readOnly: process.env.KBN_CACHE_LOADER_WRITABLE ? false : IS_KIBANA_DISTRIBUTABLE, - }, - }, - ...loaders, - ]; - })(babelLoaderCacheDir, [ - { - loader: 'thread-loader', - options: threadLoaderPoolConfig, - }, - { - loader: 'babel-loader', - options: { - babelrc: false, - presets: [BABEL_PRESET_PATH], - }, - }, - ]), - }, - { - test: /\.(html|tmpl)$/, - loader: 'raw-loader', - }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - { - test: /\.png$/, - loader: 'url-loader', - }, - { - test: /\.(woff|woff2|ttf|eot|svg|ico)(\?|$)/, - loader: 'file-loader', - }, - ], - noParse: dllNoParseRules, - }, - plugins: [ - new webpack.DllPlugin({ - context: dllContext, - name: dllBundleName, - path: dllManifestPath, - }), - new MiniCssExtractPlugin({ - filename: dllStyleFilename, - }), - ...getValidateSyntaxPlugins(), - ], - // Single runtime for the dll bundles which assures that common transient dependencies won't be evaluated twice. - // The module cache will be shared, even when module code may be duplicated across chunks. - optimization: { - runtimeChunk: { - name: 'vendors_runtime', - }, - }, - performance: { - // NOTE: we are disabling this as those hints - // are more tailored for the final bundles result - // and not for the webpack compilations performance itself - hints: false, - }, - externals: { - ...UiSharedDeps.externals, - }, - }; -} - -function extendRawConfig(rawConfig) { - // Build all extended configs from raw config - const dllAlias = rawConfig.uiBundles.getAliases(); - const dllValidateSyntax = rawConfig.uiBundles.shouldValidateSyntaxOfNodeModules(); - const dllNoParseRules = rawConfig.uiBundles.getWebpackNoParseRules(); - const dllDevMode = rawConfig.uiBundles.isDevMode(); - const dllContext = rawConfig.context; - const dllChunks = rawConfig.chunks; - const dllEntry = {}; - const dllEntryName = rawConfig.entryName; - const dllBundleName = rawConfig.dllName; - const dllManifestName = rawConfig.dllName; - const dllStyleName = rawConfig.styleName; - const dllEntryExt = rawConfig.entryExt; - const dllBundleExt = rawConfig.dllExt; - const dllManifestExt = rawConfig.manifestExt; - const dllStyleExt = rawConfig.styleExt; - const dllOutputPath = rawConfig.outputPath; - const dllPublicPath = rawConfig.publicPath; - const dllBundleFilename = `${dllBundleName}${dllBundleExt}`; - const dllManifestPath = `${dllOutputPath}/${dllManifestName}${dllManifestExt}`; - const dllStyleFilename = `${dllStyleName}${dllStyleExt}`; - const babelLoaderCacheDir = rawConfig.babelLoaderCacheDir; - const threadLoaderPoolConfig = rawConfig.threadLoaderPoolConfig; - - // Create webpack entry object key with the provided dllEntryName - dllChunks.reduce((dllEntryObj, chunk) => { - dllEntryObj[`${dllEntryName}${chunk}`] = [ - `${dllOutputPath}/${dllEntryName}${chunk}${dllEntryExt}`, - ]; - return dllEntryObj; - }, dllEntry); - - // Export dll config map - return { - dllAlias, - dllValidateSyntax, - dllNoParseRules, - dllDevMode, - dllContext, - dllEntry, - dllOutputPath, - dllPublicPath, - dllBundleName, - dllBundleFilename, - dllStyleFilename, - dllManifestPath, - babelLoaderCacheDir, - threadLoaderPoolConfig, - }; -} - -function common(config) { - return webpackMerge(generateDLL(config)); -} - -function optimized() { - return webpackMerge({ - mode: 'production', - optimization: { - minimizer: [ - new TerserPlugin({ - // NOTE: we should not enable that option for now - // Since 2.0.0 terser-webpack-plugin is using jest-worker - // to run tasks in a pool of workers. Currently it looks like - // is requiring too much memory and break on large entry points - // compilations (like this) one. Also the gain we have enabling - // that option was barely noticed. - // https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 - parallel: false, - sourceMap: false, - cache: false, - extractComments: false, - terserOptions: { - compress: false, - mangle: false, - }, - }), - ], - }, - }); -} - -function unoptimized() { - return webpackMerge({ - mode: 'development', - }); -} - -export function configModel(rawConfig = {}) { - const config = extendRawConfig(rawConfig); - - if (config.dllDevMode) { - return webpackMerge(common(config), unoptimized()); - } - - return webpackMerge(common(config), optimized()); -} diff --git a/src/optimize/dynamic_dll_plugin/dll_entry_template.js b/src/optimize/dynamic_dll_plugin/dll_entry_template.js deleted file mode 100644 index 351bed4e591ba..0000000000000 --- a/src/optimize/dynamic_dll_plugin/dll_entry_template.js +++ /dev/null @@ -1,37 +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. - */ - -export function dllEntryTemplate(requirePaths = []) { - return requirePaths - .map((path) => `require('${path}');`) - .sort() - .join('\n'); -} - -export function dllEntryFileContentStringToArray(content = '') { - return content.split('\n'); -} - -export function dllEntryFileContentArrayToString(content = []) { - return content.join('\n'); -} - -export function dllMergeAllEntryFilesContent(content = []) { - return content.join('\n').split('\n').sort().join('\n'); -} diff --git a/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js b/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js deleted file mode 100644 index fb6f6e097b5cd..0000000000000 --- a/src/optimize/dynamic_dll_plugin/dynamic_dll_plugin.js +++ /dev/null @@ -1,354 +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 { DllCompiler } from './dll_compiler'; -import { notInNodeModulesOrWebpackShims, inPluginNodeModules } from './dll_allowed_modules'; -import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; -import { dllEntryTemplate } from './dll_entry_template'; -import RawModule from 'webpack/lib/RawModule'; -import webpack from 'webpack'; -import path from 'path'; -import normalizePosixPath from 'normalize-path'; -import fs from 'fs'; -import { promisify } from 'util'; - -const realPathAsync = promisify(fs.realpath); -const DLL_ENTRY_STUB_MODULE_TYPE = 'javascript/dll-entry-stub'; - -export class DynamicDllPlugin { - constructor({ uiBundles, threadLoaderPoolConfig, logWithMetadata, maxCompilations = 1 }) { - this.logWithMetadata = logWithMetadata || (() => null); - this.dllCompiler = new DllCompiler(uiBundles, threadLoaderPoolConfig, logWithMetadata); - this.entryPaths = dllEntryTemplate(); - this.afterCompilationEntryPaths = dllEntryTemplate(); - this.maxCompilations = maxCompilations; - this.performedCompilations = 0; - this.forceDLLCreationFlag = !!(process && process.env && process.env.FORCE_DLL_CREATION); - } - - async init() { - await this.dllCompiler.init(); - this.entryPaths = await this.dllCompiler.readEntryFiles(); - } - - apply(compiler) { - // Just register the init basic hooks - // in order to run the init function - this.registerInitBasicHooks(compiler); - // The dll reference should always be bind to the - // main webpack config. - this.bindDllReferencePlugin(compiler); - - // Verify if we must init and run the dynamic dll plugin tasks. - // We must run it every time we are not under a distributable env - if (!this.mustRunDynamicDllPluginTasks()) { - return; - } - - // This call init all the DynamicDllPlugin tasks - // as it attaches the plugin to the main webpack - // lifecycle hooks needed to perform the logic - this.registerTasksHooks(compiler); - } - - bindDllReferencePlugin(compiler) { - const rawDllConfig = this.dllCompiler.rawDllConfig; - const dllContext = rawDllConfig.context; - const dllManifestPaths = this.dllCompiler.getManifestPaths(); - - dllManifestPaths.forEach((dllChunkManifestPath) => { - new webpack.DllReferencePlugin({ - context: dllContext, - manifest: dllChunkManifestPath, - }).apply(compiler); - }); - } - - registerInitBasicHooks(compiler) { - this.registerRunHook(compiler); - this.registerWatchRunHook(compiler); - } - - registerTasksHooks(compiler) { - this.logWithMetadata( - ['info', 'optimize:dynamic_dll_plugin'], - 'Started dynamic dll plugin tasks' - ); - this.registerBeforeCompileHook(compiler); - this.registerCompilationHook(compiler); - this.registerDoneHook(compiler); - } - - registerRunHook(compiler) { - compiler.hooks.run.tapPromise('DynamicDllPlugin', async () => { - await this.init(); - }); - } - - registerWatchRunHook(compiler) { - compiler.hooks.watchRun.tapPromise('DynamicDllPlugin', async () => { - await this.init(); - }); - } - - registerBeforeCompileHook(compiler) { - compiler.hooks.beforeCompile.tapPromise('DynamicDllPlugin', async ({ normalModuleFactory }) => { - normalModuleFactory.hooks.factory.tap('DynamicDllPlugin', (actualFactory) => (params, cb) => { - // This is used in order to avoid the cache for DLL modules - // resolved from other dependencies - normalModuleFactory.cachePredicate = (module) => - !(module.stubType === DLL_ENTRY_STUB_MODULE_TYPE); - - // Overrides the normalModuleFactory module creation behaviour - // in order to understand the modules we need to add to the DLL - actualFactory(params, (error, module) => { - if (error || !module) { - cb(error, module); - } else { - this.mapNormalModule(module).then( - (m = module) => cb(undefined, m), - (error) => cb(error) - ); - } - }); - }); - }); - } - - registerCompilationHook(compiler) { - compiler.hooks.compilation.tap('DynamicDllPlugin', (compilation) => { - compilation.hooks.needAdditionalPass.tap('DynamicDllPlugin', () => { - // Run the procedures in order to execute our dll compilation - // The process is very straightforward in it's conception: - // - // * 1 - loop through every compilation module in order to start building - // the dll entry paths arrays and assume it is the new entry paths - // * 1.1 - start from adding the modules already included into the dll, if any. - // * 1.2 - adding the new discovered stub modules - // * 1.3 - check if the module added to the entry path is from node_modules or - // webpackShims, otherwise throw an error. - // * 1.3.1 - for the entry path modules coming from webpackShims search for every - // require statements inside of them - // * 1.3.2 - discard the ones that are not js dependencies - // * 1.3.3 - add those new discovered dependencies inside the webpackShims to the - // entry paths array - // * 2 - compare the built entry paths and compares it to the old one (if any) - // * 3 - runs a new dll compilation in case there is none old entry paths or if the - // new built one differs from the old one. - // - const rawDllConfig = this.dllCompiler.rawDllConfig; - const dllContext = rawDllConfig.context; - const dllOutputPath = rawDllConfig.outputPath; - const requiresMap = {}; - - for (const module of compilation.modules) { - // re-include requires for modules already handled by the dll - if (module.delegateData) { - const absoluteResource = path.resolve(dllContext, module.userRequest); - if ( - absoluteResource.includes('node_modules') || - absoluteResource.includes('webpackShims') - ) { - // NOTE: normalizePosixPath is been used as we only want to have posix - // paths inside our final dll entry file - requiresMap[ - normalizePosixPath(path.relative(dllOutputPath, absoluteResource)) - ] = true; - } - } - - // include requires for modules that need to be added to the dll - if (module.stubType === DLL_ENTRY_STUB_MODULE_TYPE) { - requiresMap[ - normalizePosixPath(path.relative(dllOutputPath, module.stubResource)) - ] = true; - } - } - - // Sort and join all the discovered require deps - // in order to create a consistent entry file - this.afterCompilationEntryPaths = dllEntryTemplate(Object.keys(requiresMap)); - // The dll compilation will run if on of the following conditions return true: - // 1 - the new generated entry paths are different from the - // old ones - // 2 - if no dll bundle is yet created - // 3 - if this.forceDLLCreationFlag were set from the node env var FORCE_DLL_CREATION and - // we are not running over the distributable. If we are running under the watch optimizer, - // this.forceDLLCreationFlag will only be applied in the very first execution, - // then will be set to false - compilation.needsDLLCompilation = - this.afterCompilationEntryPaths !== this.entryPaths || - !this.dllCompiler.dllsExistsSync() || - (this.isToForceDLLCreation() && this.performedCompilations === 0); - this.entryPaths = this.afterCompilationEntryPaths; - - // Only run this info log in the first performed dll compilation - // per each execution run - if (this.performedCompilations === 0) { - this.logWithMetadata( - ['info', 'optimize:dynamic_dll_plugin'], - compilation.needsDLLCompilation - ? 'Need to compile the client vendors dll' - : 'No need to compile client vendors dll' - ); - } - - return compilation.needsDLLCompilation; - }); - }); - } - - registerDoneHook(compiler) { - compiler.hooks.done.tapPromise('DynamicDllPlugin', async (stats) => { - if (stats.compilation.needsDLLCompilation) { - // Run the dlls compiler and increment - // the performed compilations - // - // NOTE: check the need for this extra try/catch after upgrading - // past webpack v4.29.3. For now it is needed so we can log the error - // otherwise the error log we'll get will be something like: [fatal] [object Object] - try { - await this.runDLLCompiler(compiler); - } catch (error) { - this.logWithMetadata(['error', 'optimize:dynamic_dll_plugin'], error.message); - throw error; - } - - return; - } - - this.performedCompilations = 0; - // reset this flag var set from the node env FORCE_DLL_CREATION on init, - // has the force_dll_creation is only valid for the very first run - if (this.forceDLLCreationFlag) { - this.forceDLLCreationFlag = false; - } - this.logWithMetadata( - ['info', 'optimize:dynamic_dll_plugin'], - 'Finished all dynamic dll plugin tasks' - ); - }); - } - - isToForceDLLCreation() { - return this.forceDLLCreationFlag; - } - - mustRunDynamicDllPluginTasks() { - return !IS_KIBANA_DISTRIBUTABLE || this.isToForceDLLCreation(); - } - - async mapNormalModule(module) { - // ignore anything that doesn't have a resource (ignored) or is already delegating to the DLL - if (!module.resource || module.delegateData) { - return; - } - - // ignore anything that needs special loaders or config - if (module.request.includes('!') || module.request.includes('?')) { - return; - } - - // ignore files that are not in node_modules - if (notInNodeModulesOrWebpackShims(module.resource)) { - return; - } - - // also ignore files that are symlinked into node_modules, but only - // do the `realpath` call after checking the plain resource path - if (notInNodeModulesOrWebpackShims(await realPathAsync(module.resource))) { - return; - } - - const dirs = module.resource.split(path.sep); - const nodeModuleName = dirs[dirs.lastIndexOf('node_modules') + 1]; - - // ignore webpack loader modules - if (nodeModuleName.endsWith('-loader')) { - return; - } - - // ignore modules from plugins - if (inPluginNodeModules(module.resource)) { - return; - } - - // also ignore files that are symlinked into plugins node_modules, but only - // do the `realpath` call after checking the plain resource path - if (inPluginNodeModules(await realPathAsync(module.resource))) { - return; - } - - // This is a StubModule (as a RawModule) in order - // to mimic the missing modules from the DLL and - // also hold useful metadata - const stubModule = new RawModule( - `/* pending dll entry */`, - `dll pending:${module.resource}`, - module.resource - ); - stubModule.stubType = DLL_ENTRY_STUB_MODULE_TYPE; - stubModule.stubResource = module.resource; - stubModule.stubOriginalModule = module; - - return stubModule; - } - - async assertMaxCompilations() { - // Logic to run the max compilation requirements. - // Only enable this for CI builds in order to ensure - // we have an healthy dll ecosystem. - if (this.performedCompilations === this.maxCompilations) { - throw new Error( - 'All the allowed dll compilations were already performed and one more is needed which is not possible' - ); - } - } - - async runDLLCompiler(mainCompiler) { - const runCompilerErrors = []; - - try { - await this.dllCompiler.run(this.entryPaths); - } catch (e) { - runCompilerErrors.push(e); - } - - try { - await this.assertMaxCompilations(); - } catch (e) { - runCompilerErrors.push(e); - } - - // We need to purge the cache into the inputFileSystem - // for every single built in previous compilation - // that we rely in next ones. - this.dllCompiler - .getManifestPaths() - .forEach((chunkDllManifestPath) => mainCompiler.inputFileSystem.purge(chunkDllManifestPath)); - - this.performedCompilations++; - - if (!runCompilerErrors.length) { - return; - } - - throw new Error(runCompilerErrors.join('\n-')); - } -} diff --git a/src/optimize/dynamic_dll_plugin/index.js b/src/optimize/dynamic_dll_plugin/index.js deleted file mode 100644 index 347811b5356b7..0000000000000 --- a/src/optimize/dynamic_dll_plugin/index.js +++ /dev/null @@ -1,21 +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. - */ - -export { DynamicDllPlugin } from './dynamic_dll_plugin'; -export { DllCompiler } from './dll_compiler'; diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts index 9a3f08e2f667e..947c918a567f5 100644 --- a/src/optimize/optimize_mixin.ts +++ b/src/optimize/optimize_mixin.ts @@ -22,8 +22,6 @@ import Hapi from 'hapi'; // @ts-ignore not TS yet import FsOptimizer from './fs_optimizer'; import { createBundlesRoute } from './bundles_route'; -// @ts-ignore not TS yet -import { DllCompiler } from './dynamic_dll_plugin'; import { fromRoot } from '../core/server/utils'; import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; import KbnServer, { KibanaConfig } from '../legacy/server/kbn_server'; @@ -40,7 +38,7 @@ export const optimizeMixin = async ( // bundles in a "middleware" style. // // the server listening on 5601 may be restarted a number of times, depending - // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* + // on the watch setup managed by the cli. It proxies all bundles/* // requests to the other server. The server on 5602 is long running, in order // to prevent complete rebuilds of the optimize content. const watch = config.get('optimize.watch'); @@ -53,7 +51,6 @@ export const optimizeMixin = async ( server.route( createBundlesRoute({ regularBundlesPath: uiBundles.getWorkingDir(), - dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: config.get('server.basePath'), builtCssPath: fromRoot('built_assets/css'), npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index ba8007e1065b4..0057c04219ec6 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -17,12 +17,8 @@ * under the License. */ -import { resolve } from 'path'; - import WatchServer from './watch_server'; import WatchOptimizer, { STATUS } from './watch_optimizer'; -import { DllCompiler } from '../dynamic_dll_plugin'; -import { WatchCache } from './watch_cache'; import { getNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; export default async (kbnServer, kibanaHapiServer, config) => { @@ -36,12 +32,6 @@ export default async (kbnServer, kibanaHapiServer, config) => { sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), prebuild: config.get('optimize.watchPrebuild'), - watchCache: new WatchCache({ - logWithMetadata, - outputPath: config.get('path.data'), - dllsPath: DllCompiler.getRawDllConfig().outputPath, - cachePath: resolve(kbnServer.uiBundles.getCacheDirectory(), '../'), - }), }); const server = new WatchServer( diff --git a/src/optimize/watch/watch.js b/src/optimize/watch/watch.js index a284da11f294f..7774577fb7677 100644 --- a/src/optimize/watch/watch.js +++ b/src/optimize/watch/watch.js @@ -32,7 +32,7 @@ export default async (kbnServer) => { * while the optimizer is running * * server: this process runs the entire kibana server and proxies - * all requests for /bundles/* or /built_assets/dlls/* to the optmzr process + * all requests for /bundles/* to the optmzr process * * @param {string} process.env.kbnWorkerType */ diff --git a/src/optimize/watch/watch_cache.ts b/src/optimize/watch/watch_cache.ts deleted file mode 100644 index 40bd1d6075f47..0000000000000 --- a/src/optimize/watch/watch_cache.ts +++ /dev/null @@ -1,189 +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 { createHash } from 'crypto'; -import { readFile, writeFile, readdir, unlink, rmdir } from 'fs'; -import { resolve } from 'path'; -import { promisify } from 'util'; -import path from 'path'; -import del from 'del'; -import normalizePosixPath from 'normalize-path'; - -const readAsync = promisify(readFile); -const writeAsync = promisify(writeFile); -const readdirAsync = promisify(readdir); -const unlinkAsync = promisify(unlink); -const rmdirAsync = promisify(rmdir); - -interface Params { - logWithMetadata: (tags: string[], message: string, metadata?: { [key: string]: any }) => void; - outputPath: string; - dllsPath: string; - cachePath: string; -} - -interface WatchCacheStateContent { - optimizerConfigSha?: string; - yarnLockSha?: string; -} - -export class WatchCache { - private readonly logWithMetadata: Params['logWithMetadata']; - private readonly outputPath: Params['outputPath']; - private readonly dllsPath: Params['dllsPath']; - private readonly cachePath: Params['cachePath']; - private readonly cacheState: WatchCacheStateContent; - private statePath: string; - private diskCacheState: WatchCacheStateContent; - private isInitialized: boolean; - - constructor(params: Params) { - this.logWithMetadata = params.logWithMetadata; - this.outputPath = params.outputPath; - this.dllsPath = params.dllsPath; - this.cachePath = params.cachePath; - - this.isInitialized = false; - this.statePath = ''; - this.cacheState = {}; - this.diskCacheState = {}; - this.cacheState.yarnLockSha = ''; - this.cacheState.optimizerConfigSha = ''; - } - - public async tryInit() { - if (!this.isInitialized) { - this.statePath = resolve(this.outputPath, 'watch_optimizer_cache_state.json'); - this.diskCacheState = await this.read(); - this.cacheState.yarnLockSha = await this.buildYarnLockSha(); - this.cacheState.optimizerConfigSha = await this.buildOptimizerConfigSha(); - this.isInitialized = true; - } - } - - public async tryReset() { - await this.tryInit(); - - if (!this.isResetNeeded()) { - return; - } - - await this.reset(); - } - - public async reset() { - this.logWithMetadata(['info', 'optimize:watch_cache'], 'The optimizer watch cache will reset'); - - // start by deleting the state file to lower the - // amount of time that another process might be able to - // successfully read it once we decide to delete it - await del(this.statePath, { force: true }); - - // delete everything in optimize/.cache directory - await recursiveDelete(normalizePosixPath(this.cachePath)); - - // delete dlls - await del(this.dllsPath); - - // re-write new cache state file - await this.write(); - - this.logWithMetadata(['info', 'optimize:watch_cache'], 'The optimizer watch cache has reset'); - } - - private async buildShaWithMultipleFiles(filePaths: string[]) { - const shaHash = createHash('sha1'); - - for (const filePath of filePaths) { - try { - shaHash.update(await readAsync(filePath, 'utf8'), 'utf8'); - } catch (e) { - /* no-op */ - } - } - - return shaHash.digest('hex'); - } - - private async buildYarnLockSha() { - const kibanaYarnLock = resolve(__dirname, '../../../yarn.lock'); - - return await this.buildShaWithMultipleFiles([kibanaYarnLock]); - } - - private async buildOptimizerConfigSha() { - const baseOptimizer = resolve(__dirname, '../base_optimizer.js'); - const dynamicDllConfigModel = resolve(__dirname, '../dynamic_dll_plugin/dll_config_model.js'); - const dynamicDllPlugin = resolve(__dirname, '../dynamic_dll_plugin/dynamic_dll_plugin.js'); - - return await this.buildShaWithMultipleFiles([ - baseOptimizer, - dynamicDllConfigModel, - dynamicDllPlugin, - ]); - } - - private isResetNeeded() { - return this.hasYarnLockChanged() || this.hasOptimizerConfigChanged(); - } - - private hasYarnLockChanged() { - return this.cacheState.yarnLockSha !== this.diskCacheState.yarnLockSha; - } - - private hasOptimizerConfigChanged() { - return this.cacheState.optimizerConfigSha !== this.diskCacheState.optimizerConfigSha; - } - - private async write() { - await writeAsync(this.statePath, JSON.stringify(this.cacheState, null, 2), 'utf8'); - this.diskCacheState = this.cacheState; - } - - private async read(): Promise { - try { - return JSON.parse(await readAsync(this.statePath, 'utf8')); - } catch (error) { - return {}; - } - } -} - -/** - * Recursively deletes a folder. This is a workaround for a bug in `del` where - * very large folders (with 84K+ files) cause a stack overflow. - */ -async function recursiveDelete(directory: string) { - try { - const entries = await readdirAsync(directory, { withFileTypes: true }); - - await Promise.all( - entries.map((entry) => { - const absolutePath = path.join(directory, entry.name); - return entry.isDirectory() ? recursiveDelete(absolutePath) : unlinkAsync(absolutePath); - }) - ); - - return rmdirAsync(directory); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } -} diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 816185e544ab5..000c03ffb34fe 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -19,7 +19,6 @@ import BaseOptimizer from '../base_optimizer'; import { createBundlesRoute } from '../bundles_route'; -import { DllCompiler } from '../dynamic_dll_plugin'; import { fromRoot } from '../../core/server/utils'; import * as Rx from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; @@ -35,7 +34,6 @@ export default class WatchOptimizer extends BaseOptimizer { constructor(opts) { super(opts); this.prebuild = opts.prebuild || false; - this.watchCache = opts.watchCache; this.status$ = new Rx.ReplaySubject(1); } @@ -43,9 +41,6 @@ export default class WatchOptimizer extends BaseOptimizer { this.initializing = true; this.initialBuildComplete = false; - // try reset the watch optimizer cache - await this.watchCache.tryReset(); - // log status changes this.status$.subscribe(this.onStatusChangeHandler); await this.uiBundles.resetBundleDir(); @@ -120,7 +115,6 @@ export default class WatchOptimizer extends BaseOptimizer { npUiPluginPublicDirs: npUiPluginPublicDirs, buildHash, regularBundlesPath: this.compiler.outputPath, - dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, builtCssPath: fromRoot('built_assets/css'), }) diff --git a/tasks/config/karma.js b/tasks/config/karma.js index fa4bdc8ed2266..114e09876406c 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -21,7 +21,6 @@ import { dirname } from 'path'; import { times } from 'lodash'; import { makeJunitReportPath } from '@kbn/test'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); @@ -63,12 +62,6 @@ module.exports = function (grunt) { ), `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - `http://localhost:5610/${buildHash}/built_assets/dlls/vendors_runtime.bundle.dll.js`, - ...DllCompiler.getRawDllConfig().chunks.map( - (chunk) => - `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.bundle.dll.js` - ), - shardNum === undefined ? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js` : `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, @@ -77,10 +70,6 @@ module.exports = function (grunt) { // this causes tilemap tests to fail, probably because the eui styles haven't been // included in the karma harness a long some time, if ever // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, - ...DllCompiler.getRawDllConfig().chunks.map( - (chunk) => - `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.style.dll.css` - ), `http://localhost:5610/${buildHash}/bundles/tests.style.css`, ]; } @@ -133,7 +122,6 @@ module.exports = function (grunt) { '/tests/': 'http://localhost:5610/tests/', '/test_bundle/': 'http://localhost:5610/test_bundle/', [`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`, - [`/${buildHash}/built_assets/dlls/`]: `http://localhost:5610/${buildHash}/built_assets/dlls/`, }, client: { From 138c0417a7818097a4167cbb7d3057d9714a0cfe Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 17 Jul 2020 12:11:15 +0100 Subject: [PATCH 36/76] skip flaky suite (#60865) --- x-pack/test/api_integration/apis/fleet/agents/enroll.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 e9f7471f6437e..8a21fbcf24c7d 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -21,7 +21,8 @@ export default function (providerContext: FtrProviderContext) { let apiKey: { id: string; api_key: string }; let kibanaVersion: string; - describe('fleet_agents_enroll', () => { + // Flaky: https://github.com/elastic/kibana/issues/60865 + describe.skip('fleet_agents_enroll', () => { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); From 19def177f4806710897e8d148cb06ac4b103a981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 17 Jul 2020 12:15:15 +0100 Subject: [PATCH 37/76] replacing hard coded links for ela.st (#72240) --- .../observability/public/components/app/header/index.tsx | 5 +---- .../public/components/app/ingest_manager_panel/index.tsx | 2 +- .../observability/public/components/app/resources/index.tsx | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index 1c6ce766d0901..531e6abf3d236 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -69,10 +69,7 @@ export const Header = ({ {showGiveFeedback && ( - + {i18n.translate('xpack.observability.home.feedback', { defaultMessage: 'Give us feedback', })} diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx index f7a1deb83fbe4..41bcfa1da7fa1 100644 --- a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx @@ -40,7 +40,7 @@ export const IngestManagerPanel = () => { - + {i18n.translate('xpack.observability.ingestManafer.button', { defaultMessage: 'Try Ingest Manager Beta', })} diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx index c330c358d022a..929802df3329b 100644 --- a/x-pack/plugins/observability/public/components/app/resources/index.tsx +++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx @@ -13,21 +13,21 @@ const resources = [ label: i18n.translate('xpack.observability.resources.documentation', { defaultMessage: 'Documentation', }), - href: 'https://www.elastic.co/guide/en/observability/current/observability-ui.html', + href: 'https://ela.st/observability-documentation', }, { iconType: 'editorComment', label: i18n.translate('xpack.observability.resources.forum', { defaultMessage: 'Discuss forum', }), - href: 'https://discuss.elastic.co/c/observability/', + href: 'https://ela.st/observability-discuss', }, { iconType: 'training', label: i18n.translate('xpack.observability.resources.training', { defaultMessage: 'Observability fundamentals', }), - href: 'https://www.elastic.co/training/observability-fundamentals', + href: 'https://ela.st/observability-training', }, ]; From b4f07de564892c6460acd9f45d9db9e0f57d9b0d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 17 Jul 2020 15:00:34 +0300 Subject: [PATCH 38/76] [Security Solution][Case] Fix connector's dropdown with conflicting requests (#72037) --- .../cases/components/case_view/index.test.tsx | 28 ++++++++++ .../cases/components/case_view/index.tsx | 54 ++++++++++++++----- .../components/edit_connector/index.test.tsx | 27 +++++++++- .../cases/components/edit_connector/index.tsx | 13 +++-- .../user_action_tree/index.test.tsx | 2 +- .../components/user_action_tree/index.tsx | 5 +- .../cases/containers/use_update_case.test.tsx | 7 +++ .../cases/containers/use_update_case.tsx | 19 ++++++- .../events_viewer/events_viewer.test.tsx | 43 ++++----------- .../ml/anomaly/use_anomalies_table_data.ts | 4 +- .../public/common/containers/source/index.tsx | 4 +- .../rules/fetch_index_patterns.tsx | 4 +- .../timelines/store/timeline/helpers.ts | 4 +- 13 files changed, 154 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index b93df325b5a8b..4e29db4022e65 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -409,4 +409,32 @@ describe('CaseView ', () => { wrapper.find('button[data-test-subj="push-to-external-service"]').first().prop('disabled') ).toBeTruthy(); }); + + it('should revert to the initial connector in case of failure', async () => { + updateCaseProperty.mockImplementation(({ onError }) => { + onError(); + }); + const wrapper = mount( + + + + + + ); + await wait(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + wrapper.update(); + await wait(); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').at(0).prop('valueOfSelected') + ).toBe('servicenow-1'); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 3718249479b63..b23169af6ceb3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -44,6 +44,13 @@ interface Props { userCanCrud: boolean; } +export interface OnUpdateFields { + key: keyof Case; + value: Case[keyof Case]; + onSuccess?: () => void; + onError?: () => void; +} + const MyWrapper = styled.div` padding: ${({ theme, @@ -88,12 +95,12 @@ export const CaseComponent = React.memo( // Update Fields const onUpdateField = useCallback( - (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + ({ key, value, onSuccess, onError }: OnUpdateFields) => { const handleUpdateNewCase = (newCase: Case) => updateCase({ ...newCase, comments: caseData.comments }); - switch (newUpdateKey) { + switch (key) { case 'title': - const titleUpdate = getTypedPayload(updateValue); + const titleUpdate = getTypedPayload(value); if (titleUpdate.length > 0) { updateCaseProperty({ fetchCaseUserActions, @@ -101,11 +108,13 @@ export const CaseComponent = React.memo( updateValue: titleUpdate, updateCase: handleUpdateNewCase, version: caseData.version, + onSuccess, + onError, }); } break; case 'connectorId': - const connectorId = getTypedPayload(updateValue); + const connectorId = getTypedPayload(value); if (connectorId.length > 0) { updateCaseProperty({ fetchCaseUserActions, @@ -113,11 +122,13 @@ export const CaseComponent = React.memo( updateValue: connectorId, updateCase: handleUpdateNewCase, version: caseData.version, + onSuccess, + onError, }); } break; case 'description': - const descriptionUpdate = getTypedPayload(updateValue); + const descriptionUpdate = getTypedPayload(value); if (descriptionUpdate.length > 0) { updateCaseProperty({ fetchCaseUserActions, @@ -125,28 +136,34 @@ export const CaseComponent = React.memo( updateValue: descriptionUpdate, updateCase: handleUpdateNewCase, version: caseData.version, + onSuccess, + onError, }); } break; case 'tags': - const tagsUpdate = getTypedPayload(updateValue); + const tagsUpdate = getTypedPayload(value); updateCaseProperty({ fetchCaseUserActions, updateKey: 'tags', updateValue: tagsUpdate, updateCase: handleUpdateNewCase, version: caseData.version, + onSuccess, + onError, }); break; case 'status': - const statusUpdate = getTypedPayload(updateValue); - if (caseData.status !== updateValue) { + const statusUpdate = getTypedPayload(value); + if (caseData.status !== value) { updateCaseProperty({ fetchCaseUserActions, updateKey: 'status', updateValue: statusUpdate, updateCase: handleUpdateNewCase, version: caseData.version, + onSuccess, + onError, }); } default: @@ -191,15 +208,28 @@ export const CaseComponent = React.memo( }); const onSubmitConnector = useCallback( - (connectorId) => onUpdateField('connectorId', connectorId), + (connectorId, onSuccess, onError) => + onUpdateField({ + key: 'connectorId', + value: connectorId, + onSuccess, + onError, + }), [onUpdateField] ); - const onSubmitTags = useCallback((newTags) => onUpdateField('tags', newTags), [onUpdateField]); - const onSubmitTitle = useCallback((newTitle) => onUpdateField('title', newTitle), [ + const onSubmitTags = useCallback((newTags) => onUpdateField({ key: 'tags', value: newTags }), [ onUpdateField, ]); + const onSubmitTitle = useCallback( + (newTitle) => onUpdateField({ key: 'title', value: newTitle }), + [onUpdateField] + ); const toggleStatusCase = useCallback( - (e) => onUpdateField('status', e.target.checked ? 'closed' : 'open'), + (e) => + onUpdateField({ + key: 'status', + value: e.target.checked ? 'closed' : 'open', + }), [onUpdateField] ); const handleRefresh = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx index 251d0b6e81bf8..564ce2e19df00 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx @@ -69,10 +69,35 @@ describe('EditConnector ', () => { await act(async () => { wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); await wait(); - expect(onSubmit).toBeCalledWith(sampleConnector); + expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector); }); }); + it('Revert to initial external service on error', async () => { + onSubmit.mockImplementation((connector, onSuccess, onError) => { + onError(new Error('An error has occurred')); + }); + const wrapper = mount( + + + + ); + + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); + + await act(async () => { + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + await wait(); + wrapper.update(); + }); + expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none'); + }); + it('Resets selector on cancel', async () => { const props = { ...defaultProps, diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index 11938a55181d3..95ef3353a025f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -15,6 +15,8 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { noop } from 'lodash/fp'; + import * as i18n from '../../translations'; import { Form, UseField, useForm } from '../../../shared_imports'; import { schema } from './schema'; @@ -25,7 +27,7 @@ interface EditConnectorProps { connectors: Connector[]; disabled?: boolean; isLoading: boolean; - onSubmit: (a: string[]) => void; + onSubmit: (a: string[], onSuccess: () => void, onError: () => void) => void; selectedConnector: string; } @@ -61,6 +63,11 @@ export const EditConnector = React.memo( [selectedConnector] ); + const onError = useCallback(() => { + setFieldValue('connector', selectedConnector); + setConnectorHasChanged(false); + }, [setFieldValue, selectedConnector]); + const onCancelConnector = useCallback(() => { setFieldValue('connector', selectedConnector); setConnectorHasChanged(false); @@ -69,10 +76,10 @@ export const EditConnector = React.memo( const onSubmitConnector = useCallback(async () => { const { isValid, data: newData } = await submit(); if (isValid && newData.connector) { - onSubmit(newData.connector); + onSubmit(newData.connector, noop, onError); setConnectorHasChanged(false); } - }, [submit, onSubmit]); + }, [submit, onSubmit, onError]); return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index 285584cf2233c..23f1fb222a841 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -277,7 +277,7 @@ describe('UserActionTree ', () => { ) .exists() ).toEqual(false); - expect(onUpdateField).toBeCalledWith('description', sampleData.content); + expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 52c2779a93fc0..0c1da8694bf1a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -21,6 +21,7 @@ import { UserActionMarkdown } from './user_action_markdown'; import { Connector } from '../../../../../case/common/api/cases'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; +import { OnUpdateFields } from '../case_view'; export interface UserActionTreeProps { caseServices: CaseServices; @@ -30,7 +31,7 @@ export interface UserActionTreeProps { fetchUserActions: () => void; isLoadingDescription: boolean; isLoadingUserActions: boolean; - onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; + onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; updateCase: (newCase: Case) => void; userCanCrud: boolean; } @@ -138,7 +139,7 @@ export const UserActionTree = React.memo( content={caseData.description} isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} onSaveContent={(content: string) => { - onUpdateField(DESCRIPTION_ID, content); + onUpdateField({ key: DESCRIPTION_ID, value: content }); }} onChangeEditable={handleManageMarkdownEditId} /> diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx index 86cfc3459c595..01e64fa780d52 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx @@ -16,12 +16,17 @@ describe('useUpdateCase', () => { const fetchCaseUserActions = jest.fn(); const updateCase = jest.fn(); const updateKey: UpdateKey = 'description'; + const onSuccess = jest.fn(); + const onError = jest.fn(); + const sampleUpdate = { fetchCaseUserActions, updateKey, updateValue: 'updated description', updateCase, version: basicCase.version, + onSuccess, + onError, }; beforeEach(() => { jest.clearAllMocks(); @@ -79,6 +84,7 @@ describe('useUpdateCase', () => { }); expect(fetchCaseUserActions).toBeCalledWith(basicCase.id); expect(updateCase).toBeCalledWith(basicCase); + expect(onSuccess).toHaveBeenCalled(); }); }); @@ -114,6 +120,7 @@ describe('useUpdateCase', () => { isError: true, updateCaseProperty: result.current.updateCaseProperty, }); + expect(onError).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index 18dd9f5278503..6ede91b572dba 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -5,6 +5,7 @@ */ import { useReducer, useCallback } from 'react'; + import { displaySuccessToast, errorToToaster, @@ -33,6 +34,8 @@ export interface UpdateByKey { fetchCaseUserActions?: (caseId: string) => void; updateCase?: (newCase: Case) => void; version: string; + onSuccess?: () => void; + onError?: () => void; } type Action = @@ -81,7 +84,15 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => const [, dispatchToaster] = useStateToaster(); const dispatchUpdateCaseProperty = useCallback( - async ({ fetchCaseUserActions, updateKey, updateValue, updateCase, version }: UpdateByKey) => { + async ({ + fetchCaseUserActions, + updateKey, + updateValue, + updateCase, + version, + onSuccess, + onError, + }: UpdateByKey) => { let cancel = false; const abortCtrl = new AbortController(); @@ -102,6 +113,9 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => } dispatch({ type: 'FETCH_SUCCESS' }); displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); + if (onSuccess) { + onSuccess(); + } } } catch (error) { if (!cancel) { @@ -111,6 +125,9 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => dispatchToaster, }); dispatch({ type: 'FETCH_FAILURE' }); + if (onError) { + onError(); + } } } return () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 8c1f69279d31c..2a7cbff5ee149 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -32,18 +32,18 @@ mockUseResizeObserver.mockImplementation(() => ({})); const from = '2019-08-26T22:10:56.791Z'; const to = '2019-08-27T22:10:56.794Z'; +const defaultMocks = { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: false, +}; + describe('EventsViewer', () => { const mount = useMountAppended(); beforeEach(() => { - mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - docValueFields: mockDocValueFields, - isLoading: false, - }, - ]); + mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks }]); }); test('it renders the "Showing..." subtitle with the expected event count', async () => { @@ -69,14 +69,7 @@ describe('EventsViewer', () => { }); test('it does NOT render fetch index pattern is loading', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - docValueFields: mockDocValueFields, - isLoading: true, - }, - ]); + mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); const wrapper = mount( @@ -98,14 +91,7 @@ describe('EventsViewer', () => { }); test('it does NOT render when start is empty', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - docValueFields: mockDocValueFields, - isLoading: true, - }, - ]); + mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); const wrapper = mount( @@ -127,14 +113,7 @@ describe('EventsViewer', () => { }); test('it does NOT render when end is empty', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - docValueFields: mockDocValueFields, - isLoading: true, - }, - ]); + mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index a6bbdee79cf04..6fbb308672e5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -129,8 +129,8 @@ export const useAnomaliesTableData = ({ influencersOrCriteriaToString(influencers), // eslint-disable-next-line react-hooks/exhaustive-deps influencersOrCriteriaToString(criteriaFields), - startDate, - endDate, + startDateMs, + endDateMs, skip, userPermissions, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 8c03ab7b9f508..cc43dd6f42772 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -81,7 +81,7 @@ export const getBrowserFields = memoizeOne( (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); -export const getdocValueFields = memoizeOne( +export const getDocValueFields = memoizeOne( (_title: string, fields: IndexField[]): DocValueFields[] => fields && fields.length > 0 ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { @@ -177,7 +177,7 @@ export const useWithSource = ( defaultIndex.join(), get('data.source.status.indexFields', result) ), - docValueFields: getdocValueFields( + docValueFields: getDocValueFields( defaultIndex.join(), get('data.source.status.indexFields', result) ), diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx index ab12f045cddbc..9a2f43bb475b1 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -12,7 +12,7 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, - getdocValueFields, + getDocValueFields, getIndexFields, sourceQuery, DocValueFields, @@ -89,7 +89,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => indices.join(), get('data.source.status.indexFields', result) ), - docValueFields: getdocValueFields( + docValueFields: getDocValueFields( indices.join(), get('data.source.status.indexFields', result) ), diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 2d16892329e19..a6b78269cff2f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -154,7 +154,7 @@ interface AddNewTimelineParams { export const addNewTimeline = ({ columns, dataProviders = [], - dateRange: mayDateRange, + dateRange: maybeDateRange, excludedRowRendererIds = [], filters = timelineDefaults.filters, id, @@ -167,7 +167,7 @@ export const addNewTimeline = ({ timelineType, }: AddNewTimelineParams): TimelineById => { const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' }); - const dateRange = mayDateRange ?? { start: startDateRange, end: endDateRange }; + const dateRange = maybeDateRange ?? { start: startDateRange, end: endDateRange }; const templateTimelineInfo = timelineType === TimelineType.template ? { From f31d592e611d3c13f6846a904d3953693806979c Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 17 Jul 2020 14:35:26 +0200 Subject: [PATCH 39/76] updates 'External alerts' tab text (#72237) --- .../public/detections/pages/detection_engine/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index bfe5dfc012530..92dc02ac8478c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -29,7 +29,7 @@ export const SIGNAL = i18n.translate('xpack.securitySolution.detectionEngine.sig }); export const ALERT = i18n.translate('xpack.securitySolution.detectionEngine.alertTitle', { - defaultMessage: 'External alerts', + defaultMessage: 'Detection alerts', }); export const BUTTON_MANAGE_RULES = i18n.translate( From 45a4393459e0400171564f1d096784ebc97cc8ed Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 17 Jul 2020 13:55:51 +0100 Subject: [PATCH 40/76] skip flaky suite (#72146) --- test/functional/apps/dashboard/dashboard_error_handling.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/dashboard_error_handling.ts b/test/functional/apps/dashboard/dashboard_error_handling.ts index 6bd8327a110b9..38803739ff129 100644 --- a/test/functional/apps/dashboard/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/dashboard_error_handling.ts @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { /** * Common test suite for testing exception scenarious within dashboard */ - describe('dashboard error handling', () => { + // Flaky: https://github.com/elastic/kibana/issues/72146 + describe.skip('dashboard error handling', () => { before(async () => { await esArchiver.loadIfNeeded('dashboard/current/kibana'); await PageObjects.common.navigateToApp('dashboard'); From 93be1cff8d509658ed270bf5843df6311bb2e711 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 17 Jul 2020 08:58:10 -0400 Subject: [PATCH 41/76] [SECURITY] Bug truncation on timeline (#72221) * bring back truncated ceil + only show menu context hover text * update unit test --- .../index.test.tsx | 4 +++- .../drag_and_drop/draggable_wrapper.test.tsx | 4 ++-- .../components/with_hover_actions/index.tsx | 17 ++++++++++------- .../fields_browser/field_name.test.tsx | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx index 9c08e05ddfa39..2af6569394e8f 100644 --- a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx @@ -157,8 +157,10 @@ describe('AddFilterToGlobalSearchBar Component', () => { ); + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); + wrapper.update(); + wrapper - .simulate('mouseenter') .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') .first() .simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index da68280ed760c..e17fc7b9ef9bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -65,7 +65,7 @@ describe('DraggableWrapper', () => { expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); }); - test('it renders hover actions when the mouse is over the draggable wrapper', () => { + test('it renders hover actions when the mouse is over the text of draggable wrapper', () => { const wrapper = mount( @@ -76,7 +76,7 @@ describe('DraggableWrapper', () => { ); - wrapper.simulate('mouseenter'); + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index 97705533689e9..e6577bd040e25 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -17,10 +17,6 @@ const WithHoverActionsPopover = (styled(EuiPopover as any)` } ` as unknown) as typeof EuiPopover; -const Container = styled.div` - width: fit-content; -`; - interface Props { /** * Always show the hover menu contents (default: false) @@ -68,7 +64,14 @@ export const WithHoverActions = React.memo( setShowHoverContent(false); }, []); - const content = useMemo(() => <>{render(showHoverContent)}, [render, showHoverContent]); + const content = useMemo( + () => ( +
+ {render(showHoverContent)} +
+ ), + [onMouseEnter, render, showHoverContent] + ); useEffect(() => { setIsOpen(hoverContent != null && (showHoverContent || alwaysShow)); @@ -79,7 +82,7 @@ export const WithHoverActions = React.memo( }, [closePopOverTrigger]); return ( - +
( > {isOpen ? <>{hoverContent} : null} - +
); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 44e4818830acd..ddd5c6f07e8b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -46,7 +46,7 @@ describe('FieldName', () => {
); - wrapper.find('div').at(1).simulate('mouseenter'); + wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); wrapper.update(); expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); From 44888d3536ae365c7395f07e2757004e8f9cba7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 17 Jul 2020 15:08:38 +0200 Subject: [PATCH 42/76] [Logs UI] Fix display of dataset values in anomaly and category example rows (#71693) This removes an unnecessary JSON encoding step of values before they are passed to the field column component used in the log entry example rows in both the Anomalies and Categories tabs. --- .../top_categories/category_example_message.tsx | 12 ++++-------- .../sections/anomalies/log_entry_example.tsx | 10 +++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 21c7f48eb80f8..908e52f01cbcc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState, useCallback, useContext } from 'react'; +import React, { useState, useCallback, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { encode } from 'rison-node'; import moment from 'moment'; @@ -40,12 +40,8 @@ export const CategoryExampleMessage: React.FunctionComponent<{ context: LogEntryContext; }> = ({ id, dataset, message, timestamp, timeRange, tiebreaker, context }) => { const [, { setContextEntry }] = useContext(ViewLogInContext.Context); - // the dataset must be encoded for the field column and the empty value must - // be turned into a user-friendly value - const encodedDatasetFieldValue = useMemo( - () => JSON.stringify(getFriendlyNameForPartitionId(dataset)), - [dataset] - ); + // handle special cases for the dataset value + const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); const [isHovered, setIsHovered] = useState(false); const setHovered = useCallback(() => setIsHovered(true), []); @@ -100,7 +96,7 @@ export const CategoryExampleMessage: React.FunctionComponent<{ columnValue={{ columnId: datasetColumnId, field: 'event.dataset', - value: encodedDatasetFieldValue, + value: humanFriendlyDataset, highlights: [], }} highlights={noHighlights} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index 2965e1fede822..fece2522de574 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -80,12 +80,8 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ const setItemIsHovered = useCallback(() => setIsHovered(true), []); const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); - // the dataset must be encoded for the field column and the empty value must - // be turned into a user-friendly value - const encodedDatasetFieldValue = useMemo( - () => JSON.stringify(getFriendlyNameForPartitionId(dataset)), - [dataset] - ); + // handle special cases for the dataset value + const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); const viewInStreamLinkProps = useLinkProps({ app: 'logs', @@ -158,7 +154,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ columnValue={{ columnId: datasetColumnId, field: 'event.dataset', - value: encodedDatasetFieldValue, + value: humanFriendlyDataset, highlights: [], }} highlights={noHighlights} From 86629d76b05eb87ded25de66019f0f2d4fe253cb Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 17 Jul 2020 09:24:32 -0400 Subject: [PATCH 43/76] [Ingest Manager] Fix failing test conflict error (#72149) * save kibana installation references after other updates have completed to avoid conflict error * unskip tests * uncomment out line * add back await to not change things * unskip fleet_unenroll_agent Co-authored-by: Elastic Machine --- .../services/epm/kibana/assets/install.ts | 22 ++++++++++--------- .../server/services/epm/packages/install.ts | 9 +++++--- .../test/api_integration/apis/fleet/setup.ts | 3 +-- .../apis/fleet/unenroll_agent.ts | 3 +-- .../apis/epm/install.ts | 3 +-- .../apps/endpoint/policy_details.ts | 3 +-- .../apps/endpoint/policy_list.ts | 3 +-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index 2a743f244e64d..a3fe444b19b1a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,10 +11,14 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { + AssetType, + KibanaAssetType, + AssetReference, + KibanaAssetReference, +} from '../../../../types'; import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; import { getInstallationObject, savedObjectTypes } from '../../packages'; -import { saveInstalledKibanaRefs } from '../../packages/install'; type SavedObjectToBe = Required & { type: AssetType }; export type ArchiveAsset = Pick< @@ -49,7 +53,7 @@ export async function installKibanaAssets(options: { pkgName: string; paths: string[]; isUpdate: boolean; -}): Promise { +}): Promise { const { savedObjectsClient, paths, pkgName, isUpdate } = options; if (isUpdate) { @@ -65,16 +69,14 @@ export async function installKibanaAssets(options: { // install the new assets and save installation references const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + const installedAssets = await Promise.all( + kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + ) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] // call .flat to flatten into one dimensional array - const newInstalledKibanaAssets = await Promise.all(installationPromises).then((results) => - results.flat() - ); - await saveInstalledKibanaRefs(savedObjectsClient, pkgName, newInstalledKibanaAssets); - return newInstalledKibanaAssets; + return installedAssets.flat(); } export const deleteKibanaInstalledRefs = async ( savedObjectsClient: SavedObjectsClientContract, 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 cd4259a0c30d7..a69daae6e0410 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 @@ -169,17 +169,20 @@ export async function installPackage(options: { ); } - // update to newly installed version when all assets are successfully installed - if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); + const [installedKibanaAssets] = await Promise.all([ installKibanaAssetsPromise, installIndexPatternPromise, ]); + + await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets); + // update to newly installed version when all assets are successfully installed + if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; } const updateVersion = async ( @@ -230,7 +233,7 @@ export async function createInstallation(options: { export const saveInstalledKibanaRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: AssetReference[] + installedAssets: KibanaAssetReference[] ) => { await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_kibana: installedAssets, diff --git a/x-pack/test/api_integration/apis/fleet/setup.ts b/x-pack/test/api_integration/apis/fleet/setup.ts index 82e494b0ab28c..4fcf39886e202 100644 --- a/x-pack/test/api_integration/apis/fleet/setup.ts +++ b/x-pack/test/api_integration/apis/fleet/setup.ts @@ -11,8 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/72053 - describe.skip('fleet_setup', () => { + describe('fleet_setup', () => { beforeEach(async () => { try { await es.security.deleteUser({ diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index 4e1443ad1fc68..bc6c44e590cc4 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,8 +16,7 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/64696 - describe.skip('fleet_unenroll_agent', () => { + describe('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts index b6807b2fd3414..f73ba56c172c4 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts @@ -21,8 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const mappingsPackage = 'overrides-0.1.0'; const server = dockerServers.get('registry'); - // FLAKY: https://github.com/elastic/kibana/issues/71939 - describe.skip('installs packages that include settings and mappings overrides', async () => { + describe('installs packages that include settings and mappings overrides', async () => { after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index d57bd32441454..cf76f297d83be 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -19,8 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - // FLAKY: https://github.com/elastic/kibana/issues/72102 - describe.skip('When on the Endpoint Policy Details Page', function () { + describe('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); describe('with an invalid policy id', () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index b91f0647487ff..57321ab4cd911 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -19,8 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const RELATIVE_DATE_FORMAT = /\d (?:seconds|minutes) ago/i; - // FLAKY: https://github.com/elastic/kibana/issues/71951 - describe.skip('When on the Endpoint Policy List', function () { + describe('When on the Endpoint Policy List', function () { this.tags(['ciGroup7']); before(async () => { await pageObjects.policy.navigateToPolicyList(); From 5160c7ee459b564b6b116790f3728778d53dfab7 Mon Sep 17 00:00:00 2001 From: Tre Date: Fri, 17 Jul 2020 07:56:59 -0600 Subject: [PATCH 44/76] [QA][Code Coverage] Drop flaky integration tests (#72089) --- .../integration_tests/ingest_coverage.test.js | 92 ------------------- 1 file changed, 92 deletions(-) diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index 95056d9f0d8d7..ba73922ec508a 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -69,96 +69,4 @@ describe('Ingesting coverage', () => { expect(folderStructure.test(actualUrl)).ok(); }); }); - describe(`vcsInfo`, () => { - let stdOutWithVcsInfo = ''; - describe(`without a commit msg in the vcs info file`, () => { - beforeAll(async () => { - const args = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt', - '--path', - ]; - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - stdOutWithVcsInfo = stdout; - }); - - it(`should be an obj w/o a commit msg`, () => { - const commitMsgRE = /"commitMsg"/; - expect(commitMsgRE.test(stdOutWithVcsInfo)).to.not.be.ok(); - }); - }); - describe(`including previous sha`, () => { - let stdOutWithPrevious = ''; - beforeAll(async () => { - const opts = [...verboseArgs, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - stdOutWithPrevious = stdout; - }); - - it(`should have a vcsCompareUrl`, () => { - const previousCompareUrlRe = /vcsCompareUrl.+\s*.*https.+compare\/FAKE_PREVIOUS_SHA\.\.\.f07b34f6206/; - expect(previousCompareUrlRe.test(stdOutWithPrevious)).to.be.ok(); - }); - }); - describe(`with a commit msg in the vcs info file`, () => { - beforeAll(async () => { - const args = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', - '--path', - ]; - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - stdOutWithVcsInfo = stdout; - }); - - it(`should be an obj w/ a commit msg`, () => { - const commitMsgRE = /commitMsg/; - expect(commitMsgRE.test(stdOutWithVcsInfo)).to.be.ok(); - }); - }); - }); - describe(`team assignment`, () => { - let shouldNotHavePipelineOut = ''; - let shouldIndeedHavePipelineOut = ''; - - const args = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', - '--path', - ]; - - const teamAssignRE = /pipeline:/; - - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-just-total.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldNotHavePipelineOut = stdout; - }); - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldIndeedHavePipelineOut = stdout; - }); - - it(`should not occur when going to the totals index`, () => { - const actual = teamAssignRE.test(shouldNotHavePipelineOut); - expect(actual).to.not.be.ok(); - }); - it(`should indeed occur when going to the coverage index`, () => { - const actual = /ingest-pipe=>team_assignment/.test(shouldIndeedHavePipelineOut); - expect(actual).to.be.ok(); - }); - }); }); From f9baaa267d75558decd7ada45258d4ecced182e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 17 Jul 2020 15:11:41 +0100 Subject: [PATCH 45/76] Observability landing page title (#72088) * updating window title based on breadcrumbs * updating window title based on breadcrumbs * updating window title based on breadcrumbs --- .../public/application/index.tsx | 35 ++++++++++++------- .../observability/public/routes/index.tsx | 2 ++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 8cfbca37e8d05..d76c033a41756 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -3,18 +3,31 @@ * 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 { createHashHistory } from 'history'; import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; import { PluginContext } from '../context/plugin_context'; -import { useRouteParams } from '../hooks/use_route_params'; -import { routes } from '../routes'; import { usePluginContext } from '../hooks/use_plugin_context'; +import { useRouteParams } from '../hooks/use_route_params'; +import { Breadcrumbs, routes } from '../routes'; + +const observabilityLabelBreadcrumb = { + text: i18n.translate('xpack.observability.observability.breadcrumb.', { + defaultMessage: 'Observability', + }), +}; + +function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) { + return breadcrumbs + .map(({ text }) => text) + .reverse() + .join(' | '); +} const App = () => { return ( @@ -25,16 +38,12 @@ const App = () => { const route = routes[path]; const Wrapper = () => { const { core } = usePluginContext(); + + const breadcrumb = [observabilityLabelBreadcrumb, ...route.breadcrumb]; useEffect(() => { - core.chrome.setBreadcrumbs([ - { - text: i18n.translate('xpack.observability.observability.breadcrumb.', { - defaultMessage: 'Observability', - }), - }, - ...route.breadcrumb, - ]); - }, [core]); + core.chrome.setBreadcrumbs(breadcrumb); + document.title = getTitleFromBreadCrumbs(breadcrumb); + }, [core, breadcrumb]); const { query, path: pathParams } = useRouteParams(route.params); return route.handler({ query, path: pathParams }); diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 10f9b4dc42723..bee6a4dd7133a 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -17,6 +17,8 @@ type DecodeParams = { [key in keyof TParams]: TParams[key] extends t.Any ? t.TypeOf : never; }; +export type Breadcrumbs = Array<{ text: string }>; + export interface Params { query?: t.HasProps; path?: t.HasProps; From 937314ad1111d904e9538015d2676368acb0ac54 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 17 Jul 2020 17:14:55 +0300 Subject: [PATCH 46/76] Bump @elastic/elasticsearch to v7.9.0-rc1 (#72231) * bump @elastic/elasticsearch to 7.9.0-rc1 * bump other packages using @elastic/elasticsearch --- package.json | 2 +- packages/kbn-es/package.json | 2 +- x-pack/plugins/apm/scripts/package.json | 2 +- yarn.lock | 35 ++++--------------------- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 593e8f82f0541..a22871e314bae 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@elastic/apm-rum": "^5.2.0", "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "7.8.0", + "@elastic/elasticsearch": "7.9.0-rc.1", "@elastic/ems-client": "7.9.3", "@elastic/eui": "26.3.1", "@elastic/filesaver": "1.1.2", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 271b4a3dc661b..f53eb694ec712 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "private": true, "dependencies": { - "@elastic/elasticsearch": "^7.4.0", + "@elastic/elasticsearch": "7.9.0-rc.1", "@kbn/dev-utils": "1.0.0", "abort-controller": "^2.0.3", "chalk": "^2.4.2", diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index c5a9df792f856..4d0906514b5e1 100644 --- a/x-pack/plugins/apm/scripts/package.json +++ b/x-pack/plugins/apm/scripts/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "dependencies": { - "@elastic/elasticsearch": "^7.6.1", + "@elastic/elasticsearch": "7.9.0-rc.1", "@octokit/rest": "^16.35.0", "@types/console-stamp": "^0.2.32", "console-stamp": "^0.2.9", diff --git a/yarn.lock b/yarn.lock index a5066b51f3198..580e436a60282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2172,10 +2172,10 @@ redux-immutable-state-invariant "^2.1.0" redux-logger "^3.0.6" -"@elastic/elasticsearch@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.8.0.tgz#3f9ee54fe8ef79874ebd231db03825fa500a7111" - integrity sha512-rUOTNN1At0KoN0Fcjd6+J7efghuURnoMTB/od9EMK6Mcdebi6N3z5ulShTsKRn6OanS9Eq3l/OmheQY1Y+WLcg== +"@elastic/elasticsearch@7.9.0-rc.1": + version "7.9.0-rc.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.9.0-rc.1.tgz#50205507ec84ccb95cb7a6d36e5570808749fee9" + integrity sha512-rVjiVj7VPLCusJPfywpb3gvcaA99uylYSum1Frcq4vi2Iqg118KXgYW6GOis2Y70oDZ6w6XRlT0ze5NA6SBa+g== dependencies: debug "^4.1.1" decompress-response "^4.2.0" @@ -2183,18 +2183,6 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@elastic/elasticsearch@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.4.0.tgz#57f4066acf25e9d4e9b4f6376088433aae6f25d4" - integrity sha512-HpEKHH6mHQRvea3lw4NNJw9ZUS1KmkpwWKHucaHi1svDn+/fEAwY0wD8egL1vZJo4ZmWfCQMjVqGL+Hoy1HYRw== - dependencies: - debug "^4.1.1" - decompress-response "^4.2.0" - into-stream "^5.1.0" - ms "^2.1.1" - once "^1.4.0" - pump "^3.0.0" - "@elastic/ems-client@7.9.3": version "7.9.3" resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.9.3.tgz#71b79914f76e347f050ead8474ad65d761e94a8a" @@ -15294,7 +15282,7 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0, from2@^2.1.1, from2@^2.3.0: +from2@^2.1.0, from2@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -18081,14 +18069,6 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" -into-stream@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.0.tgz#b05f37d8fed05c06a0b43b556d74e53e5af23878" - integrity sha512-cbDhb8qlxKMxPBk/QxTtYg1DQ4CwXmadu7quG3B7nrJsgSncEreF2kwWKZFdnjc/lSNNIkFPsjI7SM0Cx/QXPw== - dependencies: - from2 "^2.3.0" - p-is-promise "^2.0.0" - invariant@2.2.4, invariant@^2.1.0, invariant@^2.1.1, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -23776,11 +23756,6 @@ p-is-promise@^1.1.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" From 8f442f8318dfe0613fd51660cc7b4370b3ca2a29 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 17 Jul 2020 16:37:10 +0200 Subject: [PATCH 47/76] [ML] Fix HTML named characters encoding (#72060) * [ML] improve special characters encoding * [ML] update renovate.json5 --- renovate.json5 | 8 +++++++ x-pack/package.json | 2 ++ .../application/util/string_utils.test.ts | 7 +++++-- .../public/application/util/string_utils.ts | 21 +++++++++++-------- yarn.lock | 7 ++++++- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index 6424894622c9f..ae32043daaf5f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -418,6 +418,14 @@ '@types/has-ansi', ], }, + { + groupSlug: 'he', + groupName: 'he related packages', + packageNames: [ + 'he', + '@types/he', + ], + }, { groupSlug: 'history', groupName: 'history related packages', diff --git a/x-pack/package.json b/x-pack/package.json index 6715fa132c1b5..1de009ae1232f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -72,6 +72,7 @@ "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", + "@types/he": "^1.1.1", "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", "@types/jest": "^25.2.3", @@ -265,6 +266,7 @@ "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", "handlebars": "4.7.6", + "he": "^1.2.0", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts index d7ed30065219a..80e318629d352 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -126,8 +126,11 @@ describe('ML - string utils', () => { expect(mlEscape('foobar')).toBe('foo>bar'); expect(mlEscape('foo"bar')).toBe('foo"bar'); - expect(mlEscape("foo'bar")).toBe('foo'bar'); - expect(mlEscape('foo/bar')).toBe('foo/bar'); + expect(mlEscape("foo'bar")).toBe('foo'bar'); + expect(mlEscape('foo/bar')).toBe('foo/bar'); + expect(mlEscape('escape © everything ≠ / 𝌆 \\')).toBe( + 'escape © everything ≠ / �� \' + ); }); }); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.ts b/x-pack/plugins/ml/public/application/util/string_utils.ts index f659f8fdc1be1..55dd16082a07c 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -9,6 +9,7 @@ */ import _ from 'lodash'; import d3 from 'd3'; +import he from 'he'; import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls'; import { Detector } from '../../../common/types/anomaly_detection_jobs'; @@ -105,15 +106,17 @@ export function toLocaleString(x: number | undefined | null): string { // escape html characters export function mlEscape(str: string): string { - const entityMap: { [escapeChar: string]: string } = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - }; - return String(str).replace(/[&<>"'\/]/g, (s) => entityMap[s]); + // It's not possible to use "he" encoding directly + // because \ and / characters are not going to be replaced without + // encodeEverything option. But with this option enabled + // each word character is encoded as well. + return String(str).replace(/\W/g, (s) => + he.encode(s, { + useNamedReferences: true, + encodeEverything: true, + allowUnsafeSymbols: false, + }) + ); } // Escapes reserved characters for use in Elasticsearch query terms. diff --git a/yarn.lock b/yarn.lock index 580e436a60282..e5975efe0b7d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5141,6 +5141,11 @@ resolved "https://registry.yarnpkg.com/@types/has-ansi/-/has-ansi-3.0.0.tgz#636403dc4e0b2649421c4158e5c404416f3f0330" integrity sha512-H3vFOwfLlFEC0MOOrcSkus8PCnMCzz4N0EqUbdJZCdDhBTfkAu86aRYA+MTxjKW6jCpUvxcn4715US8g+28BMA== +"@types/he@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/he/-/he-1.1.1.tgz#19e14033c4ee8f1a702c74dcc6182664839ac2b7" + integrity sha512-jpzrsR1ns0n3kyWt92QfOUQhIuJGQ9+QGa7M62rO6toe98woQjnsnzjdMtsQXCdvjjmqjS2ZBCC7xKw0cdzU+Q== + "@types/history@*": version "4.7.2" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" @@ -17063,7 +17068,7 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@1.2.0, he@1.2.x, he@^1.1.1: +he@1.2.0, he@1.2.x, he@^1.1.1, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== From ac9178ab9a2537ce2468593d86d4c21b622db9cf Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 17 Jul 2020 16:41:44 +0200 Subject: [PATCH 48/76] updates advanced settings text (#72249) --- .../detections/components/rules/step_about_rule/schema.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index f178923df5915..fbd03850eee75 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -34,7 +34,7 @@ export const schema: FormSchema = { 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorHelpText', { defaultMessage: - 'Type one or more author for this rule. Press enter after each author to add a new one.', + 'Type one or more authors for this rule. Press enter after each author to add a new one.', } ), labelAppend: OptionalFieldLabel, @@ -280,7 +280,7 @@ export const schema: FormSchema = { 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText', { defaultMessage: - 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from alerts generated by this rule.', + 'Provide helpful information for analysts that are investigating detection alerts. This guide will appear on the rule details page and in timelines (as notes) created from detection alerts generated by this rule.', } ), labelAppend: OptionalFieldLabel, From 260eb139f86f2621edc7fd85417ca0aa71c82955 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 17 Jul 2020 10:56:02 -0400 Subject: [PATCH 49/76] re-enable tests. retry on fail (#72061) --- .../components/configuration_step/analysis_fields_table.tsx | 1 + .../test/functional/apps/ml/data_frame_analytics/index.ts | 3 +-- .../functional/services/ml/data_frame_analytics_creation.ts | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index ff8797bc523c1..a229a79d316d7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -172,6 +172,7 @@ export const AnalysisFieldsTable: FC<{ return ( { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardIncludesSelect'); + }); }, // async assertIncludedFieldsSelection(expectedSelection: string[]) { From 6a03e8b5b8b54c2f96e1fefb41cd29d81ecf22df Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 17 Jul 2020 11:08:57 -0400 Subject: [PATCH 50/76] Add a few asciidoc readmes (#72082) * Add a few asciidoc readmes * add updated code-exploration. Need to fix the script in another go to add asciidoc support. The snippet didn't show up anyway. --- docs/developer/architecture/code-exploration.asciidoc | 5 ++++- examples/{README.md => README.asciidoc} | 8 +++++--- src/dev/precommit_hook/casing_check_config.js | 1 + src/plugins/dashboard/README.asciidoc | 5 +++++ x-pack/plugins/embeddable_enhanced/README.asciidoc | 6 ++++++ x-pack/plugins/embeddable_enhanced/README.md | 1 - 6 files changed, 21 insertions(+), 5 deletions(-) rename examples/{README.md => README.asciidoc} (68%) create mode 100644 src/plugins/dashboard/README.asciidoc create mode 100644 x-pack/plugins/embeddable_enhanced/README.asciidoc delete mode 100644 x-pack/plugins/embeddable_enhanced/README.md diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index 23ba1c54d27d3..bed54277c82b4 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -365,7 +365,10 @@ WARNING: Missing README. WARNING: Missing README. -- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced/README.md[embeddableEnhanced] +- {kib-repo}blob/{branch}/x-pack/plugins/embeddable_enhanced[embeddableEnhanced] + +WARNING: Missing README. + - {kib-repo}blob/{branch}/x-pack/plugins/encrypted_saved_objects/README.md[encryptedSavedObjects] diff --git a/examples/README.md b/examples/README.asciidoc similarity index 68% rename from examples/README.md rename to examples/README.asciidoc index 2b214a8d1eb52..d33c5e825ce12 100644 --- a/examples/README.md +++ b/examples/README.asciidoc @@ -1,7 +1,9 @@ -## Example plugins +[[example-plugins]] +== Example plugins This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag, via -``` +[source,bash] +---- yarn start --run-examples -``` +---- diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 6b1f1dfaeabb4..929de8c6701d4 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -30,6 +30,7 @@ export const IGNORE_FILE_GLOBS = [ 'docs/**/*', '**/bin/**/*', '**/+([A-Z_]).md', + '**/+([A-Z_]).asciidoc', '**/LICENSE', '**/*.txt', '**/Gruntfile.js', diff --git a/src/plugins/dashboard/README.asciidoc b/src/plugins/dashboard/README.asciidoc new file mode 100644 index 0000000000000..78163b14c14d6 --- /dev/null +++ b/src/plugins/dashboard/README.asciidoc @@ -0,0 +1,5 @@ +[[kibana-dashboard-plugin]] +== Dashboard plugin + +- Registers the dashboard application. +- Adds a dashboard embeddable that can be used in other applications. \ No newline at end of file diff --git a/x-pack/plugins/embeddable_enhanced/README.asciidoc b/x-pack/plugins/embeddable_enhanced/README.asciidoc new file mode 100644 index 0000000000000..9a7fe9c2669d9 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/README.asciidoc @@ -0,0 +1,6 @@ +[[enhanced-embeddables-plugin]] +== Enhanced embeddables plugin + +Enhances Embeddables by registering a custom factory provider. The enhanced factory provider +adds dynamic actions to every embeddables state, in order to support drilldowns. + diff --git a/x-pack/plugins/embeddable_enhanced/README.md b/x-pack/plugins/embeddable_enhanced/README.md deleted file mode 100644 index a0be90731fdb0..0000000000000 --- a/x-pack/plugins/embeddable_enhanced/README.md +++ /dev/null @@ -1 +0,0 @@ -# X-Pack part of `embeddable` plugin From 2ad4328fda8a5eb35a3a82bfb010a27202f196a2 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 17 Jul 2020 17:09:18 +0200 Subject: [PATCH 51/76] [ML] Remove DragSelect event handlers and selectors on the swim lane unmount (#72250) * [ML] remove selector element on unmount * [ML] stop handler on mount * [ML] remove throttling --- .../application/explorer/explorer_swimlane.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx index 926f38ac8b552..2590ab2f1cb23 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx @@ -204,6 +204,8 @@ export class ExplorerSwimlane extends React.Component { }); this.renderSwimlane(); + + this.dragSelect.stop(); } componentDidUpdate() { @@ -211,11 +213,11 @@ export class ExplorerSwimlane extends React.Component { } componentWillUnmount() { - if (this.dragSelectSubscriber !== null) { - this.dragSelectSubscriber.unsubscribe(); - } - const element = d3.select(this.rootNode.current!); - element.html(''); + this.dragSelectSubscriber!.unsubscribe(); + // Remove selector element from DOM + this.dragSelect.selector.remove(); + // removes all mousedown event handlers + this.dragSelect.stop(true); } selectCell(cellsToSelect: any[], { laneLabels, bucketScore, times }: SelectedData) { From 39381ca3c849a9c9b3d486d9552f51e52cb942d9 Mon Sep 17 00:00:00 2001 From: igoristic Date: Fri, 17 Jul 2020 11:13:40 -0400 Subject: [PATCH 52/76] [Monitoring] Added a case for Alerting if security/ssl is disabled (#71846) * Added a case for Alerting if security/ssl is disabled * Code feedback * Fixed types --- x-pack/plugins/monitoring/kibana.json | 3 +- .../public/alerts/lib/security_toasts.tsx | 137 ++++++++++++++++++ .../monitoring/public/services/clusters.js | 4 +- .../elasticsearch/verify_alerting_security.ts | 49 +++++++ x-pack/plugins/monitoring/server/plugin.ts | 1 + .../server/routes/api/v1/alerts/enable.ts | 25 +++- x-pack/plugins/monitoring/server/types.ts | 3 + 7 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx create mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 3b9e60124b034..2b8756ea0cb46 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -11,7 +11,8 @@ "kibanaLegacy", "triggers_actions_ui", "alerts", - "actions" + "actions", + "encryptedSavedObjects" ], "optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], "server": true, diff --git a/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx new file mode 100644 index 0000000000000..918c0b5c9b609 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx @@ -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. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiLink, EuiCode, EuiText } from '@elastic/eui'; +import { Legacy } from '../../legacy_shims'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +export interface AlertingFrameworkHealth { + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + +const showTlsAndEncryptionError = () => { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + + Legacy.shims.toastNotifications.addWarning({ + title: toMountPoint( + + ), + text: toMountPoint( +
+

+ {i18n.translate('xpack.monitoring.healthCheck.tlsAndEncryptionError', { + defaultMessage: `You must enable Transport Layer Security between Kibana and Elasticsearch + and configure an encryption key in your kibana.yml file to use the Alerting feature.`, + })} +

+ + + {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', { + defaultMessage: 'Learn how.', + })} + +
+ ), + }); +}; + +const showEncryptionError = () => { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + + Legacy.shims.toastNotifications.addWarning( + { + title: toMountPoint( + + ), + text: toMountPoint( +
+ {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorBeforeKey', { + defaultMessage: 'To create an alert, set a value for ', + })} + + {'xpack.encryptedSavedObjects.encryptionKey'} + + {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAfterKey', { + defaultMessage: ' in your kibana.yml file. ', + })} + + {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', { + defaultMessage: 'Learn how.', + })} + +
+ ), + }, + {} + ); +}; + +const showTlsError = () => { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + + Legacy.shims.toastNotifications.addWarning({ + title: toMountPoint( + + ), + text: toMountPoint( +
+ {i18n.translate('xpack.monitoring.healthCheck.tlsError', { + defaultMessage: + 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', + })} + + {i18n.translate('xpack.monitoring.healthCheck.tlsErrorAction', { + defaultMessage: 'Learn how to enable TLS.', + })} + +
+ ), + }); +}; + +export const showSecurityToast = (alertingHealth: AlertingFrameworkHealth) => { + const { isSufficientlySecure, hasPermanentEncryptionKey } = alertingHealth; + if ( + Array.isArray(alertingHealth) || + (!alertingHealth.hasOwnProperty('isSufficientlySecure') && + !alertingHealth.hasOwnProperty('hasPermanentEncryptionKey')) + ) { + return; + } + + if (!isSufficientlySecure && !hasPermanentEncryptionKey) { + showTlsAndEncryptionError(); + } else if (!isSufficientlySecure) { + showTlsError(); + } else if (!hasPermanentEncryptionKey) { + showEncryptionError(); + } +}; diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index f3eadcaf9831b..5173984dbe868 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -7,6 +7,7 @@ import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; import { Legacy } from '../legacy_shims'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; +import { showSecurityToast } from '../alerts/lib/security_toasts'; function formatClusters(clusters) { return clusters.map(formatCluster); @@ -66,7 +67,8 @@ export function monitoringClustersProvider($injector) { return getClusters().then((clusters) => { if (clusters.length) { return ensureAlertsEnabled() - .then(() => { + .then(({ data }) => { + showSecurityToast(data); once = true; return clusters; }) diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts new file mode 100644 index 0000000000000..047b14bd37fbc --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.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 { RequestHandlerContext } from 'kibana/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; + +export interface AlertingFrameworkHealth { + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + +export interface XPackUsageSecurity { + security?: { + enabled?: boolean; + ssl?: { + http?: { + enabled?: boolean; + }; + }; + }; +} + +export class AlertingSecurity { + public static readonly getSecurityHealth = async ( + context: RequestHandlerContext, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup + ): Promise => { + const { + security: { + enabled: isSecurityEnabled = false, + ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, + } = {}, + }: XPackUsageSecurity = await context.core.elasticsearch.legacy.client.callAsInternalUser( + 'transport.request', + { + method: 'GET', + path: '/_xpack/usage', + } + ); + + return { + isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), + hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey, + }; + }; +} diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 5f358badde401..39ec5fe1ffaa7 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -203,6 +203,7 @@ export class Plugin { requireUIRoutes(this.monitoringCore, { router, licenseService: this.licenseService, + encryptedSavedObjects: plugins.encryptedSavedObjects, }); initInfraSource(config, plugins.infra); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index b7cc088d2716c..64beb5c58dc07 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -10,18 +10,36 @@ import { AlertsFactory } from '../../../../alerts'; import { RouteDependencies } from '../../../../types'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; import { ActionResult } from '../../../../../../actions/common'; -// import { fetchDefaultEmailAddress } from '../../../../lib/alerts/fetch_default_email_address'; +import { AlertingSecurity } from '../../../../lib/elasticsearch/verify_alerting_security'; const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log'; -export function enableAlertsRoute(server: any, npRoute: RouteDependencies) { +export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alerts/enable', validate: false, }, - async (context, request, response) => { + async (context, _request, response) => { try { + const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); + + if (alerts.length) { + const { + isSufficientlySecure, + hasPermanentEncryptionKey, + } = await AlertingSecurity.getSecurityHealth(context, npRoute.encryptedSavedObjects); + + if (!isSufficientlySecure || !hasPermanentEncryptionKey) { + return response.ok({ + body: { + isSufficientlySecure, + hasPermanentEncryptionKey, + }, + }); + } + } + const alertsClient = context.alerting?.getAlertsClient(); const actionsClient = context.actions?.getActionsClient(); const types = context.actions?.listTypes(); @@ -57,7 +75,6 @@ export function enableAlertsRoute(server: any, npRoute: RouteDependencies) { }, ]; - const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); const createdAlerts = await Promise.all( alerts.map( async (alert) => await alert.createIfDoesNotExist(alertsClient, actionsClient, actions) diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0c346c8082475..1e7a5acb33644 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -16,6 +16,7 @@ import { import { InfraPluginSetup } from '../../infra/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -36,6 +37,7 @@ export interface LegacyAPI { } export interface PluginsSetup { + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; @@ -56,6 +58,7 @@ export interface MonitoringCoreConfig { export interface RouteDependencies { router: IRouter; licenseService: MonitoringLicenseService; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; } export interface MonitoringCore { From 825c16875ea9dab3168c8177abde685d808250d1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Jul 2020 17:16:28 +0200 Subject: [PATCH 53/76] register graph usage (#72041) --- x-pack/plugins/graph/server/lib/license_state.ts | 13 +++++++++++++ x-pack/plugins/graph/server/plugin.ts | 10 +++++++--- x-pack/plugins/graph/server/routes/explore.ts | 1 + x-pack/plugins/graph/server/routes/search.ts | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/graph/server/lib/license_state.ts b/x-pack/plugins/graph/server/lib/license_state.ts index d86cb5380a2e1..8d64c826d8fa1 100644 --- a/x-pack/plugins/graph/server/lib/license_state.ts +++ b/x-pack/plugins/graph/server/lib/license_state.ts @@ -7,6 +7,7 @@ import Boom from 'boom'; import { map } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; +import { LicensingPluginStart } from '../../../licensing/server'; import { ILicense } from '../../../licensing/common/types'; import { checkLicense, GraphLicenseInformation } from '../../common/check_license'; @@ -14,6 +15,7 @@ export class LicenseState { private licenseInformation: GraphLicenseInformation = checkLicense(undefined); private subscription: Subscription | null = null; private observable: Observable | null = null; + private _notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage'] | null = null; private updateInformation(licenseInformation: GraphLicenseInformation) { this.licenseInformation = licenseInformation; @@ -24,6 +26,17 @@ export class LicenseState { this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); } + public setNotifyUsage(notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage']) { + this._notifyUsage = notifyUsage; + } + + // 'Graph' is the only allowed feature here at the moment, if this gets extended in the future, add to the union type + public notifyUsage(featureName: 'Graph') { + if (this._notifyUsage) { + this._notifyUsage(featureName); + } + } + public stop() { if (this.subscription) { this.subscription.unsubscribe(); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 141d5d0ea8db4..b2b825fa4683b 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { Plugin, CoreSetup } from 'src/core/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LicenseState } from './lib/license_state'; import { registerSearchRoute } from './routes/search'; import { registerExploreRoute } from './routes/explore'; @@ -34,6 +34,7 @@ export class GraphPlugin implements Plugin { licenseState.start(licensing.license$); this.licenseState = licenseState; core.savedObjects.registerType(graphWorkspace); + licensing.featureUsage.register('Graph', 'platinum'); if (home) { registerSampleData(home.sampleData, licenseState); @@ -79,7 +80,10 @@ export class GraphPlugin implements Plugin { registerExploreRoute({ licenseState, router }); } - public start() {} + public start(core: CoreStart, { licensing }: { licensing: LicensingPluginStart }) { + this.licenseState!.setNotifyUsage(licensing.featureUsage.notifyUsage); + } + public stop() { if (this.licenseState) { this.licenseState.stop(); diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index b0b8cf14ff699..c436fbd1c79af 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -42,6 +42,7 @@ export function registerExploreRoute({ response ) => { verifyApiAccess(licenseState); + licenseState.notifyUsage('Graph'); try { return response.ok({ body: { diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 645e6b520013f..e1d430eeb311a 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -42,6 +42,7 @@ export function registerSearchRoute({ response ) => { verifyApiAccess(licenseState); + licenseState.notifyUsage('Graph'); const includeFrozen = await uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); try { return response.ok({ From 44fc2a828c755f77e14713c0484e541cfcecaf1b Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 17 Jul 2020 12:04:18 -0400 Subject: [PATCH 54/76] Fix indentation level in code exploration doc (#72274) * fix indentation level in code exploration doc * run the script to update the file --- docs/developer/architecture/code-exploration.asciidoc | 8 ++++---- .../kbn-dev-utils/src/plugin_list/generate_plugin_list.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index bed54277c82b4..2f67ae002c916 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -11,7 +11,7 @@ NOTE: //// [[code-exploration]] -=== Exploring Kibana code +== Exploring Kibana code The goals of our folder heirarchy are: @@ -28,10 +28,10 @@ To that aim, we strive to: [discrete] [[kibana-services-applications]] -==== Services and Applications +=== Services and Applications [discrete] -===== src/plugins +==== src/plugins - {kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] @@ -283,7 +283,7 @@ WARNING: Missing README. [discrete] -===== x-pack/plugins +==== x-pack/plugins - {kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] diff --git a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts index f3f8817299bb1..f0f799862e24e 100644 --- a/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts +++ b/packages/kbn-dev-utils/src/plugin_list/generate_plugin_list.ts @@ -54,7 +54,7 @@ NOTE: //// [[code-exploration]] -=== Exploring Kibana code +== Exploring Kibana code The goals of our folder heirarchy are: @@ -71,14 +71,14 @@ To that aim, we strive to: [discrete] [[kibana-services-applications]] -==== Services and Applications +=== Services and Applications [discrete] -===== src/plugins +==== src/plugins ${Array.from(printPlugins(ossPlugins)).join('\n')} [discrete] -===== x-pack/plugins +==== x-pack/plugins ${Array.from(printPlugins(xpackPlugins)).join('\n')} `; } From 1adaa3b76c16e4f02bd9ee98ff181eaea9c24566 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 17 Jul 2020 12:39:51 -0400 Subject: [PATCH 55/76] [Security Solution][Exceptions] - Remove initial add exception item button in builder (#72215) ## Summary This PR addresses two issues in the builder: - **Existing behavior:** if you add a bunch of entries then delete all but one, the indent that shows for when multiple entries exists does not go away - **Updated behavior:** if you add a bunch of entries and delete all but one, the indent that shows for when multiple entries exist goes away - **Existing behavior:** on render of add exception modal, if no exception items exist (or no exception items with entries exist) an `Add Exception` button appears - **Updated behavior:** if only one entry exists, the delete button is disabled for that entry; on initial render of the add exception modal, if no entries exist, an empty entry is shown --- .../builder_button_options.stories.tsx | 17 -- .../builder/builder_button_options.test.tsx | 44 --- .../builder/builder_button_options.tsx | 76 ++--- .../builder/builder_exception_item.test.tsx | 282 ++++++++++++++++++ .../builder/builder_exception_item.tsx | 161 ++++++++++ .../exceptions/builder/exception_item.tsx | 140 --------- .../components/exceptions/builder/index.tsx | 48 ++- .../components/exceptions/translations.ts | 7 - 8 files changed, 493 insertions(+), 282 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx index 7e4cbe34f9a64..9486008e708ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx @@ -16,26 +16,12 @@ addDecorator((storyFn) => ( )); storiesOf('Components|Exceptions|BuilderButtonOptions', module) - .add('init button', () => { - return ( - - ); - }) .add('and/or buttons', () => { return ( { isAndDisabled={false} isOrDisabled={false} showNestedButton={false} - displayInitButton={false} onOrClicked={jest.fn()} onAndClicked={jest.fn()} onNestedClicked={jest.fn()} @@ -31,44 +30,6 @@ describe('BuilderButtonOptions', () => { expect(wrapper.find('[data-test-subj="exceptionsNestedButton"] button')).toHaveLength(0); }); - test('it renders "add exception" button if "displayInitButton" is true', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button')).toHaveLength( - 1 - ); - }); - - test('it invokes "onAddExceptionClicked" when "add exception" button is clicked', () => { - const onOrClicked = jest.fn(); - - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button').simulate('click'); - - expect(onOrClicked).toHaveBeenCalledTimes(1); - }); - test('it invokes "onOrClicked" when "or" button is clicked', () => { const onOrClicked = jest.fn(); @@ -77,7 +38,6 @@ describe('BuilderButtonOptions', () => { isAndDisabled={false} isOrDisabled={false} showNestedButton={false} - displayInitButton={false} onOrClicked={onOrClicked} onAndClicked={jest.fn()} onNestedClicked={jest.fn()} @@ -97,7 +57,6 @@ describe('BuilderButtonOptions', () => { isAndDisabled={false} isOrDisabled={false} showNestedButton={false} - displayInitButton={false} onOrClicked={jest.fn()} onAndClicked={onAndClicked} onNestedClicked={jest.fn()} @@ -113,7 +72,6 @@ describe('BuilderButtonOptions', () => { const wrapper = mount( { const wrapper = mount( { isAndDisabled={false} isOrDisabled={false} showNestedButton - displayInitButton={false} onOrClicked={jest.fn()} onAndClicked={jest.fn()} onNestedClicked={onNestedClicked} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx index ff1556bcc4d25..eb224b82d756f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx @@ -16,7 +16,6 @@ const MyEuiButton = styled(EuiButton)` interface BuilderButtonOptionsProps { isOrDisabled: boolean; isAndDisabled: boolean; - displayInitButton: boolean; showNestedButton: boolean; onAndClicked: () => void; onOrClicked: () => void; @@ -26,64 +25,47 @@ interface BuilderButtonOptionsProps { export const BuilderButtonOptions: React.FC = ({ isOrDisabled = false, isAndDisabled = false, - displayInitButton, showNestedButton = false, onAndClicked, onOrClicked, onNestedClicked, }) => ( - {displayInitButton ? ( + + + {i18n.AND} + + + + + {i18n.OR} + + + {showNestedButton && ( - {i18n.ADD_EXCEPTION_TITLE} + {i18n.ADD_NESTED_DESCRIPTION} - ) : ( - <> - - - {i18n.AND} - - - - - {i18n.OR} - - - {showNestedButton && ( - - - {i18n.ADD_NESTED_DESCRIPTION} - - - )} - )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx new file mode 100644 index 0000000000000..9ca7a371ce81b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx @@ -0,0 +1,282 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionListItemComponent } from './builder_exception_item'; +import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { + getEntryMatchMock, + getEntryMatchAnyMock, +} from '../../../../../../lists/common/schemas/types/entries.mock'; + +describe('ExceptionListItemComponent', () => { + describe('and badge logic', () => { + test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders "and" badge when more than one exception item entry exists and it is not the first exception item', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy(); + }); + + test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock()]; + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock()]; + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists() + ).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + }); + }); + + describe('delete button logic', () => { + test('it renders delete button disabled when it is only entry left in builder', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock()]; + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + ).toBeTruthy(); + }); + + test('it does not render delete button disabled when it is not the only entry left in builder', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock()]; + + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + ).toBeFalsy(); + }); + + test('it does not render delete button disabled when "exceptionItemIndex" is not "0"', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock()]; + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + ).toBeFalsy(); + }); + + test('it does not render delete button disabled when more than one entry exists', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').at(0).props() + .disabled + ).toBeFalsy(); + }); + + test('it invokes "onChangeExceptionItem" when delete button clicked', () => { + const mockOnDeleteExceptionItem = jest.fn(); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()]; + const wrapper = mount( + + ); + + wrapper + .find('[data-test-subj="exceptionItemEntryDeleteButton"] button') + .at(0) + .simulate('click'); + + expect(mockOnDeleteExceptionItem).toHaveBeenCalledWith( + { + ...exceptionItem, + entries: [getEntryMatchAnyMock()], + }, + 0 + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx new file mode 100644 index 0000000000000..8e57e83d0e7e4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx @@ -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 React, { useMemo, useCallback } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { AndOrBadge } from '../../and_or_badge'; +import { EntryItemComponent } from './entry_item'; +import { getFormattedBuilderEntries } from '../helpers'; +import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface ExceptionListItemProps { + exceptionItem: ExceptionsBuilderExceptionItem; + exceptionId: string; + exceptionItemIndex: number; + isLoading: boolean; + indexPattern: IIndexPattern; + andLogicIncluded: boolean; + isOnlyItem: boolean; + onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; + onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; +} + +export const ExceptionListItemComponent = React.memo( + ({ + exceptionItem, + exceptionId, + exceptionItemIndex, + indexPattern, + isLoading, + isOnlyItem, + andLogicIncluded, + onDeleteExceptionItem, + onChangeExceptionItem, + }) => { + const handleEntryChange = useCallback( + (entry: BuilderEntry, entryIndex: number): void => { + const updatedEntries: BuilderEntry[] = [ + ...exceptionItem.entries.slice(0, entryIndex), + { ...entry }, + ...exceptionItem.entries.slice(entryIndex + 1), + ]; + const updatedExceptionItem: ExceptionsBuilderExceptionItem = { + ...exceptionItem, + entries: updatedEntries, + }; + onChangeExceptionItem(updatedExceptionItem, exceptionItemIndex); + }, + [onChangeExceptionItem, exceptionItem, exceptionItemIndex] + ); + + const handleDeleteEntry = useCallback( + (entryIndex: number): void => { + const updatedEntries: BuilderEntry[] = [ + ...exceptionItem.entries.slice(0, entryIndex), + ...exceptionItem.entries.slice(entryIndex + 1), + ]; + const updatedExceptionItem: ExceptionsBuilderExceptionItem = { + ...exceptionItem, + entries: updatedEntries, + }; + + onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); + }, + [exceptionItem, onDeleteExceptionItem, exceptionItemIndex] + ); + + const entries = useMemo( + (): FormattedBuilderEntry[] => + indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [], + [indexPattern, exceptionItem] + ); + + const andBadge = useMemo((): JSX.Element => { + const badge = ; + if (entries.length > 1 && exceptionItemIndex === 0) { + return ( + + {badge} + + ); + } else if (entries.length > 1) { + return ( + + {badge} + + ); + } else { + return ( + + {badge} + + ); + } + }, [entries.length, exceptionItemIndex]); + + const getDeleteButton = useCallback( + (index: number): JSX.Element => { + const button = ( + handleDeleteEntry(index)} + isDisabled={isOnlyItem && entries.length === 1 && exceptionItemIndex === 0} + aria-label="entryDeleteButton" + className="exceptionItemEntryDeleteButton" + data-test-subj="exceptionItemEntryDeleteButton" + /> + ); + if (index === 0 && exceptionItemIndex === 0) { + return {button}; + } else { + return {button}; + } + }, + [entries.length, exceptionItemIndex, handleDeleteEntry, isOnlyItem] + ); + + return ( + + {andLogicIncluded && andBadge} + + + {entries.map((item, index) => ( + + + + + + {getDeleteButton(index)} + + + ))} + + + + ); + } +); + +ExceptionListItemComponent.displayName = 'ExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx deleted file mode 100644 index 5e53ce3ba6578..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx +++ /dev/null @@ -1,140 +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, { useMemo } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; - -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { AndOrBadge } from '../../and_or_badge'; -import { EntryItemComponent } from './entry_item'; -import { getFormattedBuilderEntries } from '../helpers'; -import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; - -const MyInvisibleAndBadge = styled(EuiFlexItem)` - visibility: hidden; -`; - -const MyFirstRowContainer = styled(EuiFlexItem)` - padding-top: 20px; -`; - -interface ExceptionListItemProps { - exceptionItem: ExceptionsBuilderExceptionItem; - exceptionId: string; - exceptionItemIndex: number; - isLoading: boolean; - indexPattern: IIndexPattern; - andLogicIncluded: boolean; - onCheckAndLogic: (item: ExceptionsBuilderExceptionItem[]) => void; - onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; - onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void; -} - -export const ExceptionListItemComponent = React.memo( - ({ - exceptionItem, - exceptionId, - exceptionItemIndex, - indexPattern, - isLoading, - andLogicIncluded, - onCheckAndLogic, - onDeleteExceptionItem, - onExceptionItemChange, - }) => { - const handleEntryChange = (entry: BuilderEntry, entryIndex: number): void => { - const updatedEntries: BuilderEntry[] = [ - ...exceptionItem.entries.slice(0, entryIndex), - { ...entry }, - ...exceptionItem.entries.slice(entryIndex + 1), - ]; - const updatedExceptionItem: ExceptionsBuilderExceptionItem = { - ...exceptionItem, - entries: updatedEntries, - }; - onExceptionItemChange(updatedExceptionItem, exceptionItemIndex); - }; - - const handleDeleteEntry = (entryIndex: number): void => { - const updatedEntries: BuilderEntry[] = [ - ...exceptionItem.entries.slice(0, entryIndex), - ...exceptionItem.entries.slice(entryIndex + 1), - ]; - const updatedExceptionItem: ExceptionsBuilderExceptionItem = { - ...exceptionItem, - entries: updatedEntries, - }; - - onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); - }; - - const entries = useMemo((): FormattedBuilderEntry[] => { - onCheckAndLogic([exceptionItem]); - return indexPattern != null - ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) - : []; - }, [indexPattern, exceptionItem, onCheckAndLogic]); - - const andBadge = useMemo((): JSX.Element => { - const badge = ; - if (entries.length > 1 && exceptionItemIndex === 0) { - return {badge}; - } else if (entries.length > 1) { - return {badge}; - } else { - return {badge}; - } - }, [entries.length, exceptionItemIndex]); - - const getDeleteButton = (index: number): JSX.Element => { - const button = ( - handleDeleteEntry(index)} - aria-label="entryDeleteButton" - className="exceptionItemEntryDeleteButton" - data-test-subj="exceptionItemEntryDeleteButton" - /> - ); - if (index === 0 && exceptionItemIndex === 0) { - return {button}; - } else { - return {button}; - } - }; - - return ( - - {andLogicIncluded && andBadge} - - - {entries.map((item, index) => ( - - - - - - {getDeleteButton(index)} - - - ))} - - - - ); - } -); - -ExceptionListItemComponent.displayName = 'ExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 6bff33afaf70c..08e5b49073ecf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -3,11 +3,11 @@ * 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, useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { ExceptionListItemComponent } from './exception_item'; +import { ExceptionListItemComponent } from './builder_exception_item'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { ExceptionListItemSchema, @@ -80,20 +80,9 @@ export const ExceptionBuilder = ({ ); const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { - setAndLogicIncluded((includesAnd: boolean): boolean => { - if (includesAnd) { - return true; - } else { - return items.filter(({ entries }) => entries.length > 1).length > 0; - } - }); + setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0); }; - // Bubble up changes to parent - useEffect(() => { - onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); - }, [onChange, exceptionsToDelete, exceptions]); - const handleDeleteExceptionItem = ( item: ExceptionsBuilderExceptionItem, itemIndex: number @@ -164,16 +153,6 @@ export const ExceptionBuilder = ({ setExceptions((existingExceptions) => [...existingExceptions, { ...newException }]); }, [setExceptions, listType, listId, listNamespaceType, ruleName]); - // An exception item can have an empty array for `entries` - const displayInitialAddExceptionButton = useMemo((): boolean => { - return ( - exceptions.length === 0 || - (exceptions.length === 1 && - exceptions[0].entries != null && - exceptions[0].entries.length === 0) - ); - }, [exceptions]); - // Filters index pattern fields by exceptionable fields if list type is endpoint const filterIndexPatterns = useCallback(() => { if (listType === 'endpoint') { @@ -199,6 +178,22 @@ export const ExceptionBuilder = ({ } }; + // Bubble up changes to parent + useEffect(() => { + onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); + }, [onChange, exceptionsToDelete, exceptions]); + + useEffect(() => { + if ( + exceptions.length === 0 || + (exceptions.length === 1 && + exceptions[0].entries != null && + exceptions[0].entries.length === 0) + ) { + handleAddNewExceptionItem(); + } + }, [exceptions, handleAddNewExceptionItem]); + return ( {(isLoading || indexPatternLoading) && ( @@ -233,9 +228,9 @@ export const ExceptionBuilder = ({ isLoading={indexPatternLoading} exceptionItemIndex={index} andLogicIncluded={andLogicIncluded} - onCheckAndLogic={handleCheckAndLogic} + isOnlyItem={exceptions.length === 1} onDeleteExceptionItem={handleDeleteExceptionItem} - onExceptionItemChange={handleExceptionItemChange} + onChangeExceptionItem={handleExceptionItemChange} />
@@ -253,7 +248,6 @@ export const ExceptionBuilder = ({ Date: Fri, 17 Jul 2020 10:06:54 -0700 Subject: [PATCH 56/76] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20add=20"Explo?= =?UTF-8?q?re=20underlying=20data"=20user=20docs=20(#70807)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: ✏️ add "Explore underlying data" user docs * docs: ✏️ improve docs * docs: ✏️ change the way Discover is referred * docs: ✏️ improve texts in line with review comments * Update docs/drilldowns/explore-underlying-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/drilldowns/explore-underlying-data.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../explore-underlying-data.asciidoc | 41 ++++++++++++++++++ .../images/explore_data_context_menu.png | Bin 0 -> 103214 bytes .../images/explore_data_in_chart.png | Bin 0 -> 99870 bytes docs/user/dashboard.asciidoc | 1 + 4 files changed, 42 insertions(+) create mode 100644 docs/drilldowns/explore-underlying-data.asciidoc create mode 100644 docs/drilldowns/images/explore_data_context_menu.png create mode 100644 docs/drilldowns/images/explore_data_in_chart.png diff --git a/docs/drilldowns/explore-underlying-data.asciidoc b/docs/drilldowns/explore-underlying-data.asciidoc new file mode 100644 index 0000000000000..e0f940f73e96e --- /dev/null +++ b/docs/drilldowns/explore-underlying-data.asciidoc @@ -0,0 +1,41 @@ +[[explore-underlying-data]] +== Explore the underlying data for a visualization + +++++ +Explore the underlying data +++++ + +Dashboard panels have an *Explore underlying data* action that navigates you to *Discover*, +where you can narrow your documents to the ones you'll most likely use in a visualization. +This action is available for visualizations backed by a single index pattern. + +You can access *Explore underlying data* in two ways: from the panel context +menu or from the menu that appears when you interact with the chart. + +[float] +[[explore-data-from-panel-context-menu]] +=== Explore data from panel context menu + +The *Explore underlying data* action in the panel menu navigates you to Discover, +carrying over the index pattern, filters, query, and time range for the visualization. + +[role="screenshot"] +image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] + +[float] +[[explore-data-from-chart]] +=== Explore data from chart action + +Initiating *Explore underlying data* from the chart also navigates to Discover, +carrying over the current context for the visualization. In addition, this action +applies the filters and time range created by the events that triggered the action. + +[role="screenshot"] +image::images/explore_data_in_chart.png[Explore underlying data from chart] + +You can disable this action by adding the following line to your `kibana.yml` config. + +["source","yml"] +----------- +xpack.discoverEnhanced.actions.exploreDataInChart.enabled: false +----------- diff --git a/docs/drilldowns/images/explore_data_context_menu.png b/docs/drilldowns/images/explore_data_context_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..5742991030c892151dad9d156b7fb3b0ad3f5cdc GIT binary patch literal 103214 zcmZr&1z3}97Y3veMGz1W2?eD_cPc1d(l9`}*=UfKQV;|leKE$Z$Xmz6Qs!tBbOpE=C6Cpg z5X;=*WW*%pFaPvnnm&}IF!Yf&4y_BVaq?3KJ!yxu=eaG+AoY3zyrBumdHu>s&B^7C zQ}0D;R86fL+N$%uS&5fAxgUnrM-eglz?g*C9(qx1DGp)|KNi|uEPRTGAP}Vl#rhQp zVvi;@Z0MruaN%^`5L&O2NrdKSDv`vXj*CxgkB-KEt8Fm|jY#t2%*&S_6@prbsM?JD zhK!WI*}& zk((TJt|a@C8MJ&s0^kH1lEQuqGx=r@qA$a*dLnP{6Z1Nehut>9qwERa&)xehDWKQ0 zWVt-Dw-n%`actL7``}&hRjl?`s(D|1RwV`hIDE8cZ0{emN)!-&_NQm3ld}uZAq}hI zl;m?Xt7Z0~dOc2WCG|Rirn4vQdZxQ?`rO61*rt73Y4t%w3RBEPIx5ciwF6x(@M|*B`&i7z^~@;%~a< z;44BY7GUJmI*!;;Df1mCNg)!#y@@x9mmD~$_JCo($e7}$5S=fAaiM^n``Z?64*9)T zI1;XHZ@C>vkgq(Sh~E{#eR=#!7tUy}v0dOBy#mEH90Hq2J7lDNxb@*h=$BFM^5n9* zU>$YRixRPQ?b6`<4(fZT3GtIKAp#?sqjH9dJHoG%o0N=KiZitDzc6}O%(zMKst^=1 z+PBce%d^Pl!;vD=b{`cYUjHp8?>&B{Zl!%?L;|PEqXgAY6ECK6OX3}6-ytGf{0-4> zg*_xiyfdz3d z2{lW+y+!bq%;DM8Ek5Gcfpq;cBJYDu=>=ckR;4eEfRt+4BHO@&py6ceZYg0988u$&~IkRpc}sZl&UX!73)xF z;*FT}8Hb;hkwO=-Q`6WT^01bH&$bHhx?4Jm@e3&Pza-4Ddy{T_eOY`WaLWS8I)w$F zlM%rqM-J{*eShwCt)2?fIJuku{aP*gl|mYc0$vZMI7URITpOLj!w-4pB#BUVRMf-8KzwsD@9LOA)97sN=j6K<;6{A0nl6heAo}0<^!b{kxoVt2u1x*~3<(E;F6f!t<#)C8cvR2-hfUbj(Vfb7fI&M6hwrZgznf?*h|=F*Cz;H5*`tX z5=IkR+|i`C$GvH4tRT!75#I~$5oxS#I_GG%#k2^t{QCZu%YFD;cN?C8M`JRk>gMVY z=jqh|&Q5_YfnN74hm1W@lBuulrmxzaqq`Y%6kk50e0xa4bVrA8zp>3wK~?SHp!=Xs z_M{?v!f||j;?ubHu93uKt_@C$1f0Z`gd$FH`x(2YV%)Mw?Sjt&MtfA9ku63`rOY~Z z1qSkJoig%SOMIQKojxp{`494m^IztBj;PyIxKtcbZfI^8P!S~V@s-+H+xdLX8Y{7D zso1dX9jh-dt(5<)XF6(m(lyhTVqs`&XyZHTzfH8r(fx5^F5!NE)K=UIqJ6J~WPNez zfoq;)_2OG0Pht5aenGdl^l$g68K^Zx*Ru(_gBG4IKHu1Hj9H`H(w(1JJvA=tUOnt* zkxr$!81L?6o3#s>Oy)c_`4-MZF(|UE?Do*j-!tZn_hz*;qE#e8?t)F2fy{E)pl3pvQdq>_oD_dHtZjC!P zeaWvxu0$_;zfDAthEH=ItNLl6iNDa3u%Kn(;D*|~u!uAx68|K$YEVYd6Rr$geQ;1%*CiCIGDV@$fStwQRItH z5=V{QPG&l1!r{h?XNvda65lsL4WHMu8jO4^FhJKTR@w#MVFlmcB*S{G`6k62$F!)! zCzd3#R47O9?gKdB#S!lb-@~e+s`MOZms^&%I;h%|+l$y~$xX@bMy<;qWZJ29=k&x) z&^g7%bO`036!nA)weB&@=1Sa-zO5L9y}Rq9x|VjXiAsA^{3&mB`fza#$6~cRvIikB zXtn%4UODVbSVL-1FL9T3vYnu3((G6trFIrwVJ+ z&ljFI4IIpdqu~2B{$}Hy6EIKq+1gi#C)M-cI?`flwAKol4UR02z7G@HXxns*)EWqz zZB^m-hO%vZ+W4>`#FD|1I5Jsa`1x`HdoOrHgHGd!N|7Hl{@pbiIvCU4l(x-MP2WBUt33j`52lOBonkJ8($3C26^ zt8RQ45zcw8tZ$RpH{v6-emI`m>?`CF?6mxK+^Dkks_`sayeGMi+3U0 zq866J+dGv?y>(-!{(SQ5Y;{U!%E`%xOZdszH}V_oH!g0Sx3RFdPc~01#4^3yC&VxAubC`8Y*p?~%@CWVest$fdjkd6@To0+>$+^6~jLj3gloE!AJsl}%T0*_|UhMJUIz$+DK#WZ|1 zs7R{%{CDebn$V7R@7PnSntCGgt1NfFNxJiM>~>s7d(MrPx_Ir{MItt~Sd%cbE0tfY z)p|{~-0NNYVqWbaI2GPXK#`R9%?tY0BG0tYMiQ^-D*{~Dn^@=9Ij2C>5o)F_Yp$q> z#tdBJqG6&_qG177=)gk^o$Al)XXua6uKn>j1{zw36&mKRdz66JAAga+<42pHuh-rO zqu~JWh=GTD8piLtF%fCkeqUo00Nz^oh%{FqKU%~fDiDXGCEFZ zXk-jO9_X^qAM67Ck6Ni~J8LT{2$?|aKt`qzV>6Ju9rQ;(Xd>=Hz@?p;vk|Skovpo- zkh>`TA9n}=*FQdHr>Fhn7H1n#dTm7&T1kkb87(h}2gE@yhEGdNE8=KsF7#aL*{|ln zJ5hQ|XJ@DoJG+~k8_10t1aY)r=M)qaWar>w=i*`m?qGBBuy;0cXR~*@|Fe_d{YaTP znK)WOovk4Dv_JYaGKRP~i_+8o80gQxpYt?xxB6=&d#7K^0v5>r;~REP5C{98eFIHJ zetasVV&!gTt1V?^2gnQwlKQX4fB*8| z4gVNZ=dUsO1bF^FzB_t%IAzW%r#I;M2Wkw-(5K$DevtnQA!IgRB|od|OQ zV$5G*Y;63(jhuPJhX2!8{a85L7<$ok;A6M@wwJw5%PCddmB%yPHJ%L%HNr00V%%qr zCzUHn?JOXs{79bQQ2e4e{=CS2#|vf{S7};$0l0&u3#PL?lE~D5l|{*{lWeUvLg$Bu zflK?Zrx#fJ1B8e&XRo%b&W0Rg=9;PIG4E0?lEHFPCLU|_n-@b`n z~0y+uRBdVk(_&*cCEO|=Xr`fN7y=<&hyc_^maLRdm4KB5& z!_$W!qTRp5Ioi$o-wF9ixDrSa7k+=yDPKd(R%AcelDDixfd&4I(~oA;5^Ahd;v95t z^Nk_PjQAa*$rQ) zJ+&<*mTL`bEGoqR(Jo-U{kUcJ{R%=A5Yj<%tz|kb7pI&u3^)R_n>9|%<|8YAd4=<8 zUqw)v^|(R;9}^Gx?Vc^lZt8_6|CNOQUGhF!>!%L6tOol>h-z1Rl4!gv z8pmO&ZFQ^0m{nS*Wwy_}l?k^=+bH!XWJ@(d9nX&)QU6_@ggxGQO^H-?PaJy-gW3G> zqBRA6)j%$$hFX;xCAwCb7Mtj5XiN*2%QA*%>xf?d{W(p0(Hbz@e}#=Cux#T+r3$+G zgbz>14QeQ)f_)b1J8T@V5*algy2(*MvDA3@MJI=5bZt+k2h86j3zzM)zsCEodZ77F zk;L)3eYS&-bsV-1=X%Tc2X>l)bFCe!3S?WRoHd+sM?j})O zVq;=wfi+~`f@-|fV){QAv>53}fEdyFLV&211N$h0{ok#K-q!MzUvaLku2Rba=zK!l z2=cV^uqo>rFM{>j?Eg`wB=X&Bpg|ni2y(zYwBH9&){;(8Bf?A$9ai5~_h?+AZs${G z3Ym2I!vtfA2^0 z_xmw*-MZx4ag^Xw(#;SSD?+$TqsH_UQ@CHJr(y;$Dhn_A=yhu-);3M_|IykWg74^B z*+vbjDz#H| zVCh^C)q*uja7#WYPKrEu8ZxtGchygMR}J-f5oWUFOB}bt`quCFBYjldmBnBWett@S<>%j;~&`~ z!3Ga^C9jJjtIb!B_WQovCpn#RX)SbYqr+)`N^Z!=ZP6{K&FE^RMiLr+$ETr-Yl)p_ zvg)JZtm0BqX>VDnWDynP{bwTcn(bPKUi3BvpR2_UW8T~pyK_H69QC1L4YiVd-}@zG zzIXKKYx>hL(**->l6w`BA;`}bmd#jcv!UvBYgwF_+&u=Vx#T@s5iJsdXk9qS`pxU6q;;V5a zi(fcRE?jy=E)bi2zMj7I%p}U9M{n_b;k*3QmyLB?FB)eqz061!T7B;u`W%%T+7^kr z!Hz}^dsBOzjpv$!2AraO@c8qs8;^GHM|2F2ehpEpoOKPm%eNaD*m`Q zJ=$O?YB)_7Pwn}do0H5`)Nq+?c(~PH)XtcASW>jy{GpLLh zN9?Ie(>yd0KOF=4e*Xw02ShWPOu%!P1n;rHy27@V(y{RVuqHsg+Bor~h!|lmj*6G^G z*BBPU2oYlENb0rzI=xTswv8^|jY8z|M+%>qO)2EZvMo(H^~mW2$ah*JxlNX@uT?Z* z+YefYm$D&4>*CUkG%(-|m#2Ca(>^(4)29csmpk%nL%Gr7G{T%#1E!_*8D%^t4+rUV z8ney-_+wOvqn)mH$#<2BU-QV)yKEshj4kaE6o-E&)3aryDrl%bzc>-!N0A!_kJz+j zb;~63*-`4I5OVWgltvSdeii%|6IECC^JB?=V=RO|dUG}$cDyy2^+9=9YA+|%hm6p6 zDvj3+$Z~I)Z@IUGlBRcKH(YFc>Wm(ql#QETWXi*a7lpaSUDkOTHbFI;54sb0qfG(0 z0-f%67A>-q7#B2LR+qFaCr(a0v&WgRIw5DAa&P8wINvPooi-NS3tTYmWaFz`A=QhP zF4{Vo^@UjtW+%QIXEr?=u1QcSuCYZJ-Sj;hd$W!WKAAa5n{ppy`&@Ih0=eT_c-OP5 z|FSkwyUH=E_GAXfUQ;L`a^tFEs=24(qNnCLrAL#*fkt9ykMPzbZi}AxC$m=)WUSx^ zdKa^?>2xY3e)65~Xs*P4XQ}MUKF$_!BOrB^)}M^VBE&Br8G4KMVB3lE&&cpimqj#q zr{k4h?uF2pRj>Kl9>MjcY|gE&44ZoPi0xKl*PMoOcF6ctTsq4r=1uO??DuNGHk@3> zZb^r9guCz7Bgko2FGkE$qaR*b^;iX$*^KrCdQy$4AQTh%wuvqURwFwErXAPLH$No4 zLvM{g<1%WNiWm2-KV0qCKTQzZ6>`~JO@$@C_7)q_^<-PKOGv#u-GFeJZ%Q9$q3eb@ z*_9@t|C0#NdIE^EPKT5ui$k<7`r}Bq9euHPEWOkg=JegJBEML6t)Aat7cWOwOZVH@ za9CK1ii$u@ZUl~WRb5nEwTXG|Siu@l%X*Icxj7*nm=>?RH7~f$LcuTBVm>b5bXu!5 z>Dkvm9s!$#BaMair~Pbll20PV6%?8H!Cw7Xjt6X&)jCvCb^8Mc!1l9 z_pK0@_NKLDd%x^Vgw(zVXgV*=%kG$C#n6|V$ok>k*u>y=bR6n+n%?ADf1J&S{4?%# zWw!mzY$XdVVdR}ml0A_TiWa+{17^Mm+c#(&&*blC$3p5Z-dz{7lCdP8eS1Yem-xUW z6^l?g)t&W}D-cqT(gfAaTayf2+@45Hq;S1Z5&Y&np9-0IzZRS($-Lm)sc~GI(KPfX zKv{9j!XW(x`!Pyj?z@SdWjw1C?nOD}FU;Nuv%4 z_u+*<({;gs&-z?Rx{ePMEFNl4e04YTE=H>7-{)7YvT|I>}u|csuW~z5$>$p|pfgbiU zqkulD!k_Dot&H){`@OMSM=$5tdIISP}>?#}qSm050yVE@b8ZIaR= z^PV)iOhl?>dK{SJ?W`k7A}YIkjeQ+%V6(LQ$X*@vHkj623Wpr$H+7CM77U1G8vE{z zylTe)RYs3Vbg|J`&Iq}Oc$^_mvQd-Vy>H(aHQb}gqsuD)JfiEQsPkp$o6x~qI^r{I zNjp&PXx?1wC)M7Nin_Jn4b#Znc9Oe!B+zNsGkwQ1YBy93mE(H5W6Kh(jRB_5q%4Li zYXApO5kg-{=A98;*LXFFSlg+;&=qoBRoU|E+){3B-wqLujTPJujDFJ1>=Hl2ZbwXM z`z)_>)_sd%@?0s0gb;_?OZ(9y&;2)16-?&oCAqQG4nv$DCrl@t)e@c8olvjV7FYxn z&KE2uZ8c$8Hc9qXrsBc`WUP_X$72s~(_6=t@A!EyW<-eNU2LxI_li;2>^$8(xK(>2 zdE5R}lj>kj_;Xmi^gsDv8y7kToEa0GyFG`qQ)hOT9(vItnC!PvwPG0Q)EmYZG(fc0 zJ!)9d-QLmBu{bXeitr?v0XUTLlTX77r^o>(FXUy7B4MhiVqs<$*I_l)6J8HK&Wr1H zqNlu9&zU{L=27He){K;JKJ=mN*V5D;h-rfW7>TlwMnYnJV;N0$vg@Xy2|DZ;Rkr0S z9F&k$ce+ndf?B??g*q&%ww{irmD`q8C_-giXpALE3$0wbd9~t@<`3ifA@JF^>^h&` z+mk#Mr6CNk=Tf|Nl;U$dPJXG!GhJ2xz4_7P%4mU3Y3A&*UQJ6nZ|JiIRNidh9>9al z`Zync--r2zyi`URmElT&Ve%-C2pSpA|oRk zOP|1ep7^BtaU!qv!87UsUZqNKiRa!ostmhmCAS-=^sf^k)lDfGVv-X{M=72((EO$C z8hcp4O(EKCGa_Ayone4uvekJ)=noWV3@9DB{^cu8E78!k$gsd#`rdY*dJ9T>g(N}M zyCPS3r%NzHJ2p~Z|9BZPXjOI)yuh`muGQeITBqo^qe%Sp03bs*MMa(;hDY1?>2d@p zi!`~W)PG$2T2oTFg%vq+D7I-arcZTMt=*8QD5D=keTG%U<4S}ZXrfwVds zW*?w{0k7GqRlFhgU7|8qT!}2tlo%TRa#_}9$yI1~H$$@gGZ?v_vWC1wEGRp+EFKOj z#biW?HK&dio4cv0wew~bbUhc28zm=1%#|vCd+qw%aQbnt$#$*Be(Mg&Vi|W2KJ|U= z0CqlvocRyMJ(<@fLEfO*EB4i)EOFVm=PkL$b6^}SiG6RaXcTR;LJy*J2kL#frY#h6 zk`?#Qj1XloASzM-Mwk{)ZA0t2-KHH9FO~!}ir;U-4!uvrwae~9_`e3z>xS7Z7$ly~ z1;@&hL9IBl(c`IjBDGBlkERQ#l@qzfQ;S%U2?7r9H;V`DeE=n3s~GDVArszz{e@&g zofk>z`lZ|P>e`o!FII{On-GJgUPIRwjr}GZlXFxUyO3dz(1EkB*s%9&LJh|A%V?{Y z6H~JziVtcG<1xX$SAtDNdzoi-XDMe2h3S)1bP73pm2HiZH$U@+Z`ZH*_<9JDT%Gea zVf(!4ux`+q@QtN1aNnkwTpaGU;L_}DrSW}U&k0dg80oLwX4hzp=|WVW&>su2)WE6T zR@s%skF>#!S-lSFFjDmj@sa!gjE{$k{tytHrk;v@YqC<_W92y(OFKq%cjXl0P)Oex z=Z;(@VS2gBp62HE`Hzu6$78f4EnPy@Q)t-|?mRGoNy!CA&bGyA& z`r9yZo?K&=zd=KHrJRR|8<>A&!FRtO$DR+=h|5Q+xHPxZsPJ?3k8P$;fPHvG$z0up z1*<*-{_(LoI_}|Ks}eh%cmHyt z-ank^ZD`A}S*GHrGNu?RG7$KYDF51u`4$3n4J3-3ZwWKol%`4K=kZ}!XuV>;T~S3c z={D=V*mSnzQ0fYf`N>`y1F(aSqkFnMcWRlaAhl6!3CxYJ37@_XVt1i{wqP5QC;7fB zsM)S&V)twm^NgP`3c~9Yx%{f;IKI?R37N=LHB?B0$h@UC@LD4k+}N~?*4~QVELCo8 zdxb?{r`GqvgYfn7jHmK=vGh&UTr17e$mI5+)~Ix279@FSWq|Nx$~$CnWu;~u;jl4M zs9@+IoOxYhfzKHIpUEeQ5qu)3nQ9~Hq^(_}U!B?aYbU2SBSyaVQwlbHtiXWZ`mzJ_ zR`8p{-fFcP|50T%?#KjUiW1AdW=~DVrfWJ={9SvMEu`A3W0ph`T@nZxSm3qK?s;$G zP3u3CEZ!*zCIc)HMeew2)X%Rn<2A@3c>2kb28_Sg8i@DYz<{n$ue=1ZsW1Z8?6hv( zO+21{{_x_la5jzWMW0Gr*2(9qcQwJ*o%)V@LC1s%MYX5ZNj~4kFON?(74=tw2|%?E z4)+62aS_{ygKd?zY>3zpKP>>WtJc2uY?}HSy5-hZ^}6Fe3(rMq?DhVgJ|1TWvbs;v zM?*}SA#H=LE169X+mwX2Xa{q8Ig)D)$=7}5M@Q8gT_(5Of}SuJ2Du!?z~oEK6|-D6 zMoO$2*d*tqcNIM*AaTbu%h-fuJTGu>#AAcAva+%(bZ7gH#86*l)*3F3o2Dn$`XO}; zl1jpzhHwb;-IHO9>>z!E4oabM?Esth_Y5U--@MOl`ghlZRxVjiN93u+PQR1j)eAni z(eb_<*RBDLSAHBdV84Hll0vueqHU{k2MvhCGQ7_Ba_|zLS}PU}YwcK{64cBd6E3?F z*c)`Yfax9AhM<$z4{)3`hB7W+A zOk9HM!zB06RrZaw8@!~@mXQI6*3rv^vf1o0rB#=@U{H4=Z>hIa)xMA`ukWi)E+ESM z6)<0dcvewsA&|^ZpV|zU84tr>QEFm^j2e2Y9xA@i#dvFb9dSs>^SYl>w&}6t7Y2D) z;Hl-{(Wt&>c@p&eb5pv*Ad6-}WZm)1?8tejUcBk!;^nc)om!TMH^g|=9V@3@qgukq zTBkkP)M?O1J7QRt5F|<^M*6!_M-&$az2cIObK$-#g`-)pHAB|ngIiNK2mh4-8~>5X ze~JzhWA0QSY4i`Frg(Xf>>2nYv(ir~)`ZqITp~L>S2Zc7k(Qx2z*n&LZ6t3oGB_D3*4NDShWAL>MR55#DuwJ>{Ucuhe65#x9InB(Wnb zQ%8Ts9D(E=HlqP=BYvlm%t_we6nPWpU-!7@arl|be(LJnNtx$Xfq(2eN~Vmh z^Is{-5;Y)x4)?P;qjrsIkG?wGL9&Z`+azwG2#6KFUe4d$RA%Ah@VJ`pPgndll}0xT zGxT9djlYbmKil?PE46}*+b7zYo^4f@^I8qyA?$xew)QaByVsq|o(=6ZT;9ow2el@A##jezX(+bU%^SXthQO+B>AY{TZ-{6siLFWL@s*iyN6R*5DR%H} zms@=rQ7o1<-K=-H%z_!laQ6z08V-My^h8}wIymq;zdHG-Jj-^zA+tE=8oTM!5*+&Y zpyOC{{-bn3xp1&&SbE~9SjIh{@2V7+eP0|tX4Et7za0!|Yr>(9(?l#C>*(q-P9z<) zKTPFh$NN6OcHD|XOS7xu51|3*^+B4`UH zc%tg`R0Rc7p*`&i)gx>7QihHjZH zoeli+v(5E_c(>l&CkIJz46pv`sAUO6!|{5zCfEn{Uq#h7rh#f#uk)2Sc>XnckNSsw*@O^^8;CItmfiA)3o;oyeo3v z`H?S7@|lQyd-@{JIfqwU;_QjgF`^0Y2DjoDdoPzq>}mh{p%|j$zW)|1ioJ z8XQ4^>+%nDx$D4ohjz30E1MpV_=WpE%1^Cs+C;U#N43M{VcFOC<`LQq&vi!~5 zKMA5^6eg6KD*tTxNrGdS9yuTXH{zQXjEVc%8u$J5&;5=3C}G>Ep`!8vqxc1Gti_5; zr2&I=5o7t~WZ?n8q@L?)g4E2rDy6r_3vHfRL~4PblWFnS@ZI>(ucLbBFPGsjhlyLt zjBd>P(+XT35m7;{#Fsy#Yx&fVe*Mhn_z(jwR6Bkb|Ia3AFEIjjhYw6_euU@#Q)F@6 zzHhKr$KYn+l9?pXDo`zueo;~j$o~ex7MA)m*lZI+e^R(v^DybwkAWn!ZJRuAE*XO@pOi^Y+iB8`7#GmNZA4+cc=qVZvJc zN~G?$pn?p4BP1^L62%%1yY6ECXh7SF^*l)LT{fd6yc!oV<*^~aw@Hu0U*`z_Q)yK4 zF$lS*_6@8I@;Nr|{xOO_-zNd{#Qg{C1`9ld3 zt~h#_+U2eb2$jh*Z|aP&V?UQ8ZRykeirbN^=i?n0=0aEH=2B8Xcy~@fQ@D zhD)p?bJug?UpC|p<_t|HILXy{3`E1{&1Fwo@qbDuk2!5_B#D_%Uct3XrEM702ky^z zj)9h@K2)mg9MvsMzI}T!vFMdVV^%)6acOugw6H1b%3Ljgg^rEjucC$ZCusvjx%dyG zyi(s}WGyb;$5F10*N->WAni#~{Aux@g#9OQE2qY5BJ(q>;A3KU9IG%mO&YMZ9w0k< zZvuyr*`YJF_RH2#tG2AG;Eh_FbOGS}xn?D0y87E)x+7M_+iGXUWPeEG54i_PWHmhx zwKLITnAP&ajHxXcxAAiD{PZBqL^;6Y=h`R(CG3LrZqVLwbdkB`Xw?D|N3PEgBloX? zu4v4)0_0ENSInNnP3h=bI$%P$^XcV4-I_$>lHe}&U&BcHf1p#tK9j3W{Vu?4RQ28X zxNT*N%wq-2BNWN?w$Y%dS$t=c}ZHBUnd^tv%h8_pT_tq z^}Dy!7iKqN2Q%9!OTpw&mJBNCFob?fxPDaNWNVE1uV`$XjzjB9A(ji3eHF`zBxq2b zQ?-rsI-)EZbuN>t*Dt|L3?1ixxNg9U;4*^Yl|SJ{nD_8seOPH3Mmj1|x^%9Jx%5!| z%T(pCtm%pHcfLGT?3WA~O=<}!dWqQndxoXYkN!%vaCXmka1FXN0!xL*c zp7G6*h7*e>10ZrpX`X)JV;DAAXoNAe0$Zy~@AidXr>zmQU3?Qkei=5bXG8CHhT?5S z8ZaZ$QIbQ(X|e@=AL&T{_AeuPkFU;8iw~iBmoq;SKWraQP~NQfH8tDkk@-)Ln8t~3 zs)ktdA_)RFC?pwa6;;vu@;5+LLh2;sTFEHdAuYxQkw zH82oVI}4)2djH1zBgeu=I3$ViZ^I=ILgG-l+~d znF#-|CI4^ifU?yFb#9bNqMS8MB;ls$T09>*xT$+m3b#^r~K2djdbUOXw`93fnT*4PZHD z3!S-a=0!|3psIVYz*$j#^5RUk1&X)31C=~KYMMUd=a97h;ZCalZrlFARMJ)}`iUM@ zcim-GvFY?~3y(cide&K_t-~rG3yLOX(Ug;wwR%-*`iBQc!w8p8E1);*V8$;gS=KBl zBdEgP@5rQOF9lu0Cm_gbXMQ?RtK-DP`l#3&>Y;eM&QJaeo3J~Rmg$5cC4Lnyf?o6O zU-YLL<0A)iZQg(&WD*Kq+UW%yTiZQlTv&f>rDK=J0coNKPRDh!S~GsR&hV#cbU}JO zwL)~TN$Jw%?o6FQb1v`WCsg2ZN^n#R-vE9v7;X*I*|By6T(s=pTy(u3%`=qzKk)rX|W*jPSajht+b@E$^Z@*?Gn!u?n#w>ee)@qk3sEHzFu6+~QUKR+z%>ZeG?lsMI&bzVRTqqK zekn~8_~piV)cq;ASK>hU%ZYM2s1l>fa>Yh7G4VLaS|At`On3x$n)yw*B-n7(MUkO_ zTFXi2-ebCYGqTy__G@Ati$x7N@(!|i<(1ZRmnHH)-C2nat!@~Lay6tl7Ow)oODl-R zv8q_Uh^~T>99@fVR;P#Xx7+$8i7p=3@jcD}K{_E80F}R~WdNs9W63Nl12bM7Mc`FS z@vV(d`=oV$cOLsKN6Jh;pV;qvE8?~flQ)V=V{el!PxfMBVHuFLsPh84OS#CllKC5~ z{1ZrVgVZRtggm<0*cUVl z8ItN#39FtFvXOcG*njp))5m8q`e|l+da%CGciyT{`HsypeM;2wQ7V${Sj@|vMnJB! z!>r>is^Y`3`QM5%`vfra$e>Tfw4Y(Hu-uaUOi%N8i@pX@Dgna^VB^=fujEhV@3!cE z|Nc43B5$-w1YK*vy#n!z@^IaPqL1p@ILD-?KWXPLujJ|Mh`_5bPGor=z(Qiy@n=94 zu97A+7wc%*%_8ic8*8X0f4@+DF4jK7SKH;WmaJ_2hgewK2PW>VZQQ>qt=gE-$)SZ+ z%hu213{9Vawn92LEXAH%b?!7bvW+t9VuA&a;-H`Z+vhZeaAiS<7%Nll5M z!$^$&JXKwc^CL*)YOpQJizUe2JZ7?nj9cqwtL(ZP6tY9^Z@I}QGR1y~m3f1QI3%4D zQVHnf+Wo0H{z=48bS>DhkX#fVA{0N&_CRVYqLYgG+^5Rf;2KCc`zC0kk^iSM0!W^N z`N~Mmocx|_N!K%<#+YY_tX632+hDzkzOBXjStd(tO8zeHU;|w_-Jl}By1}hr^4*R;shmel%e>w6t-X97u($1sGQ(lQr zz&pf6r{(W6&+%9*?Tg2#h-&z!rWhM#{L$a_J%VgR*Auw{c8ytmJ-Iiszo2WaPv}*L zj{vF5pl!*VZh%q%&Mg}oFmZFY;DMhr%?pGYyp@|U)L9(1o^EuQ zUq0uw2OR4+6tqb+20f>+H4gn2s8{Xq0Er)>H-kTk*V)avGR`E_TfPV4QP-YMp&l6{j^X5UjCJL%F%=r zkhZ<(!#@i|7mNJ}61X1jrZWR;${z$Z;%_VKQ6~uLTnhP8skI%|W$_|x5oQEhW7%)2 zd+MG#^wgqUVTVnmAIyFxcKxS_fB{>Nx6&c1JWa8D8beZ@BNJRS48~{k^vI6zm4N&| zqwI6Wpm^1=w-rgt^GOAVp1VLDqkjBPm8XSa z0yUB*!u`u{g0O;~zxalQ6~+H3!B1YY(2KCTtS+6fN}(G*Pkb%|tlD8$`V<46^ilz#{mT;DG=3PO?**tk6kB5^q`lGWg~iOJVq=!?({R=Z zY(Q;6Z|jXzXOlh!j4U(rLX^rBZ+FUNmSuvMd$O=$Ept>qJ*40xB0>Z}QPaJcn4thC zmOiK$iA70$1h(pm-dCfulRH%$Fp~gf0@!a*6J&_=KxBy+C@ev!WDA`a*~1=Hdr%q2 zV??F%m4)8soOGAxMJw$lwPOGknZtnI`?8X|NT7n8zj)AMwghl?=naj3kzZWgn4fDV zX0^J4XIJrw4#rOeU0NP&*7%iYkWj)ypfO*BxzdnbGm6z&jKw}7Pd>J`u%ctj>6{h< zifto`cq^C{qNTP(zQpwV$)C*vb+imESzM6IC!Tpcs%6)`e*Po?07!7-aJ@d&q?Bp^ zE8q%lBFcbuLZd7aSfANA+=4C*NjA6qayJt8_=q&7i~N&xR$pNVUm=jAxr(jye(;@3 zZ!$sem^*j0y%s)dMrk`PhUh{JVS=6T5CaVwAAZ_F8UWaS`2lzc{KU8yotR)0-y)X@ zVfkhVA&I4x#Q|{OpYz7}z9%u_?w_DaiwsfsH;YmJh69dz+iAaky!LJ8vX-{oN`dOW;i3-)Xpxz+l0m$@l^u#X2U!(2K^UMIqn{BFtrpfRbS#`|p3xmOWnBb~Y1a$hh*|K2F5iuv{yMFwDLVl8bFdq8$T!qD_AsZWp zt*xukc|9sBd1SW(RLW4xRY3*fE-J4CocYb)Wz&tgnfM4Gc)Jwrep~CuLhQ(Nm-p|{ zjAbCk9-X=wdA!inK%q_C;&4-udZx2!))32_Wjn zYYLS=WD7Opf}kgNX}RY7&hMF!0*H530k~KojVP-cm0M+I4F%x+NL($TQ+>MR!oi9S zbgmPq^ z4Act>f~%oqXu!JRVSX$*DTJWknHWd)#+!Kq^_Rfu6z$Z&G*Cc+GV2ae_5_)yQrgE; z9Eh_cnfV8^vM|9`v-J2smA$VGix}&NTLY@#M;YG$TO9C0Ov^GAT!9S(w_vJ!} z{Kgzvf9oJU=GX_weQVQ}_sbr<<%a?0rjq3L9!;7&NG>ei*-$SHnRt(AW1V=bJQ+f` zw)*lwIDrFr-`8;zKd0oVOM3@uMFW0%O7`>s`zaw}UvZ0V z%W~!>!gegHycz(m!JCBSKhw5A#Hf{~i>?JI9wCA_M9+HF8tQRW$xMI%P8#|dLnlP~ z0T#!Lkdv!ZT3!pt-xGs@fBn!W2JZnr0$-sqh6WE&%NQC(33#L!hmt>kr2rgy#)s&= zEQH6#B$tt=+5^eCwvw%Dw%4ADBe{fkds;U2&SnOv6PnvG;PdZ|&&XP^!pJ5U2HR`R zkiT)50U$;dg?dkC(#O6I&B)ya=JvIlW=x?Fbc*cAZ4C_jne0IG2NSi4AoqVj6ZZxi zo()5O6DbuGwbZ3lTim%RO|Yiat)2MbrN1taa9A@H(+&WXWV7e)PqeRk@FTrW0tgBp z;13fbs)-Lm?MhZLK>C2r$SdQG{w{{1-g*#bMRyU!h0_#U1bb$P9I6Ovb+1*lGf7n$&7OC0PAprL?vWaNyu%Rb<(b;94NG(^pUt}zdP+V+Zk~G z-VIQP`^y@;%BYM8p(kYQ2CU@zjvkh0U$F1d3&(HB@C^ljG50>}kbRgOxf$t->a!_= zQE*$J%uWwyq=J)PmWb0NecwK#S!|{8`9R})s#A|T7LMmK3q71MCD|Cy*HRw0Ev+wE zrnJH2H~NFt(RN}1EGT>}pz~bMW;Wf@8anRR$)oWBzvvA;x>oG0jMB~Dto0LI0w`OT zS<3IT0<0jtl~WN@fka9iIdGeW%QBpe+S$7*mU9pUWD$WhTYi^kn6WoDxL54t71-rS zMW@<1UUbKFR>;|sz4BtkeB5O;nS`tg*(|@Fy!V<5F(&i@Nh}HsWvXY7j$;#QQlvycIuub-=|(|7TDk_1?k>pzkrDyP5fG5> zp<(Eh8XAV~l$P${9)2o!t^0ldxLB?;=dJzjeD-q!MR)6t0&N;JX;3tD`6+$IQyr%Y zd4qbUB z!U|%b$Wc+#lTB!8kLbr;%K57h8Bx|m)w3K72%S&21mqojL#8yQEWo&Gz9qxyTTi$WsXSmqncSwh9e;6+hE1Ap+ z0D6nN*a6cT9Q=%EuVbXJE^cUa2Y}=No^brgqfi@X{RX@F;iE_;v~E$E|0pKo2(1LGAt4b;^a5cQZl- z9W=>4A#y?e3*tg_Q=zuDI;Q|yHn>;7b6(6vb^8f*r$fxP6;yVm((LY@hgPX3J^$tA zj8lN3E8sd4e`~0+CX(>9YlGGpmd~Q6`u9 ziDwJ8E!eEL&=BY!Z5l$paK@}rXmlRMDUCt{x$8B8GM|nBH+b{$Jh^iZHjnktrekZk z;333ww_1F%(q@xnV()OBb2%eAsEs>fIDFc!!Qz`fk5Uu2O}8<)K5>9x6@ZL7mATgC zuG=Gfd0Q6bikoPNo&s2?T|&?kw>g(uO0NNwRFmd-XQ0l>jOnHaG#4?YfX^cf3s>XT z`P6`A`6sy{C7KsUb+7#Wc`BwYAi~D`%cB0FE4!kM8D9Laa$a!$clt;4fUO)Y)Oo9< zlsebtl^Q=v3wPnrf1kjzF*gX@l@%U`cmI6rAIIo*zn>>9nWL?=FmW1~)-$-4T=#DG zhq(msZp&oj!MK(=A|W&$XWak!4l0SVR`%{F8o3`(G-kM!zH?i}9Zq>!U2D6D`9q2t zFIBaxY^k(&Op(YnnhDb>s+j`O;=etPCp;C&US1;S(_&D@{99?FHld9#RguVxov5pmDc*89v+~L@QhlEl_A!0m za!EY{gVe?^*f$ID+ae0a0I@zsfnZkxb~}p%LdJLx_RCo0V&}2hr+?{DRUO(#Z!%}K zR!}tT^=&7^(j7E`I5LIbrFiqPT9q}77zaA`V@2M~VH=A(JrrI`L#{g-6=&wKF?4<#hPF35fT*n(<({Hc(FHHhFJVf__Re(z8 zf<+A%>Ne-8A1=DEJivAs7UwpNq`ASXjr85?dU2D9=92S^L4o8}FswZ7M0JOt(v&r(#p95VtCjL9Z z;unKWs~2`10deSNmO#`bi@7}|cR}mqKdMP_&|EuIVMSW|Os$FAX_aNdI@CT)u!_b- z(0V?#6rrxuO_FFAcgPwB75cA)?YH$X7#{`%4enX^P*lN6zzxY(Xo^ibi72}*99q7i zy#n&Qq(F@!BnBsIx(n@OEvw$4*cgVPv5EpYA^90K6eVDn_5q1b8ro3!g3s$@du*sg zJZL@*wyy7Ursg_jlrpgI)Xy)=#&=Cw%aY_Pn*^nBP5<&}@2$D$mhcDQ%oW~H2*RK1A(Gl_b>ibIx&vr^KY&rHL7oa&`ybc|z z_w!PbUZo*-@y8YMIIg0O=6nPb7@mq7-ju(A9*~f`P_LqBcOqv(dy@tErI>$}62Alz zdpcE*iz6|nTKOkMfRonif8jJd%cnh*pJ^fx8^NV?9|QEXwZwnZpV3pYdD|4$fuV;_%-!v?*7@d6MJ_kdjrel z9ysrHqC_}=h@W`xf22ZeMD^c7Np2SElHaNGFf`!}Dw|DO&QbpXuwJ=2oh(MQ+HetA z>3qAxy<4j(e-B2$RPP;l7iw-yJR<-T<=qZT&#W)*9+~F|-4f6xAOoD}$&uPE0*v@KS+5v?*cL?cmt5@T9CrK$J(&E!MqH>U&l?< zuwi)or?0BFM|GICpVj|4Mql98Y3d7(p2<3-8BCO}4A~uAM`y}H0@T5rNSwx$>^CS7 z3^UsSjf5(jDM@wCD=`<|il0N+mD81fED{jea4)R-VdBe;lg8KTk=k2KL@V<5vQ{o# z9aGR^ZqYP=p-sOD@!=I?GxMg~nu4%7)5JDAgZsqB7&m+VV;1d3R%kSmbE`AAALKTx z*;NJIz18V1j|nJb7(^|BMlkz_rv5Ak7MztKhQMZ6=K;A*a?lZteQrPC9vCFR_*51a}=-W=1=^FDK*y8hhwf@ALn&j-N&sI-L)Pp?Y*XFUVsUjA1b{i+K<7GYT%K?W-|$50HCtB{XBd#JUfF2U8sd4#v~*yKHNB z7TN8BQ8c^-o(NWc#JCGRV25c+3SZRi77+PkP%$tj+ZQ-by}w%v5-h&H3}rXzv@mN& zlzQHPYwgsG|5IC?dO#8dvJG5>awV=5y2FDve(|IIMCXAL0`dK`7hUmxGRZ(VLWTo- z|GXZzmrNpWmU{JH~W*D#JO@|_uRxQ~!-1_QhG`dL6>>wm?XQrm>8~&Vh z`Dc^=w2Ri0uNF<2y~u+DxlT2Zau!$=2ltrWk~}27`GPiGtqYzxav5L))DKjT9a-^yvAg*%Eg z`J$-Yyu{MHHt>=CI{HX<>rIap0P$}})ZD`Wd4!Yddc_M}^*l@JG5@f--eD4ioH8rN z4frn)wV~(ho4?b`Pk*CVZv3XNYN%>>FnZ$Bw4>5G_~)4vQq6Um*Xay4WCswHq5_L7 z^HGmspus82IC(vk87JXcO(N{Wo+1D4W#~#_)o}ib(@10xyh6N4=2X2Qkla330yyBj zR3IZ$2&z`@6WRp1I~`8#jAl|=oqjlw=Y6vMMP#MD;J-|9j+37%3S^9Bkn1gQnjj(x zAi_CgUG)98FR!+-Kcp%@y*LqLV*jyU8QQMSmT_-a(dw`x?yxcfA67?#QbS{hqu~!{ zDWU4oQ2juoP$3FE-|=Vu5bGhV{|%X#?BNk})A`b@hCCz;!heWcb$&cEqE8~IG=?(f zeKH%%(G<_&l4o7vay?+7=dxnr*xltX4xg6u7WnRR?7qJ!TLe>y^gfx9qOQB92G7d> zN~GDX-bEo3JWhvc>uXCUvQUGkUmoWJ%?!4jf71I}Y+nIC7zp4*YXIlr40q+0&&E#< z&3YiF3nNlnQ}5M^jiwF?gt`@JHVidyG7)GlHqhWf2fc0?8sdlth#Pvoy=<+E6Vk77 z)$0B9l?0NYW|9Qqc!YF`7m#_Ml#xE@$H^y@|k3J-ZJdCID)&*VQRj86`u2cYW}vZi8atbC5Sbw|i^d}(+x zU62!G_Qa5fvOvIUVOrUvC$Xz1JXAw|@9)iiwM0leZz*&V#e9po;2`M?^o2Wf+x6@8 zGq?~*sL1Isw{s~0NWRm`vcK4{FohqbzRdk2^Fuqtawp$Rw@#WE+z*=(zmn0}ZUF)B zexzc8OiWCam9@oip_E>()y98T`VkdC;{t`bhYueHTcy2iA|5fk?fEb_Mx7wGm$>*X zGu>6!9Ff~B(S+!ve#?gWrz*h&K$q1B!+UT2$9CF*0U+;v@KoYu(dj5JQ(SBuHj0MP zhkMn3yKVE>-)S{5tjhPepJh>#%0JAH=)I*=^0Ou~$3oF?m#B7)HxOf0@dHwuQ`B^Q ze+Mfdpq=mjJk=!q7shf2_2}^{An~z7N&MGEuv7=Gzp3!V-<&HJ?Z6MTQttVor@;c> zPMGd-!^lS-|Lpp6-tX9f^FC_ndwq+u*dpl6@`VRzGc9ES0emJyW)_IwPN^qcD&((M z`9tp}mwuMjTG)~%HoP0RzC}BL2|<#vin7l+`@Kg3#99Z)0unxN@2N2FiZCBuP641y z_zzWK{C~CL5&OG2AQz=R(*Iom_KukkK(1Jv{zJ~hVgWmg+Gcqxj4yGFy_-ztzI}s5 zyIb0GuQEnk{VjcQ0@(PTx@WhzUoIxUg!{0lZCNXe5O4f6DKq$lwvKycCAga$1mGi0 zy2D3COtlHOPIE9VkN~PV3(X-Rid_oF_P$4^rL&LD9~fpd{D5 zvIEyRa!^RpEgwt#4fLH!@-K!Ia5)IEfzUB6u`Bbh`fdddiQns>+WFr;`1*#L-NI=6 zfzAUXSSaW}H}YoM|MdwlwUqoL@G_1Co=~5`Q~1xJx%pj=j^8;RjH!o$S` zS?FxN2iNgb`_aEk{NUe3g)o&P;+Yv+vI*f3=>f z79jlH&ep*<8*yX&xV-${q^j=cf5y+nf<;-C{M&;Kg&QMT!2jUybF^PwfMK=VWcld+ z?sCq@oBqy$@&AD4H@jxe0z476qTopN=he%Vzyr2(VCcURi6aPTzUZL2!}cFg`;MO^ z{i`O#TiYj)TX2`E1TZR-rS(7f+Qd_!Bw^$B>EKq=Z2%ey1JF<_%IexrZ(0nLwdolc zGD|;naWIe7YD5S;Tz>agw*J4$8pvBN0xT7lG0{o4{ur+84?1+v%4$T`F@Byqu^7?Mz@zUDm49YCaoa&DfORDF00v>;Cb{$r^&8}t`eQ~eCstWdWkt6S_aJuoGqe_`LiBS2=! zTXmPoZx!+X(af~@z!Rk#uJ#;%hxC0l1Kd4CA+OZms-q3h_JY@{MsMc z6Bi7Xz8-gq`un?gPN*i#knyAn%j&#`#Kbay-2i%3WM3Qj0pRse)#dmzp)O8+;#E;y zh*{hUF(gKB=bwoFGuaP?MK`@XsYI-?NR!t?k4 zuHwfzut)^2G5+nG{fW)LplAR#lYHxHzU|*}SfX+%6(msT+#W}m7$9p((~@j|i|5}- z0n2eF1$>3z&zn0<0CfjyxBvw$XZ%}*R= zHK2cVjUR{z7^Fh5@2^J!++h@8o_8>!|E_=FJs>yaTZK#iR({8C1K2XPxv#>1P2&Hv zg?IcsfS2a+df@#-cW?uuq~`x^>woo|IWSM$?Qn;Nzr%ce3(Pa}-fgp>A0;3e-H{Z% zCV%c0P*Mh_afZ9Pe=BXOz$dA1#K|9kkrDO&!n`ik?o zbKt^K`I)0;fdNQjZ|DZ4LOU{36g2j*1F(j(K*gE7^eSzwH)$@b9h)5NTGMn>=gM zwiPQ_aq5MH%NMKF8Jkq}Gb8~@7SjSz5JKbX!ks!szO6w_WpyXcl6GDuvsK`)B$_|F zIr0?+hva|9VT7t(YuWtConyDEdethNC8=Q?O%pEPn?FP@C|OE{P;ZLe`cNYTba>i+ zEP7uElp|Cf3;xX}@J89c+j+S5uU}1#M3cuTgyox%{_3@@SfTz}=cVYXy1PDNrdR|O zYP>d`>%uwlVY_ljrE%1PF~7npxxn1GdW`=Y&FA=u_ihG&iPQUQ;%d_BrgxnR>grnN zhU7Jt)!qWd$m>{0Y@&BOy{T4wJR382uV`;gl=ASK@(fiHb}iGYn-W$d&C|caNy`gp zKF1{yr`SJhUH;;G>|bHO=%w|-qJf1=Ut8R2q99Yt>1yJkz5BDlcezgarbFI^JVRF7 zuB9c|b&H9-?Ln%*O+GMpp#S@dazN4)=6u-xSUnMa+&j#)D<$Zd>Uxu?eW5|VTPh#mdU9`NB{_Woc; zO4k=MPLr;o%>51;BAXC4UG-?_s%(egR&mnd=9H9nr8Qw0ciGF_M^B<88#wj3ekHT#B{= z7}t_!^q-X$zIQ2T=gu{9ZW|cG!VK*w%Z(T3&GMDaw*!iUSCc@Es#M}cp)>%yPMeE) zzr>Ms-s9Iohw(@m`+2i^mF*@xTw6SUE~~PA6p2Y~U<0Iw8aZar4CcHJlD=?P)B%KJ z%ZW)`r+oaE#r{_Hl4JdimrsHETMc;HnNeNWMU8*6M}H9!Q&tnb6!r?-u~!2hZz%-bEwyvD(K zqw?N_mj1hho2atA6!>OV2jW*8sp?t|T7@)aZ1@9@?m~0)1Ilej-&BqdjBISoD)Dd{ zXh4BVii%a|`r5fX!O4UtKL1(9(xRo2O*KLxZgZ2pMysg{hb-! zub4Zv>FVNaR{?uN@SwoH$20K}9SyAViO?w9JpuNUQ0(1sL4#o7oj~E5E~1EtoCBwM zUj*quO1`e^)YzQTR)1Jy8uimY;cMlytuprQ$X(dB-~q2(pZA!~__O_{8DWBS~O z*8m!TMmr9N^JJD+EcwHJl|loj;IL1N=3To^JL0NzQaP{;@GlA|5O;}7H& zB869$7O>35FIHiN2YtfgJe=H>c^sGhBfn~{!aZ%m*tVkqd>wABr4e8|an+8sqEV}< z?Q>VO)<946ebx~mZSN!Kev6YLfUJKl&%qn)jWB5i~D ziG}<~_1$Gf&MvJc3~CFPo34(1fgJJ1+gBHeg#8liik`8$xwX+jrOlHU)er5pETZ48 zgN4T|?Go8-XAAwEZO4R$k~CU@!W6(KiYL&5Uj^iA`Ygw4hP;zKCi#6jkGf^A3y(#J zgpZniWfS*wc@OvB{CvQ+IhbB+k@l^=f*e3f{jTJO1)zcGomTsz$Mk7f0S=1NvErRx~MOwX>TOA z#;&qaZHllNlndw$*DkR1d2V6cT51AciORRFthfVh*s=!NPQ)MAw$#*rcRwKMtfldx zm)Fn=C@!{Lrx+O|k^5$6WeK!-5FcFIyj#Z_QDa+?M#}>X@=kRQ4nIXn0UhsFhvnl@a>F%DJC2bL@GNmjYOj%dfrnC z`aXkXS<%ydAv`w4a6O-GDtB;C_E43gBAFM+i?Mv;y30%-Zr7+7B^`Yo=Xe0utU4Nt z#q}&;zp9!U0-F+{n6Kr~_#9TtaZO#B%=)IcVr&)~&3-IPwr=c=8_JL_f4ur;s)Oxf zr3!oh{MU(z8WB&j!$wI-{?(aQ*;;2uN?3Fk7-_S!CTB5WsJ7nFT$3fAtL(o2x-y?!OMOfL-uQobAz-f!1BT2)TW6V-|u6Pa@x z%BtV&kOTATpGA{;`>l$4Z`zNxtiSC=RIGDCJ;S#>uncxPG9 z;pIqr%1eeZDurvsm19rpIMM5UpZE!fi$XD!UUUdvG+3jc_-9e=hL^=yWmuBy#f(XZ z&B~9f6X#lZ*B$YZC!^6A73-iU7v;~joLeIURw$IrDMD!)${Q$taq6R$i8UK=Ntxq->JD)qBw<_ zH1E}H7Cbk@@IPMa%T9LKXTP14V zuQmsA>9-LlB969eyq>`5J}-lqk2hc5$CD$7QuuZV!BsY@3b~3$9G!p1t>0twWb42c zP^}Ec?rk{TpcwpGT^=JYFn0Wf@TTP~6*AtxU)78<_Uc(jAbHzDH zZyK+Wk8~=5M7HBtQq^f=+pOz_@IJscD${TlaJfnm^m)hXP-nLrpbn+mD1P58rjrP= zDyRC;bZ5c@LX{bEhR!hvkQ#HFbgQ~A{M6{Bm~4lqfRQF{*%hNle0Ps0R9xq5)?%nO ztjvyAp2}H_L6fH4JLJA!c~|wuwm`Za=en0tirQ8B#^kD zaB?$E>S;Wquo>6Iqxev4-Qcco*DBL+y$hb}F6(c!-mTSZ#M4eJ248RYB(md2FD^!X z5s1Iusij%wFKg5XUx)*=Beo}1%@~_g-grWXuOz6j`f@@A*0(`CgLsmug7WnD% z;G)KzZsVYG_)*zv?y6bc2-oNtIZ|XKdqeevEu*bdzE1~H=BAO0C+bh0KGHb4t@H%L?{Vdmc5bMt!`Vc z<0^>K(bsy?jHv6${CW2!OXKOjRhr24<i-r8KPo8krM%$(-Wh9^dO?s@>xE4kJ;G8oa7m)2_0%OAqi$PxNXT z0fI2m9u0tSHs8a?+6E0oa04ktL#9e`Q=TET*V&uT;r8|e>Oif{N9ReE@bdxz>W-dx1YL?>d*+IF&>t z`un%a{&0WeRPp(EI$j)LFIth88znmpN95P+@6MgPTfsY=MO7&#R-bUmyqwKN1&9iM zx@4713_NX%pbVg;nI@yYhR7`lWF$mlC$2laOuHCKqdFh#Q${Fj)nPq!0ozr1F zztDlsTQyAHKEt0m<^u-kRBHP<@u{+OOpG;?gSo8fo=mxKZ-E4b-Nb+Mk5%y{TU)wQ zdtT<+x=d@PtO(jQO7l$SNKFe=&07(vr#P&l+wv@Eio=MrK7JG|8c$jIM8TkyL$`KD zs<(hC7V=rwW##AonEtiuImu5^d3eBP;0(P7zA`R(n#lWe+575`25j&J7h$T26D|>9 zUbGKiZL~;9iqL6q=9eyL5BD!5dv0lKy&TmL2QC-+2kQ-N7%h`h)6rIF5xgVC#|Fo8 zz;Uw6KME;My19OwxQQ~RJlrFjNV)OjwLb>tq*Tl?u^NSV8jyVpk7m87+uG1E9$aL= zPGkfcTZ-5k_Mdv@1yPk2*@Qn5+I>j_rem)Ynq(WYm%GcrYkpAAnC#xwr{^`B%VX@g z>0FSS5NIXPW@|+%*hgz162e`S=`D1jDL! zMP>Zx+u^4%0^gkm-(8rNGPCo`s&8&x9c^s8b#!mqdA~0@smW)1(aVtdGdVel=IGKY zYYFMFI$7KOa}8HoGcX`b<^AY&7CfU0N#M;)FJ1?PMQu-BH?1VFyP6|ZU0=wPR-73o zs^^b`FG{yrKMU>(ygHl<*%-<5SW0x@IG@_%m<5;??*FQ-2D$uRApG&`tySnYc%IPp zwF#YsbAETWCDZ)SV+&R&qkJJDRe4t1W>NUPt0*YEzj5C2YfFd+gR z_{5TlE_lz)e-v*>8>ubG!1s%Nt6%6SxSY3l#b5k4>9z4y=nSBTY{Bt42XXlXfNkqkTeCb!5Nu zi#meT3u|`-TxStj!({bt&7R4C5-#3~p<5eIaB{_rw6iRF9hu*}G`d}<0k05l^$1rK(uX!H{b}DXN!Pb^FEoVk8Z{{TNVb!^Vu47cZ z7xJWLiP}0qJ3wq<7@61V;kgQ8-edcyYT1J*4;_s~R>%Nw*5|^!8#^Qy z+^s-UUgIyt?}t@ANdP-numy9y+yov~3ytYz*khte+iS?&({Ag7W5O%Z%VV3V?yP#= z$`0VOY<=PK=oz)O6mi#AGcy;*eN(*EoMoJXYx@Rso+mSxv%JN-yldYt z4EwAVj1;LJjB?sPu65kTV33c}?7KifoN8D$;yXTB2_uOM=AJ6x@hSyQL68xJdZeQ| z8bx}LmX>RJ)ZQi0DH}m#?CS6Lcfekn=u~{ppOU5HqwbM(GXErC_S;N&p)$+JM^D^I zgM^dy)i-ZtX{Liuwz6d*(6=fJJ^dzMM@7O7t(!ns%j;#%i^~=@4oh+N*l!tF5RNre zl*qmr&k;!&BRwKVJ$`?sVrIyDDjaOS7&DhqY`%hrRZ=1i|7~Z~Taq=OjLBR?IZ-BA zvLgomz(bgQFV|qEC;&bEh}S}+P2iY9NT#h-hM}OtQChNWjjd8J3tAsIbVR|Mb0{T| zU|ZIK(yb}lJEfLtRcT5RRTr%?~+jL zOSNPnANrp~QV}%LFOjQ-f~9*x)rHscvAp%ajjqnaMrv#r$vojRP5V_lbLqGj$3x$^ znX%YHJjiux>FSeaCKuJvztap9VG=)Aepj@*SIvvgra90wgux#27Ln(p@Bv#5d&i?# zQyDu=Rbg*2mr?h{yR%|1OV)yK%Av5YZ?RZkSRI4bp&8vVOlqL5I}=K}-RYcuaBFUr z`75uHQWGLV5!$i>d_vRh8r+d|Gw@O-by~+^ZsQvVo1p+gRmtkn!i1sP-8u&JK=|Ux zJ6JtkGQniw+>czk`#*#VRQ6ZkR~~ zoX^FKX_%S;v!84P0$F_MI#5MZNJiV_TM`fbUb`iVKs-4EEn@szjE0f>^BX!Pb^U1 zcG^=QK}xXr*j?+wbdR@bsJqd(?^6!Q_TjI245d8R>}huFV~Q_qptW`(?Qtem@k$nS z-mt|g4o}9+7vbv{MrE)GP+lo-& zTORUrf`MoAi6{x9vywge&KO*n18dbjhHH4bJ=SQ~n@#o+GH{ zEmj7eERDUgbakW_A|Q1An|@YT+T);4ReqtrUWj|IPzO5+8?^cr<*UaSW+X=(7%l4b zN2W89IZPd1FaV#1P!xThYsN1WBuG8I|Fy=|oX)4Dlvs}N5we(oRV7Q#;PFsm-&UAL53dcVx9*#rSe#vFTV{lxcGA^~h5T&BAw z-|$SirM`l8-H39isEj~jNm+w%Zaa^_o(@Cop>quFn@aO3U6R+7!a`-=4H^A5rJzHE zbk(}$8koIsKeDhsg$Xi4ZoBe``>p+v< zMqKL>4u7dio@)P_=Xd?mwGJvJkv;1~&udT9Fq?-PPfN251`Q(;*&h8MY-`*YPp#;^ zR?FM-qt^py&llo`IyrGn!_nC4_%=y+2spA7sI7>cWRB9??zw#!73cT&%|;5r-Xj@W zR*xxIN=?0IVeNIFARDe z`JZsSU2(4d!R z2SYCi$NIki?itIcd0r6rTvzO~hDY^!%l1RDZ!*Vi&YI9stAU<&j-Cyt)X5UJ@2qcn zQ0{`cY+Lj27Qgd1=au-hlHgsmI&r^&Ik0J$%ZcciYADpp2Nwr#2cezJ@f0R*R;^U$ zqmqiGJi~cOzRByo@d3^kQam}LYeUL@-b>(|?%YQzJPnA5#b@szy zd6{DN$^Nd=TJkAIW<7ycu~mect{Ez%w&#U9R@|^@FQm2ogI;H{L^2J(>(Kj&ItSKf zj@q6Md^E4NgRBQEcT<|Uiy@YA(uAe@cPpMdAEp&D($fNGW7N585gsGR*sPcwg)qj zj@t@%S0V28&>h=YAW32Y!?G4J*^ndW@hWG*z`@ik)4ZZd!@+n~_hG?WhOvc1lR*>Q z)T>xs)~ubbuuU$hB!vp`!6)9Trp{~kIHG<97?YqU#g1+x0g=fwg34V{Kh+S=I!0{- z$Q{zYS3{{p9Aoml)pRQTAZspGBvKarT7e=wcGw_wTjz7WhMe=l5;oJ&CuOn;T6c!0 z{3>)1MV4eX3&~pZAGzn6UN1B{Pr^TAgP+-J~+8WYMHLYS#s;(Ok&{EN{GXW}0N2o0szu^n1tHgkm+q+-XW z#F80NN#AgoWSUoC`)}Y#S+@^ByVTwvBrJIK_4kA65*s3!c=~E_5gE-=M*CEbX$&2nTq_ufSOMAIL6Ng+Fx^|$ z#8G1C`vuexwvWcWQSk}fc_vu0Ib&sWFD(4_L0R`lQhVJ>MSOA`<|&U7xN@GqDQSZ} zfrT7xh=T6v=cafE61cr(@D6%imYDG1@jG>`x49lrN&^WDLJZV_0c7!y5XLnjb4^&6 z)37AHdX#-O7bA0|3@Pm0Z1T^@o7+P==@dl9k{%eG#2q>7vBaIHmi~Efn+2^k)o|^# zW??45l;z3RQ1|ImtA!{vv@wkg+m=O{>YPQLT*7p4tltq`-toqRv))t|>$MqKIy$pA)1~AwcOYskUOdCA{KPF6Nv~Cms}p`QyHY!3xS2yxNj>BJFvcmk>vdTZ z@$0#}exYqR?8;sX8*0CfzNr4HxPRDK-qktB_PJYk0CB2g6d*{C6dSkHf__{G41NTf zS?i051L3^!Vv>^9`t;tc@p*I$7~fuXHrBI&07h_~ZMaf&ABPGigG>4FY-NOx(2co& zvq%e1k@S86i(F5vb12aPPA!{ZyhN_U5+j-4N>T{@FCvr$<`Di|W0F~C1LHnBm92(l zdUe%Q`b;&><(vX0v^b`?;m;DDH<$R z(~_X9B=Bz5yFs@=m8DnwnmU;tPc!rABbdi7>0BRx==!DZ z@7M;D5oi#=?~T56cWks&!MuLNYd5TmWSnQhN8FjTxe~Wq{|q@v;>}qdOUcz4kwi=C z0N?Mn?1K?H7&0iYqiQUC+#Mv1eSA$cys3k5k(hT3n#izl`qgg(hI0q3JkmQn`ZROq zaYRE`mVnI+h0qtk0>dA%2hdr^&s6vq^1u8*%SP(TP<}5Vx2mWQks$tdbLue%TX4

1NzFg> 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 literal 0 HcmV?d00001 diff --git a/docs/drilldowns/images/explore_data_in_chart.png b/docs/drilldowns/images/explore_data_in_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..05d4f5fac9b2fafe808e72ff0ab2c5e0028f33fc GIT binary patch literal 99870 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

@1p8Xrj8Q!w#bssBL9{8 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 VWGX&|iQ!=%RYeVWsl0j6{{Z*TLa+b; literal 0 HcmV?d00001 diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index a812d4e3bdd2d..b812af7e981bf 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -160,6 +160,7 @@ When you're finished adding and arranging the panels, save the dashboard. . Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. include::{kib-repo-dir}/drilldowns/drilldowns.asciidoc[] +include::{kib-repo-dir}/drilldowns/explore-underlying-data.asciidoc[] [[sharing-dashboards]] == Share the dashboard From 679209b308d70bcdecdad2eadf3dc21e81eae8a3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 17 Jul 2020 11:38:45 -0600 Subject: [PATCH 57/76] [Maps] 7.9 documenation updates (#71893) * [Maps] 7.9 documenation updates * more cleanup * add vector tiles card to tile layers page --- docs/maps/heatmap-layer.asciidoc | 4 +-- docs/maps/maps-aggregations.asciidoc | 4 +-- docs/maps/maps-getting-started.asciidoc | 35 +++++-------------------- docs/maps/tile-layer.asciidoc | 4 ++- docs/maps/vector-layer.asciidoc | 11 ++++---- 5 files changed, 18 insertions(+), 40 deletions(-) diff --git a/docs/maps/heatmap-layer.asciidoc b/docs/maps/heatmap-layer.asciidoc index 7149bc5623169..9dc2781db44a3 100644 --- a/docs/maps/heatmap-layer.asciidoc +++ b/docs/maps/heatmap-layer.asciidoc @@ -7,8 +7,8 @@ Heat map layers cluster point data to show locations with higher densities. [role="screenshot"] image::maps/images/heatmap_layer.png[] -To add a heat map layer to your map, click *Add layer*, then select the *Heat map* layer. -The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. +To add a heat map layer to your map, click *Add layer*, then select *Heat map*. +The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. NOTE: Only count, sum, unique count metric aggregations are available with the grid aggregation source and heat map layers. Average, min, and max are turned off because the heat map will blend nearby values. diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 872ed1cdedb7e..6b2dc8077bc30 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -47,7 +47,7 @@ Grid aggregation layers use {ref}/search-aggregations-bucket-geotilegrid-aggrega Symbolize grid aggregation metrics as: *Clusters*:: Creates a <> with a cluster symbol for each gridded cell. -The cluster location is the weighted centroid for all geo-points in the gridded cell. +The cluster location is the weighted centroid for all documents in the gridded cell. *Grid rectangles*:: Creates a <> with a bounding box polygon for each gridded cell. @@ -60,7 +60,7 @@ To enable a grid aggregation layer: To enable a blended layer that dynamically shows clusters or documents: . Click *Add layer*, then select the *Documents* layer. -. Configure *Index pattern* and the *Geospatial field*. To enable clustering, the *Geospatial field* must be set to a field mapped as {ref}/geo-point.html[geo_point]. +. Configure *Index pattern* and the *Geospatial field*. . In *Scaling*, select *Show clusters when results exceed 10000*. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 09a4dc61cae28..e0d43a571a331 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -68,40 +68,17 @@ The first layer you'll add is a choropleth layer to shade world countries by web log traffic. Darker shades symbolize countries with more web log traffic, and lighter shades symbolize countries with less traffic. -==== Add a vector layer to display world country boundaries - . Click *Add layer*. -. Select the *EMS Boundaries* layer. +. Select *Choropleth*. . From the *Layer* dropdown menu, select *World Countries*. +. Under *Statistics source*, set *Index pattern* to *kibana_sample_data_logs*. +. Set *Join field* to *geo.src*. . Click the *Add layer* button. . Set *Name* to `Total Requests by Country`. . Set *Opacity* to 50%. . Click *Add* under *Tooltip fields*. . In the popover, select *ISO 3166-1 alpha-2 code* and *name* and click *Add*. - -===== Join the vector layer with the sample web log index - -You now have a vector layer containing the world countries. -To symbolize countries by web traffic, you'll need to augment the world country features with the count of Elasticsearch weblog documents originating from each country. -To do this, you'll create a <> to link the vector source *World Countries* to -the {es} index `kibana_sample_data_logs` on the shared key iso2 = geo.src. - -. Click plus image:maps/images/gs_plus_icon.png[] next to the *Term Joins* label. -. Click *Join --select--* -. Set *Left field* to *ISO 3166-1 alpha-2 code*. -. Set *Right source* to *kibana_sample_data_logs*. -. Set *Right field* to *geo.src*. -. Click *and use metric count*. -. Set *Custom label* to *web logs count*. - -===== Set the layer style - -All of the world countries are still a single color because the layer is using <>. -To shade the world countries based on which country is sending the most requests, you'll need to use <>. - -. Under *Fill color*, change the selected value from *Solid* to *By value*. -. In the field select input, select *web logs count*. -. Select the grey color ramp. +. Under *Fill color*, select the grey color ramp. . Under *Border color*, change the selected color to *white*. . Click *Save & close*. + @@ -127,7 +104,7 @@ This layer displays web log documents as points. The layer is only visible when users zoom in the map past zoom level 9. . Click *Add layer*. -. Click the *Documents* layer. +. Select *Documents*. . Set *Index pattern* to *kibana_sample_data_logs*. . Click the *Add layer* button. . Set *Name* to `Actual Requests`. @@ -161,7 +138,7 @@ image::maps/images/grid_metrics_both.png[] ===== Add the layer . Click *Add layer*. -. Click the *Clusters and grids* layer. +. Select *Clusters and grids*. . Set *Index pattern* to *kibana_sample_data_logs*. . Click the *Add layer* button. . Set *Name* to `Total Requests and Bytes`. diff --git a/docs/maps/tile-layer.asciidoc b/docs/maps/tile-layer.asciidoc index 6da8dbad0a66d..2a60504c3c790 100644 --- a/docs/maps/tile-layer.asciidoc +++ b/docs/maps/tile-layer.asciidoc @@ -7,7 +7,7 @@ Tile layers display image tiles served from a tile server. [role="screenshot"] image::maps/images/tile_layer.png[] -To add a tile layer to your map, click *Add layer*, then select one of the following layers: +To add a tile layer to your map, click *Add layer*, then select one of the following: *Configured Tile Map Service*:: Tile map service configured in kibana.yml. See map.tilemap.url in <> for details. @@ -16,4 +16,6 @@ See map.tilemap.url in <> for details. *Tile Map Service*:: Tile map service configured in interface. +*Vector tiles*:: Data service implementing the Mapbox vector tile specification. + *Web Map Service*:: Maps from OGC Standard WMS. diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index d6a5931659a40..494bd915b7f56 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -7,15 +7,14 @@ Vector layers display points, lines, and polygons. [role="screenshot"] image::maps/images/vector_layer.png[] -To add a vector layer to your map, click *Add layer*, then select one of the following layers: +To add a vector layer to your map, click *Add layer*, then select one of the following: -*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell. -The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. +*Choropleth*:: Shaded areas to compare statistics across boundaries. -*Configured GeoJSON*:: Vector data from hosted GeoJSON configured in kibana.yml. -See map.regionmap.* in <> for details. +*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell. +The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. -*Documents*:: Vector data from a Kibana index pattern. +*Documents*:: Points, lines, and polyons from Elasticsearch. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. NOTE: Document results are limited to the `index.max_result_window` index setting, which defaults to 10000. From 35ff37a434e61d2475428ff474fce50ee9bb68b1 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 17 Jul 2020 14:02:52 -0400 Subject: [PATCH 58/76] [Lens] Fix switching with layers (#71982) * [Lens] Fix chart switching with multiple layers * Unskip Lens smokescreen test * Fix types * Revert

change --- .../config_panel/config_panel.tsx | 2 +- .../config_panel/layer_panel.test.tsx | 6 +- .../editor_frame/config_panel/layer_panel.tsx | 2 +- .../workspace_panel/chart_switch.test.tsx | 53 +++++++++++ .../workspace_panel/chart_switch.tsx | 7 +- .../xy_visualization/xy_suggestions.test.ts | 92 +++++++++++++++++-- .../public/xy_visualization/xy_suggestions.ts | 16 +++- .../test/functional/apps/lens/smokescreen.ts | 26 ++++++ .../test/functional/page_objects/lens_page.ts | 28 ++++++ 9 files changed, 214 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 7f4a48fa2fda2..73126b814f256 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -129,7 +129,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj="lnsXY_layer_add" + data-test-subj="lnsLayerAddButton" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 1f987f86d3950..9545bd3c840da 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -93,14 +93,14 @@ describe('LayerPanel', () => { describe('layer reset and remove', () => { it('should show the reset button when single layer', () => { const component = mountWithIntl(); - expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset layer' ); }); it('should show the delete button when multiple layers', () => { const component = mountWithIntl(); - expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Delete layer' ); }); @@ -109,7 +109,7 @@ describe('LayerPanel', () => { const cb = jest.fn(); const component = mountWithIntl(); act(() => { - component.find('[data-test-subj="lns_layer_remove"]').first().simulate('click'); + component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); }); expect(cb).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index e51a155a19935..f72b1429967d2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -429,7 +429,7 @@ export function LayerPanel( size="xs" iconType="trash" color="danger" - data-test-subj="lns_layer_remove" + data-test-subj="lnsLayerRemove" onClick={() => { // 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 diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 648bb5c03cb39..ceced2a7a353c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -46,6 +46,16 @@ describe('chart_switch', () => { }; } + /** + * There are three visualizations. Each one has the same suggestion behavior: + * + * visA: suggests an empty state + * visB: suggests an empty state + * visC: + * - Never switches to subvisC2 + * - Allows a switch to subvisC3 + * - Allows a switch to subvisC1 + */ function mockVisualizations() { return { visA: generateVisualization('visA'), @@ -292,6 +302,49 @@ describe('chart_switch', () => { expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); }); + it('should support multi-layer suggestions without data loss', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a', 'b']); + + const datasourceMap = mockDatasourceMap(); + datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [ + { + columnId: 'a', + operation: { + label: '', + dataType: 'string', + isBucketed: true, + }, + }, + ], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a', 'b'], + }, + ]); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); + }); + it('should indicate data loss if no data will be used', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index fa87d80e5cf40..51b4a347af6f1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -139,7 +139,7 @@ export function ChartSwitch(props: Props) { dataLoss = 'nothing'; } else if (!topSuggestion) { dataLoss = 'everything'; - } else if (layers.length > 1) { + } else if (layers.length > 1 && layers.length !== topSuggestion.keptLayerIds.length) { dataLoss = 'layers'; } else if (topSuggestion.columns !== layers[0][1].getTableSpec().length) { dataLoss = 'columns'; @@ -258,14 +258,15 @@ function getTopSuggestion( newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { - const suggestions = getSuggestions({ + const unfilteredSuggestions = getSuggestions({ datasourceMap: props.datasourceMap, datasourceStates: props.datasourceStates, visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, subVisualizationId, - }).filter((suggestion) => { + }); + const suggestions = unfilteredSuggestions.filter((suggestion) => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index f301206355060..f5828dbaeccc3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -13,6 +13,7 @@ import { } from '../types'; import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; +import { xyVisualization } from './xy_visualization'; jest.mock('../id_generator'); @@ -119,7 +120,33 @@ describe('xy_suggestions', () => { }); expect(suggestions).toHaveLength(visualizationTypes.length); - expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([ + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ + 'bar_stacked', + 'area_stacked', + 'area', + 'line', + 'bar_horizontal_stacked', + 'bar_horizontal', + 'bar', + ]); + }); + + // This limitation is acceptable for now, but is now tested + test('is unable to generate layers when switching from a non-XY chart with multiple layers', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: ['first', 'second'], + }); + + expect(suggestions).toHaveLength(visualizationTypes.length); + expect(suggestions.map(({ state }) => state.layers.length)).toEqual([1, 1, 1, 1, 1, 1, 1]); + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'bar_stacked', 'area_stacked', 'area', @@ -156,7 +183,51 @@ describe('xy_suggestions', () => { }); expect(suggestions).toHaveLength(visualizationTypes.length); - expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([ + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ + 'line', + 'bar', + 'bar_horizontal', + 'bar_stacked', + 'bar_horizontal_stacked', + 'area', + 'area_stacked', + ]); + }); + + test('suggests all basic x y charts when switching from another x y chart with multiple layers', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: ['first', 'second'], + state: { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'bar', + xAccessor: 'date', + accessors: ['bytes'], + splitAccessor: undefined, + }, + { + layerId: 'second', + seriesType: 'bar', + xAccessor: undefined, + accessors: [], + splitAccessor: undefined, + }, + ], + }, + }); + + expect(suggestions).toHaveLength(visualizationTypes.length); + expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([ 'line', 'bar', 'bar_horizontal', @@ -165,6 +236,15 @@ describe('xy_suggestions', () => { 'area', 'area_stacked', ]); + expect(suggestions.map(({ state }) => state.layers.map((l) => l.layerId))).toEqual([ + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ['first', 'second'], + ]); }); test('suggests all basic x y chart with date on x', () => { @@ -388,7 +468,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(visualizationTypes.length - 2); @@ -497,7 +577,7 @@ describe('xy_suggestions', () => { changeType: 'extended', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); @@ -536,7 +616,7 @@ describe('xy_suggestions', () => { changeType: 'reorder', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); @@ -576,7 +656,7 @@ describe('xy_suggestions', () => { changeType: 'extended', }, state: currentState, - keptLayerIds: [], + keptLayerIds: ['first'], }); expect(rest).toHaveLength(0); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index e0bfbd266f8f1..d7348f00bf8b8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -394,17 +394,25 @@ function buildSuggestion({ : undefined, }; + // Maintain consistent order for any layers that were saved const keptLayers = currentState - ? currentState.layers.filter( - (layer) => layer.layerId !== layerId && keptLayerIds.includes(layer.layerId) - ) + ? currentState.layers + // Remove layers that aren't being suggested + .filter((layer) => keptLayerIds.includes(layer.layerId)) + // Update in place + .map((layer) => (layer.layerId === layerId ? newLayer : layer)) + // Replace the seriesType on all previous layers + .map((layer) => ({ + ...layer, + seriesType, + })) : []; const state: State = { legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, fittingFunction: currentState?.fittingFunction || 'None', preferredSeriesType: seriesType, - layers: [...keptLayers, newLayer], + layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer], }; return { diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 8bb5faf2469d7..23d4cc972675b 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -165,5 +165,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + + it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.createLayer(); + + expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); + + await PageObjects.lens.switchToVisualization('line'); + + expect(await PageObjects.lens.getLayerCount()).to.eql(2); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 4252c400ff1cd..d101c9754d562 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -167,5 +167,33 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.existOrFail('visTypeTitle'); }); }, + + /** + * Checks a specific subvisualization in the chart switcher for a "data loss" indicator + * + * @param subVisualizationId - the ID of the sub-visualization to switch to, such as + * lnsDatatable or bar_stacked + */ + async hasChartSwitchWarning(subVisualizationId: string) { + await this.openChartSwitchPopover(); + + const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`); + return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element); + }, + + /** + * Returns the number of layers visible in the chart configuration + */ + async getLayerCount() { + const elements = await testSubjects.findAll('lnsLayerRemove'); + return elements.length; + }, + + /** + * Adds a new layer to the chart, fails if the chart does not support new layers + */ + async createLayer() { + await testSubjects.click('lnsLayerAddButton'); + }, }); } From 3cef292bbd52edb94d87ec73401553e8450a8736 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 17 Jul 2020 13:35:25 -0500 Subject: [PATCH 59/76] [ML] Fix annotations pagination & change labels from letters to numbers (#72204) --- .../__snapshots__/annotations_table.test.js.snap | 1 + .../annotations_table/annotations_table.js | 13 ++++++++----- .../public/application/explorer/explorer_utils.js | 2 +- .../timeseriesexplorer_utils/get_focus_data.ts | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 63ec1744b62d0..9eb44c71aa799 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -77,6 +77,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` "dataType": "boolean", "field": "current_series", "name": "current_series", + "render": [Function], "width": "0px", }, ] diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index cf4d25f159a1a..86398a57c3a45 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -13,7 +13,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import rison from 'rison-node'; import React, { Component, Fragment } from 'react'; - +import memoizeOne from 'memoize-one'; import { EuiBadge, EuiButtonIcon, @@ -130,7 +130,7 @@ export class AnnotationsTable extends Component { } } - getAnnotationsWithExtraInfo(annotations) { + getAnnotationsWithExtraInfo = memoizeOne((annotations) => { // if there is a specific view/chart entities that the annotations can be scoped to // add a new column called 'current_series' if (Array.isArray(this.props.chartDetails?.entityData?.entities)) { @@ -147,7 +147,7 @@ export class AnnotationsTable extends Component { // if not make it return the original annotations return annotations; } - } + }); getJob(jobId) { // check if the job was supplied via props and matches the supplied jobId @@ -438,7 +438,7 @@ export class AnnotationsTable extends Component { name: i18n.translate('xpack.ml.annotationsTable.labelColumnName', { defaultMessage: 'Label', }), - sortable: true, + sortable: (key) => +key, width: '60px', render: (key) => { return {key}; @@ -644,15 +644,18 @@ export class AnnotationsTable extends Component { name: CURRENT_SERIES, dataType: 'boolean', width: '0px', + render: () => '', } ); + + const items = this.getAnnotationsWithExtraInfo(annotations); return ( { - d.key = String.fromCharCode(65 + i); + d.key = (i + 1).toString(); return d; }), aggregations: resp.aggregations, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 8bac9a51af174..d213d371f1d90 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -156,8 +156,8 @@ export function getFocusData( .sort((a, b) => { return a.timestamp - b.timestamp; }) - .map((d, i) => { - d.key = String.fromCharCode(65 + i); + .map((d, i: number) => { + d.key = (i + 1).toString(); return d; }); From 4c58018d337330348b858d32cc01834e3e722c43 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 17 Jul 2020 16:27:25 -0400 Subject: [PATCH 60/76] skip flaky suite (#72339) --- .../cypress/integration/timeline_local_storage.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index 383ebe2220585..7c047459c56cc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -describe('persistent timeline', () => { +// Failing: See https://github.com/elastic/kibana/issues/72339 +describe.skip('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); From 5356941f22ccfb358f4cffab6a20bfb5a1bc9cff Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Fri, 17 Jul 2020 16:51:28 -0400 Subject: [PATCH 61/76] [Security Solution][Endpoint][Exceptions] Only write manifest to policy when there are changes (#72000) * Refactor security_solution policy creation callback - part 1 * Fix manifest dispatch * Change how dispatches are performed * simplify manifest types * Remove unused mock * Fix tests * one place to construct artifact ids * fixing linter exceptions * Add tests for stable hashes * Additional testing and type cleanup * Remove unnecessary log * Minor fixup * jsdoc * type fixup * Additional type adjustments --- x-pack/plugins/ingest_manager/common/mocks.ts | 87 ++++++ .../common/endpoint/schema/common.ts | 5 + .../common/endpoint/schema/manifest.ts | 46 ++- .../endpoint/ingest_integration.test.ts | 105 ++++--- .../server/endpoint/ingest_integration.ts | 156 +++++----- .../server/endpoint/lib/artifacts/common.ts | 16 +- .../endpoint/lib/artifacts/lists.test.ts | 94 +++++- .../server/endpoint/lib/artifacts/lists.ts | 33 +- .../endpoint/lib/artifacts/manifest.test.ts | 158 +++++----- .../server/endpoint/lib/artifacts/manifest.ts | 97 +++++- .../lib/artifacts/manifest_entry.test.ts | 28 +- .../endpoint/lib/artifacts/manifest_entry.ts | 3 +- .../server/endpoint/lib/artifacts/mocks.ts | 68 +++++ .../server/endpoint/lib/artifacts/task.ts | 84 +++-- .../server/endpoint/mocks.ts | 25 +- .../artifacts/download_exception_list.ts | 6 +- .../endpoint/schemas/artifacts/lists.mock.ts | 4 +- .../schemas/artifacts/saved_objects.mock.ts | 54 ++-- .../schemas/artifacts/saved_objects.ts | 41 ++- .../artifacts/artifact_client.test.ts | 9 +- .../services/artifacts/artifact_client.ts | 28 +- .../artifacts/manifest_client.test.ts | 5 +- .../services/artifacts/manifest_client.ts | 9 +- .../manifest_manager/manifest_manager.mock.ts | 81 ++--- .../manifest_manager/manifest_manager.test.ts | 209 +++++++++---- .../manifest_manager/manifest_manager.ts | 286 ++++++++---------- 26 files changed, 1154 insertions(+), 583 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts index e85364f2bb672..236324b11c580 100644 --- a/x-pack/plugins/ingest_manager/common/mocks.ts +++ b/x-pack/plugins/ingest_manager/common/mocks.ts @@ -44,3 +44,90 @@ export const createPackageConfigMock = (): PackageConfig => { ], }; }; + +export const createPackageConfigWithInitialManifestMock = (): PackageConfig => { + const packageConfig = createPackageConfigMock(); + packageConfig.inputs[0].config!.artifact_manifest = { + value: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', + schema_version: 'v1', + }, + }; + return packageConfig; +}; + +export const createPackageConfigWithManifestMock = (): PackageConfig => { + const packageConfig = createPackageConfigMock(); + packageConfig.inputs[0].config!.artifact_manifest = { + value: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + encoded_sha256: '57941169bb2c5416f9bd7224776c8462cb9a2be0fe8b87e6213e77a1d29be824', + decoded_size: 292, + encoded_size: 131, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + }, + manifest_version: '520f6cf88b3f36a065c6ca81058d5f8690aadadf6fe857f8dec4cc37589e7283', + schema_version: 'v1', + }, + }; + + return packageConfig; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts index 014673ebe6398..8f2ea1f8a6452 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts @@ -12,6 +12,11 @@ export const compressionAlgorithm = t.keyof({ }); export type CompressionAlgorithm = t.TypeOf; +export const compressionAlgorithmDispatch = t.keyof({ + zlib: null, +}); +export type CompressionAlgorithmDispatch = t.TypeOf; + export const encryptionAlgorithm = t.keyof({ none: null, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts index 1c8916dfdd5bb..f8bb8b70f2d5b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import { compressionAlgorithm, + compressionAlgorithmDispatch, encryptionAlgorithm, identifier, manifestSchemaVersion, @@ -16,25 +17,60 @@ import { size, } from './common'; -export const manifestEntrySchema = t.exact( +export const manifestEntryBaseSchema = t.exact( t.type({ relative_url: relativeUrl, decoded_sha256: sha256, decoded_size: size, encoded_sha256: sha256, encoded_size: size, - compression_algorithm: compressionAlgorithm, encryption_algorithm: encryptionAlgorithm, }) ); -export const manifestSchema = t.exact( +export const manifestEntrySchema = t.intersection([ + manifestEntryBaseSchema, + t.exact( + t.type({ + compression_algorithm: compressionAlgorithm, + }) + ), +]); +export type ManifestEntrySchema = t.TypeOf; + +export const manifestEntryDispatchSchema = t.intersection([ + manifestEntryBaseSchema, + t.exact( + t.type({ + compression_algorithm: compressionAlgorithmDispatch, + }) + ), +]); +export type ManifestEntryDispatchSchema = t.TypeOf; + +export const manifestBaseSchema = t.exact( t.type({ manifest_version: manifestVersion, schema_version: manifestSchemaVersion, - artifacts: t.record(identifier, manifestEntrySchema), }) ); -export type ManifestEntrySchema = t.TypeOf; +export const manifestSchema = t.intersection([ + manifestBaseSchema, + t.exact( + t.type({ + artifacts: t.record(identifier, manifestEntrySchema), + }) + ), +]); export type ManifestSchema = t.TypeOf; + +export const manifestDispatchSchema = t.intersection([ + manifestBaseSchema, + t.exact( + t.type({ + artifacts: t.record(identifier, manifestEntryDispatchSchema), + }) + ), +]); +export type ManifestDispatchSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index bb035a19f33d6..be749b2ebd25a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -4,87 +4,122 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { createNewPackageConfigMock } from '../../../ingest_manager/common/mocks'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; -import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { + getManifestManagerMock, + ManifestManagerMockType, +} from './services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackageConfigCreateCallback } from './ingest_integration'; +import { ManifestConstants } from './lib/artifacts'; describe('ingest_integration tests ', () => { describe('ingest_integration sanity checks', () => { - test('policy is updated with manifest', async () => { - const logger = loggerMock.create(); - const manifestManager = getManifestManagerMock(); + test('policy is updated with initial manifest', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.InitialSystemState, + }); + const callback = getPackageConfigCreateCallback(logger, manifestManager); - const policyConfig = createNewPackageConfigMock(); - const newPolicyConfig = await callback(policyConfig); + const policyConfig = createNewPackageConfigMock(); // policy config without manifest + const newPolicyConfig = await callback(policyConfig); // policy config WITH manifest + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({ artifacts: { 'endpoint-exceptionlist-linux-v1': { compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, encryption_algorithm: 'none', relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, }, - manifest_version: 'WzAsMF0=', + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', schema_version: 'v1', }); }); - test('policy is returned even if error is encountered during artifact sync', async () => { - const logger = loggerMock.create(); + test('policy is returned even if error is encountered during artifact creation', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); const manifestManager = getManifestManagerMock(); - manifestManager.syncArtifacts = jest.fn().mockRejectedValue([new Error('error updating')]); - const lastDispatched = await manifestManager.getLastDispatchedManifest(); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error('error updating')]); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastDispatched.toEndpointFormat() + lastComputed!.toEndpointFormat() ); }); - test('initial policy creation succeeds if snapshot retrieval fails', async () => { - const logger = loggerMock.create(); - const manifestManager = getManifestManagerMock(); - const lastDispatched = await manifestManager.getLastDispatchedManifest(); - manifestManager.getSnapshot = jest.fn().mockResolvedValue(null); + test('initial policy creation succeeds if manifest retrieval fails', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.InitialSystemState, + }); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + expect(lastComputed).toEqual(null); + + manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error('abcd')); const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastDispatched.toEndpointFormat() - ); }); test('subsequent policy creations succeed', async () => { - const logger = loggerMock.create(); + const logger = loggingSystemMock.create().get('ingest_integration.test'); const manifestManager = getManifestManagerMock(); - const snapshot = await manifestManager.getSnapshot(); - manifestManager.getLastDispatchedManifest = jest.fn().mockResolvedValue(snapshot!.manifest); - manifestManager.getSnapshot = jest.fn().mockResolvedValue({ - manifest: snapshot!.manifest, - diffs: [], - }); + const lastComputed = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(lastComputed); // no diffs const callback = getPackageConfigCreateCallback(logger, manifestManager); const policyConfig = createNewPackageConfigMock(); const newPolicyConfig = await callback(policyConfig); + expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - snapshot!.manifest.toEndpointFormat() + lastComputed!.toEndpointFormat() ); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index e2522ac4af778..11d4b12d0b76a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -8,9 +8,63 @@ import { Logger } from '../../../../../src/core/server'; import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; -import { ManifestManager, ManifestSnapshot } from './services/artifacts'; +import { ManifestManager } from './services/artifacts'; +import { Manifest } from './lib/artifacts'; import { reportErrors, ManifestConstants } from './lib/artifacts/common'; -import { ManifestSchemaVersion } from '../../common/endpoint/schema/common'; +import { InternalArtifactCompleteSchema } from './schemas/artifacts'; +import { manifestDispatchSchema } from '../../common/endpoint/schema/manifest'; + +const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => { + let manifest: Manifest | null = null; + + try { + manifest = await manifestManager.getLastComputedManifest(ManifestConstants.SCHEMA_VERSION); + + // If we have not yet computed a manifest, then we have to do so now. This should only happen + // once. + if (manifest == null) { + // New computed manifest based on current state of exception list + const newManifest = await manifestManager.buildNewManifest(ManifestConstants.SCHEMA_VERSION); + const diffs = newManifest.diff(Manifest.getDefault(ManifestConstants.SCHEMA_VERSION)); + + // Compress new artifacts + const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); + for (const artifactId of adds) { + const compressError = await newManifest.compressArtifact(artifactId); + if (compressError) { + throw compressError; + } + } + + // Persist new artifacts + const artifacts = adds + .map((artifactId) => newManifest.getArtifact(artifactId)) + .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); + if (artifacts.length !== adds.length) { + throw new Error('Invalid artifact encountered.'); + } + const persistErrors = await manifestManager.pushArtifacts(artifacts); + if (persistErrors.length) { + reportErrors(logger, persistErrors); + throw new Error('Unable to persist new artifacts.'); + } + + // Commit the manifest state + if (diffs.length) { + const error = await manifestManager.commit(newManifest); + if (error) { + throw error; + } + } + + manifest = newManifest; + } + } catch (err) { + logger.error(err); + } + + return manifest ?? Manifest.getDefault(ManifestConstants.SCHEMA_VERSION); +}; /** * Callback to handle creation of PackageConfigs in Ingest Manager @@ -31,85 +85,37 @@ export const getPackageConfigCreateCallback = ( // follow the types/schema expected let updatedPackageConfig = newPackageConfig as NewPolicyData; - // get current manifest from SO (last dispatched) - const manifest = ( - await manifestManager.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION) - )?.toEndpointFormat() ?? { - manifest_version: 'default', - schema_version: ManifestConstants.SCHEMA_VERSION as ManifestSchemaVersion, - artifacts: {}, - }; + // Get most recent manifest + const manifest = await getManifest(logger, manifestManager); + const serializedManifest = manifest.toEndpointFormat(); + if (!manifestDispatchSchema.is(serializedManifest)) { + // This should not happen. + // But if it does, we log it and return it anyway. + logger.error('Invalid manifest'); + } // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. - if (newPackageConfig.inputs.length === 0) { - updatedPackageConfig = { - ...newPackageConfig, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - artifact_manifest: { - value: manifest, - }, - policy: { - value: policyConfigFactory(), - }, + updatedPackageConfig = { + ...newPackageConfig, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + artifact_manifest: { + value: serializedManifest, + }, + policy: { + value: policyConfigFactory(), }, }, - ], - }; - } - - let snapshot: ManifestSnapshot | null = null; - let success = true; - try { - // Try to get most up-to-date manifest data. - - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest, if it exists - snapshot = await manifestManager.getSnapshot({ initialize: true }); - - if (snapshot && snapshot.diffs.length) { - // create new artifacts - const errors = await manifestManager.syncArtifacts(snapshot, 'add'); - if (errors.length) { - reportErrors(logger, errors); - throw new Error('Error writing new artifacts.'); - } - } - - if (snapshot) { - updatedPackageConfig.inputs[0].config.artifact_manifest = { - value: snapshot.manifest.toEndpointFormat(), - }; - } - - return updatedPackageConfig; - } catch (err) { - success = false; - logger.error(err); - return updatedPackageConfig; - } finally { - if (success && snapshot !== null) { - try { - if (snapshot.diffs.length > 0) { - // TODO: let's revisit the way this callback happens... use promises? - // only commit when we know the package config was created - await manifestManager.commit(snapshot.manifest); + }, + ], + }; - // clean up old artifacts - await manifestManager.syncArtifacts(snapshot, 'delete'); - } - } catch (err) { - logger.error(err); - } - } else if (snapshot === null) { - logger.error('No manifest snapshot available.'); - } - } + return updatedPackageConfig; }; return handlePackageConfigCreate; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 77a5e85b14199..7298a9bfa72a6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from 'src/core/server'; +import { + InternalArtifactSchema, + InternalArtifactCompleteSchema, + internalArtifactCompleteSchema, +} from '../../schemas/artifacts'; export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', @@ -15,7 +20,16 @@ export const ArtifactConstants = { export const ManifestConstants = { SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest', SCHEMA_VERSION: 'v1', - INITIAL_VERSION: 'WzAsMF0=', +}; + +export const getArtifactId = (artifact: InternalArtifactSchema) => { + return `${artifact.identifier}-${artifact.decodedSha256}`; +}; + +export const isCompleteArtifact = ( + artifact: InternalArtifactSchema +): artifact is InternalArtifactCompleteSchema => { + return internalArtifactCompleteSchema.is(artifact); }; export const reportErrors = (logger: Logger, errors: Error[]) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 1a19306b2fd60..d3d073efa73c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,8 @@ import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types/entries'; -import { getFullEndpointExceptionList } from './lists'; +import { buildArtifact, getFullEndpointExceptionList } from './lists'; +import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; describe('buildEventTypeSignal', () => { let mockExceptionClient: ExceptionListClient; @@ -340,4 +341,95 @@ describe('buildEventTypeSignal', () => { const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); expect(resp.entries.length).toEqual(0); }); + + test('it should return a stable hash regardless of order of entries', async () => { + const translatedEntries: TranslatedEntry[] = [ + { + entries: [ + { + field: 'some.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ]; + const translatedEntriesReversed = translatedEntries.reverse(); + + const translatedExceptionList = { + entries: [ + { + type: 'simple', + entries: translatedEntries, + }, + ], + }; + + const translatedExceptionListReversed = { + entries: [ + { + type: 'simple', + entries: translatedEntriesReversed, + }, + ], + }; + + const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); + const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); + + test('it should return a stable hash regardless of order of items', async () => { + const translatedItems: TranslatedExceptionListItem[] = [ + { + type: 'simple', + entries: [ + { + entries: [ + { + field: 'some.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], + }, + { + type: 'simple', + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }, + ]; + + const translatedExceptionList = { + entries: translatedItems, + }; + + const translatedExceptionListReversed = { + entries: translatedItems.reverse(), + }; + + const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); + const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e6fd4bad97c5f..68fa2a0511a48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -25,6 +25,8 @@ import { translatedEntryMatchMatcher, translatedEntryMatchAnyMatcher, TranslatedExceptionListItem, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, } from '../../schemas'; import { ArtifactConstants } from './common'; @@ -32,7 +34,7 @@ export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, os: string, schemaVersion: string -): Promise { +): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); @@ -45,11 +47,32 @@ export async function buildArtifact( encodedSha256: sha256, decodedSize: exceptionsBuffer.byteLength, encodedSize: exceptionsBuffer.byteLength, - created: Date.now(), body: exceptionsBuffer.toString('base64'), }; } +export async function maybeCompressArtifact( + uncompressedArtifact: InternalArtifactSchema +): Promise { + const compressedArtifact = { ...uncompressedArtifact }; + if (internalArtifactCompleteSchema.is(uncompressedArtifact)) { + const compressedExceptionList = await compressExceptionList( + Buffer.from(uncompressedArtifact.body, 'base64') + ); + compressedArtifact.body = compressedExceptionList.toString('base64'); + compressedArtifact.encodedSize = compressedExceptionList.byteLength; + compressedArtifact.compressionAlgorithm = 'zlib'; + compressedArtifact.encodedSha256 = createHash('sha256') + .update(compressedExceptionList) + .digest('hex'); + } + return compressedArtifact; +} + +export function isCompressed(artifact: InternalArtifactSchema) { + return artifact.compressionAlgorithm === 'zlib'; +} + export async function getFullEndpointExceptionList( eClient: ExceptionListClient, os: string, @@ -136,7 +159,7 @@ function translateItem( const itemSet = new Set(); return { type: item.type, - entries: item.entries.reduce((translatedEntries: TranslatedEntry[], entry) => { + entries: item.entries.reduce((translatedEntries, entry) => { const translatedEntry = translateEntry(schemaVersion, entry); if (translatedEntry !== undefined && translatedEntryType.is(translatedEntry)) { const itemHash = createHash('sha256').update(JSON.stringify(translatedEntry)).digest('hex'); @@ -156,8 +179,8 @@ function translateEntry( ): TranslatedEntry | undefined { switch (entry.type) { case 'nested': { - const nestedEntries = entry.entries.reduce( - (entries: TranslatedEntryNestedEntry[], nestedEntry) => { + const nestedEntries = entry.entries.reduce( + (entries, nestedEntry) => { const translatedEntry = translateEntry(schemaVersion, nestedEntry); if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { entries.push(translatedEntry); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index e1f6bac2620ea..95587c6fc105d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -5,103 +5,125 @@ */ import { ManifestSchemaVersion } from '../../../../common/endpoint/schema/common'; -import { InternalArtifactSchema } from '../../schemas'; -import { - getInternalArtifactMock, - getInternalArtifactMockWithDiffs, -} from '../../schemas/artifacts/saved_objects.mock'; -import { ManifestConstants } from './common'; +import { InternalArtifactCompleteSchema } from '../../schemas'; +import { ManifestConstants, getArtifactId } from './common'; import { Manifest } from './manifest'; +import { + getMockArtifacts, + getMockManifest, + getMockManifestWithDiffs, + getEmptyMockManifest, +} from './mocks'; describe('manifest', () => { describe('Manifest object sanity checks', () => { - const artifacts: InternalArtifactSchema[] = []; - const now = new Date(); + let artifacts: InternalArtifactCompleteSchema[] = []; let manifest1: Manifest; let manifest2: Manifest; + let emptyManifest: Manifest; beforeAll(async () => { - const artifactLinux = await getInternalArtifactMock('linux', 'v1'); - const artifactMacos = await getInternalArtifactMock('macos', 'v1'); - const artifactWindows = await getInternalArtifactMock('windows', 'v1'); - artifacts.push(artifactLinux); - artifacts.push(artifactMacos); - artifacts.push(artifactWindows); - - manifest1 = new Manifest(now, 'v1', ManifestConstants.INITIAL_VERSION); - manifest1.addEntry(artifactLinux); - manifest1.addEntry(artifactMacos); - manifest1.addEntry(artifactWindows); - manifest1.setVersion('abcd'); - - const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', 'v1'); - manifest2 = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); - manifest2.addEntry(newArtifactLinux); - manifest2.addEntry(artifactMacos); - manifest2.addEntry(artifactWindows); + artifacts = await getMockArtifacts({ compress: true }); + manifest1 = await getMockManifest({ compress: true }); + manifest2 = await getMockManifestWithDiffs({ compress: true }); + emptyManifest = await getEmptyMockManifest({ compress: true }); }); test('Can create manifest with valid schema version', () => { - const manifest = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION); + const manifest = new Manifest('v1'); expect(manifest).toBeInstanceOf(Manifest); }); test('Cannot create manifest with invalid schema version', () => { expect(() => { - new Manifest( - new Date(), - 'abcd' as ManifestSchemaVersion, - ManifestConstants.INITIAL_VERSION - ); + new Manifest('abcd' as ManifestSchemaVersion); }).toThrow(); }); + test('Empty manifest transforms correctly to expected endpoint format', async () => { + expect(emptyManifest.toEndpointFormat()).toStrictEqual({ + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: 'a9b7ef358a363f327f479e31efc4f228b2277a7fb4d1914ca9b4e7ca9ffcf537', + schema_version: 'v1', + }); + }); + test('Manifest transforms correctly to expected endpoint format', async () => { expect(manifest1.toEndpointFormat()).toStrictEqual({ artifacts: { 'endpoint-exceptionlist-linux-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, }, - manifest_version: 'abcd', + manifest_version: 'a7f4760bfa2662e85e30fe4fb8c01b4c4a20938c76ab21d3c5a3e781e547cce7', schema_version: 'v1', }); }); test('Manifest transforms correctly to expected saved object format', async () => { expect(manifest1.toSavedObject()).toStrictEqual({ - created: now.getTime(), ids: [ - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ], }); }); @@ -111,12 +133,12 @@ describe('manifest', () => { expect(diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, { id: - 'endpoint-exceptionlist-linux-v1-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51', + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); @@ -124,7 +146,7 @@ describe('manifest', () => { test('Manifest returns data for given artifact', async () => { const artifact = artifacts[0]; - const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.decodedSha256}`); + const returned = manifest1.getArtifact(getArtifactId(artifact)); expect(returned).toEqual(artifact); }); @@ -132,39 +154,35 @@ describe('manifest', () => { const entries = manifest1.getEntries(); const keys = Object.keys(entries); expect(keys).toEqual([ - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ]); }); test('Manifest returns true if contains artifact', async () => { const found = manifest1.contains( - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); expect(found).toEqual(true); }); test('Manifest can be created from list of artifacts', async () => { - const oldManifest = new Manifest( - new Date(), - ManifestConstants.SCHEMA_VERSION, - ManifestConstants.INITIAL_VERSION - ); + const oldManifest = new Manifest(ManifestConstants.SCHEMA_VERSION); const manifest = Manifest.fromArtifacts(artifacts, 'v1', oldManifest); expect( manifest.contains( - 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); expect( manifest.contains( - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ) ).toEqual(true); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index 576ecb08d6923..6ece2bf0f48e8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { validate } from '../../../../common/validate'; -import { InternalArtifactSchema, InternalManifestSchema } from '../../schemas/artifacts'; +import { + InternalArtifactSchema, + InternalManifestSchema, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, +} from '../../schemas/artifacts'; import { manifestSchemaVersion, ManifestSchemaVersion, } from '../../../../common/endpoint/schema/common'; import { ManifestSchema, manifestSchema } from '../../../../common/endpoint/schema/manifest'; -import { ManifestConstants } from './common'; import { ManifestEntry } from './manifest_entry'; +import { maybeCompressArtifact, isCompressed } from './lists'; +import { getArtifactId } from './common'; export interface ManifestDiff { type: string; @@ -20,15 +27,13 @@ export interface ManifestDiff { } export class Manifest { - private created: Date; private entries: Record; private schemaVersion: ManifestSchemaVersion; // For concurrency control - private version: string; + private version: string | undefined; - constructor(created: Date, schemaVersion: string, version: string) { - this.created = created; + constructor(schemaVersion: string, version?: string) { this.entries = {}; this.version = version; @@ -38,20 +43,24 @@ export class Manifest { ); if (errors != null || validated === null) { - throw new Error(`Invalid manifest version: ${schemaVersion}`); + throw new Error(`Invalid manifest schema version: ${schemaVersion}`); } this.schemaVersion = validated; } + public static getDefault(schemaVersion: string) { + return new Manifest(schemaVersion); + } + public static fromArtifacts( - artifacts: InternalArtifactSchema[], + artifacts: InternalArtifactCompleteSchema[], schemaVersion: string, oldManifest: Manifest ): Manifest { - const manifest = new Manifest(new Date(), schemaVersion, oldManifest.getVersion()); + const manifest = new Manifest(schemaVersion, oldManifest.getSoVersion()); artifacts.forEach((artifact) => { - const id = `${artifact.identifier}-${artifact.decodedSha256}`; + const id = getArtifactId(artifact); const existingArtifact = oldManifest.getArtifact(id); if (existingArtifact) { manifest.addEntry(existingArtifact); @@ -62,15 +71,70 @@ export class Manifest { return manifest; } + public static fromPkgConfig(manifestPkgConfig: ManifestSchema): Manifest | null { + if (manifestSchema.is(manifestPkgConfig)) { + const manifest = new Manifest(manifestPkgConfig.schema_version); + for (const [identifier, artifactRecord] of Object.entries(manifestPkgConfig.artifacts)) { + const artifact = { + identifier, + compressionAlgorithm: artifactRecord.compression_algorithm, + encryptionAlgorithm: artifactRecord.encryption_algorithm, + decodedSha256: artifactRecord.decoded_sha256, + decodedSize: artifactRecord.decoded_size, + encodedSha256: artifactRecord.encoded_sha256, + encodedSize: artifactRecord.encoded_size, + }; + manifest.addEntry(artifact); + } + return manifest; + } else { + return null; + } + } + + public async compressArtifact(id: string): Promise { + try { + const artifact = this.getArtifact(id); + if (artifact == null) { + throw new Error(`Corrupted manifest detected. Artifact ${id} not in manifest.`); + } + + const compressedArtifact = await maybeCompressArtifact(artifact); + if (!isCompressed(compressedArtifact)) { + throw new Error(`Unable to compress artifact: ${id}`); + } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { + throw new Error(`Incomplete artifact detected: ${id}`); + } + this.addEntry(compressedArtifact); + } catch (err) { + return err; + } + return null; + } + + public equals(manifest: Manifest): boolean { + return this.getSha256() === manifest.getSha256(); + } + + public getSha256(): string { + let sha256 = createHash('sha256'); + Object.keys(this.entries) + .sort() + .forEach((docId) => { + sha256 = sha256.update(docId); + }); + return sha256.digest('hex'); + } + public getSchemaVersion(): ManifestSchemaVersion { return this.schemaVersion; } - public getVersion(): string { + public getSoVersion(): string | undefined { return this.version; } - public setVersion(version: string) { + public setSoVersion(version: string) { this.version = version; } @@ -87,8 +151,12 @@ export class Manifest { return this.entries; } + public getEntry(artifactId: string): ManifestEntry | undefined { + return this.entries[artifactId]; + } + public getArtifact(artifactId: string): InternalArtifactSchema | undefined { - return this.entries[artifactId]?.getArtifact(); + return this.getEntry(artifactId)?.getArtifact(); } public diff(manifest: Manifest): ManifestDiff[] { @@ -111,7 +179,7 @@ export class Manifest { public toEndpointFormat(): ManifestSchema { const manifestObj: ManifestSchema = { - manifest_version: this.version ?? ManifestConstants.INITIAL_VERSION, + manifest_version: this.getSha256(), schema_version: this.schemaVersion, artifacts: {}, }; @@ -130,7 +198,6 @@ export class Manifest { public toSavedObject(): InternalManifestSchema { return { - created: this.created.getTime(), ids: Object.keys(this.entries), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts index 7ea2a07210c55..d7bd57547de0a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -14,7 +14,7 @@ describe('manifest_entry', () => { let manifestEntry: ManifestEntry; beforeAll(async () => { - artifact = await getInternalArtifactMock('windows', 'v1'); + artifact = await getInternalArtifactMock('windows', 'v1', { compress: true }); manifestEntry = new ManifestEntry(artifact); }); @@ -24,7 +24,7 @@ describe('manifest_entry', () => { test('Correct doc_id is returned', () => { expect(manifestEntry.getDocId()).toEqual( - 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); @@ -34,21 +34,21 @@ describe('manifest_entry', () => { test('Correct sha256 is returned', () => { expect(manifestEntry.getEncodedSha256()).toEqual( - '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e' ); expect(manifestEntry.getDecodedSha256()).toEqual( - '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); test('Correct size is returned', () => { - expect(manifestEntry.getEncodedSize()).toEqual(430); - expect(manifestEntry.getDecodedSize()).toEqual(430); + expect(manifestEntry.getEncodedSize()).toEqual(147); + expect(manifestEntry.getDecodedSize()).toEqual(432); }); test('Correct url is returned', () => { expect(manifestEntry.getUrl()).toEqual( - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735' + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); }); @@ -58,17 +58,15 @@ describe('manifest_entry', () => { test('Correct record is returned', () => { expect(manifestEntry.getRecord()).toEqual({ - compression_algorithm: 'none', + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - encoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', - decoded_size: 430, - encoded_size: 430, + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735', + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }); }); - - // TODO: add test for entry with compression }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts index b35e0c2b9ad6e..b6c103e24f024 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts @@ -7,6 +7,7 @@ import { InternalArtifactSchema } from '../../schemas/artifacts'; import { CompressionAlgorithm } from '../../../../common/endpoint/schema/common'; import { ManifestEntrySchema } from '../../../../common/endpoint/schema/manifest'; +import { getArtifactId } from './common'; export class ManifestEntry { private artifact: InternalArtifactSchema; @@ -16,7 +17,7 @@ export class ManifestEntry { } public getDocId(): string { - return `${this.getIdentifier()}-${this.getDecodedSha256()}`; + return getArtifactId(this.artifact); } public getIdentifier(): string { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts new file mode 100644 index 0000000000000..097151ee835ba --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; +import { + getInternalArtifactMock, + getInternalArtifactMockWithDiffs, + getEmptyInternalArtifactMock, +} from '../../schemas/artifacts/saved_objects.mock'; +import { ArtifactConstants } from './common'; +import { Manifest } from './manifest'; + +export const getMockArtifacts = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + return getInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + if (os === 'linux') { + return getInternalArtifactMockWithDiffs(os, 'v1'); + } + return getInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getEmptyMockArtifacts = async (opts?: { compress: boolean }) => { + return Promise.all( + ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + async (os) => { + return getEmptyInternalArtifactMock(os, 'v1', opts); + } + ) + ); +}; + +export const getMockManifest = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getMockArtifacts(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; + +export const getMockManifestWithDiffs = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getMockArtifactsWithDiff(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; + +export const getEmptyMockManifest = async (opts?: { compress: boolean }) => { + const manifest = new Manifest('v1'); + const artifacts = await getEmptyMockArtifacts(opts); + artifacts.forEach((artifact) => manifest.addEntry(artifact)); + return manifest; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 583f4499f591b..ba164059866ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -11,7 +11,8 @@ import { TaskManagerStartContract, } from '../../../../../task_manager/server'; import { EndpointAppContext } from '../../types'; -import { reportErrors } from './common'; +import { reportErrors, ManifestConstants } from './common'; +import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; export const ManifestTaskConstants = { TIMEOUT: '1m', @@ -89,37 +90,66 @@ export class ManifestTask { return; } - let errors: Error[] = []; try { - // get snapshot based on exception-list-agnostic SOs - // with diffs from last dispatched manifest - const snapshot = await manifestManager.getSnapshot(); - if (snapshot && snapshot.diffs.length > 0) { - // create new artifacts - errors = await manifestManager.syncArtifacts(snapshot, 'add'); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error writing new artifacts.'); - } - // write to ingest-manager package config - errors = await manifestManager.dispatch(snapshot.manifest); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error dispatching manifest.'); + // Last manifest we computed, which was saved to ES + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + if (oldManifest == null) { + this.logger.debug('User manifest not available yet.'); + return; + } + + // New computed manifest based on current state of exception list + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest + ); + const diffs = newManifest.diff(oldManifest); + + // Compress new artifacts + const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); + for (const artifactId of adds) { + const compressError = await newManifest.compressArtifact(artifactId); + if (compressError) { + throw compressError; } - // commit latest manifest state to user-artifact-manifest SO - const error = await manifestManager.commit(snapshot.manifest); + } + + // Persist new artifacts + const artifacts = adds + .map((artifactId) => newManifest.getArtifact(artifactId)) + .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); + if (artifacts.length !== adds.length) { + throw new Error('Invalid artifact encountered.'); + } + const persistErrors = await manifestManager.pushArtifacts(artifacts); + if (persistErrors.length) { + reportErrors(this.logger, persistErrors); + throw new Error('Unable to persist new artifacts.'); + } + + // Commit latest manifest state, if different + if (diffs.length) { + const error = await manifestManager.commit(newManifest); if (error) { - reportErrors(this.logger, [error]); - throw new Error('Error committing manifest.'); - } - // clean up old artifacts - errors = await manifestManager.syncArtifacts(snapshot, 'delete'); - if (errors.length) { - reportErrors(this.logger, errors); - throw new Error('Error cleaning up outdated artifacts.'); + throw error; } } + + // Try dispatching to ingest-manager package configs + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + if (dispatchErrors.length) { + reportErrors(this.logger, dispatchErrors); + throw new Error('Error dispatching manifest.'); + } + + // Try to clean up superceded artifacts + const deletes = diffs.filter((diff) => diff.type === 'delete').map((diff) => diff.id); + const deleteErrors = await manifestManager.deleteArtifacts(deletes); + if (deleteErrors.length) { + reportErrors(this.logger, deleteErrors); + } } catch (err) { this.logger.error(err); } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 6a8c26e08d9dd..9ca447d53bf45 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,8 +6,6 @@ import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggerMock } from 'src/core/server/logging/logger.mock'; import { xpackMocks } from '../../../../mocks'; import { AgentService, @@ -15,28 +13,24 @@ import { ExternalCallback, } from '../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../ingest_manager/server/mocks'; -import { ConfigType } from '../config'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, EndpointAppContextServiceStartContract, } from './endpoint_app_context_services'; -import { - ManifestManagerMock, - getManifestManagerMock, -} from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; +import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; /** * Creates a mocked EndpointAppContext. */ export const createMockEndpointAppContext = ( - mockManifestManager?: ManifestManagerMock + mockManifestManager?: ManifestManager ): EndpointAppContext => { return { logFactory: loggingSystemMock.create(), - // @ts-ignore - config: createMockConfig() as ConfigType, + config: () => Promise.resolve(createMockConfig()), service: createMockEndpointAppContextService(mockManifestManager), }; }; @@ -45,16 +39,15 @@ export const createMockEndpointAppContext = ( * Creates a mocked EndpointAppContextService */ export const createMockEndpointAppContextService = ( - mockManifestManager?: ManifestManagerMock + mockManifestManager?: ManifestManager ): jest.Mocked => { - return { + return ({ start: jest.fn(), stop: jest.fn(), getAgentService: jest.fn(), - // @ts-ignore - getManifestManager: mockManifestManager ?? jest.fn(), + getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), getScopedSavedObjectsClient: jest.fn(), - }; + } as unknown) as jest.Mocked; }; /** @@ -65,7 +58,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), - logger: loggerMock.create(), + logger: loggingSystemMock.create().get('mock_endpoint_app_context'), savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), registerIngestCallback: jest.fn< diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 1b364a04a4272..218f7c059da48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -20,7 +20,7 @@ import { DownloadArtifactRequestParamsSchema, downloadArtifactRequestParamsSchema, downloadArtifactResponseSchema, - InternalArtifactSchema, + InternalArtifactCompleteSchema, } from '../../schemas/artifacts'; import { EndpointAppContext } from '../../types'; @@ -86,8 +86,8 @@ export function registerDownloadExceptionListRoute( } else { logger.debug(`Cache MISS artifact ${id}`); return scopedSOClient - .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) - .then((artifact: SavedObject) => { + .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) + .then((artifact: SavedObject) => { const body = Buffer.from(artifact.attributes.body, 'base64'); cache.set(id, body); return buildAndValidateResponse(artifact.attributes.identifier, body); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts index 343b192163479..2cef1f3be69c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts @@ -15,13 +15,13 @@ export const getTranslatedExceptionListMock = (): WrappedTranslatedExceptionList { entries: [ { - field: 'some.not.nested.field', + field: 'some.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', }, ], - field: 'some.field', + field: 'some.parentField', type: 'nested', }, { diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index 183a819807ed2..d95627601a183 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -4,37 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ArtifactConstants, buildArtifact } from '../../lib/artifacts'; +import { buildArtifact, maybeCompressArtifact, isCompressed } from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; -import { InternalArtifactSchema, InternalManifestSchema } from './saved_objects'; +import { + InternalManifestSchema, + internalArtifactCompleteSchema, + InternalArtifactCompleteSchema, +} from './saved_objects'; + +const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { + const compressedArtifact = await maybeCompressArtifact(artifact); + if (!isCompressed(compressedArtifact)) { + throw new Error(`Unable to compress artifact: ${artifact.identifier}`); + } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { + throw new Error(`Incomplete artifact detected: ${artifact.identifier}`); + } + return compressedArtifact; +}; export const getInternalArtifactMock = async ( os: string, - schemaVersion: string -): Promise => { - return buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const artifact = await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; -export const getInternalArtifactMockWithDiffs = async ( +export const getEmptyInternalArtifactMock = async ( os: string, - schemaVersion: string -): Promise => { - const mock = getTranslatedExceptionListMock(); - mock.entries.pop(); - return buildArtifact(mock, os, schemaVersion); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const artifact = await buildArtifact({ entries: [] }, os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; -export const getInternalArtifactsMock = async ( +export const getInternalArtifactMockWithDiffs = async ( os: string, - schemaVersion: string -): Promise => { - // @ts-ignore - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map(async () => { - await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); - }); + schemaVersion: string, + opts?: { compress: boolean } +): Promise => { + const mock = getTranslatedExceptionListMock(); + mock.entries.pop(); + const artifact = await buildArtifact(mock, os, schemaVersion); + return opts?.compress ? compressArtifact(artifact) : artifact; }; export const getInternalManifestMock = (): InternalManifestSchema => ({ - created: Date.now(), ids: [], }); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index aa11f4409269a..4dea916dcb436 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -16,7 +16,7 @@ import { created } from './common'; export const body = t.string; // base64 -export const internalArtifactSchema = t.exact( +export const internalArtifactRecordSchema = t.exact( t.type({ identifier, compressionAlgorithm, @@ -25,18 +25,49 @@ export const internalArtifactSchema = t.exact( decodedSize: size, encodedSha256: sha256, encodedSize: size, - created, - body, }) ); +export type InternalArtifactRecordSchema = t.TypeOf; +export const internalArtifactAdditionalFields = { + body, +}; + +export const internalArtifactSchema = t.intersection([ + internalArtifactRecordSchema, + t.partial(internalArtifactAdditionalFields), +]); export type InternalArtifactSchema = t.TypeOf; +export const internalArtifactCompleteSchema = t.intersection([ + internalArtifactRecordSchema, + t.exact(t.type(internalArtifactAdditionalFields)), +]); +export type InternalArtifactCompleteSchema = t.TypeOf; + +export const internalArtifactCreateSchema = t.intersection([ + internalArtifactCompleteSchema, + t.exact( + t.type({ + created, + }) + ), +]); +export type InternalArtifactCreateSchema = t.TypeOf; + export const internalManifestSchema = t.exact( t.type({ - created, ids: t.array(identifier), }) ); - export type InternalManifestSchema = t.TypeOf; + +export const internalManifestCreateSchema = t.intersection([ + internalManifestSchema, + t.exact( + t.type({ + created, + }) + ), +]); +export type InternalManifestCreateSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index 3e3b12c04d65c..0787231e242cb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { ArtifactConstants } from '../../lib/artifacts'; +import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; import { getArtifactClientMock } from './artifact_client.mock'; import { ArtifactClient } from './artifact_client'; @@ -31,8 +31,11 @@ describe('artifact_client', () => { await artifactClient.createArtifact(artifact); expect(savedObjectsClient.create).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, - artifact, - { id: artifactClient.getArtifactId(artifact) } + { + ...artifact, + created: expect.any(Number), + }, + { id: getArtifactId(artifact) } ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index ca53a891c4d6b..6138b4fb7e6dc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -5,8 +5,11 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { ArtifactConstants } from '../../lib/artifacts'; -import { InternalArtifactSchema } from '../../schemas/artifacts'; +import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; +import { + InternalArtifactCompleteSchema, + InternalArtifactCreateSchema, +} from '../../schemas/artifacts'; export class ArtifactClient { private savedObjectsClient: SavedObjectsClientContract; @@ -15,24 +18,23 @@ export class ArtifactClient { this.savedObjectsClient = savedObjectsClient; } - public getArtifactId(artifact: InternalArtifactSchema) { - return `${artifact.identifier}-${artifact.decodedSha256}`; - } - - public async getArtifact(id: string): Promise> { - return this.savedObjectsClient.get( + public async getArtifact(id: string): Promise> { + return this.savedObjectsClient.get( ArtifactConstants.SAVED_OBJECT_TYPE, id ); } public async createArtifact( - artifact: InternalArtifactSchema - ): Promise> { - return this.savedObjectsClient.create( + artifact: InternalArtifactCompleteSchema + ): Promise> { + return this.savedObjectsClient.create( ArtifactConstants.SAVED_OBJECT_TYPE, - artifact, - { id: this.getArtifactId(artifact) } + { + ...artifact, + created: Date.now(), + }, + { id: getArtifactId(artifact) } ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts index fe3f193bc8ff5..6db29289e983d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts @@ -38,7 +38,10 @@ describe('manifest_client', () => { await manifestClient.createManifest(manifest); expect(savedObjectsClient.create).toHaveBeenCalledWith( ManifestConstants.SAVED_OBJECT_TYPE, - manifest, + { + ...manifest, + created: expect.any(Number), + }, { id: manifestClient.getManifestId() } ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts index 45182841e56fc..385f115e6301a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts @@ -15,7 +15,7 @@ import { } from '../../../../common/endpoint/schema/common'; import { validate } from '../../../../common/validate'; import { ManifestConstants } from '../../lib/artifacts'; -import { InternalManifestSchema } from '../../schemas/artifacts'; +import { InternalManifestSchema, InternalManifestCreateSchema } from '../../schemas/artifacts'; interface UpdateManifestOpts { version: string; @@ -57,9 +57,12 @@ export class ManifestClient { public async createManifest( manifest: InternalManifestSchema ): Promise> { - return this.savedObjectsClient.create( + return this.savedObjectsClient.create( ManifestConstants.SAVED_OBJECT_TYPE, - manifest, + { + ...manifest, + created: Date.now(), + }, { id: this.getManifestId() } ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 3e4fee8871b8a..08cdb9816a1c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -6,53 +6,34 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { Logger } from 'src/core/server'; -import { createPackageConfigMock } from '../../../../../../ingest_manager/common/mocks'; +import { + createPackageConfigWithManifestMock, + createPackageConfigWithInitialManifestMock, +} from '../../../../../../ingest_manager/common/mocks'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; -import { - ExceptionsCache, - Manifest, - buildArtifact, - getFullEndpointExceptionList, -} from '../../../lib/artifacts'; -import { ManifestConstants } from '../../../lib/artifacts/common'; -import { InternalArtifactSchema } from '../../../schemas/artifacts'; +import { ExceptionsCache } from '../../../lib/artifacts'; import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; import { ManifestManager } from './manifest_manager'; +import { + getMockManifest, + getMockArtifactsWithDiff, + getEmptyMockArtifacts, +} from '../../../lib/artifacts/mocks'; -async function mockBuildExceptionListArtifacts( - os: string, - schemaVersion: string -): Promise { - const mockExceptionClient = listMock.getExceptionListClient(); - const first = getFoundExceptionListItemSchemaMock(); - mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const exceptions = await getFullEndpointExceptionList(mockExceptionClient, os, schemaVersion); - return [await buildArtifact(exceptions, os, schemaVersion)]; -} - -export class ManifestManagerMock extends ManifestManager { - protected buildExceptionListArtifacts = jest - .fn() - .mockResolvedValue(mockBuildExceptionListArtifacts('linux', 'v1')); - - public getLastDispatchedManifest = jest - .fn() - .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION)); - - protected getManifestClient = jest - .fn() - .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); +export enum ManifestManagerMockType { + InitialSystemState, + NormalFlow, } export const getManifestManagerMock = (opts?: { + mockType?: ManifestManagerMockType; cache?: ExceptionsCache; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; -}): ManifestManagerMock => { +}): ManifestManager => { let cache = new ExceptionsCache(5); if (opts?.cache !== undefined) { cache = opts.cache; @@ -64,7 +45,11 @@ export const getManifestManagerMock = (opts?: { } packageConfigService.list = jest.fn().mockResolvedValue({ total: 1, - items: [{ version: 'abcd', ...createPackageConfigMock() }], + items: [ + { version: 'policy-1-version', ...createPackageConfigWithManifestMock() }, + { version: 'policy-2-version', ...createPackageConfigWithInitialManifestMock() }, + { version: 'policy-3-version', ...createPackageConfigWithInitialManifestMock() }, + ], }); let savedObjectsClient = savedObjectsClientMock.create(); @@ -72,6 +57,32 @@ export const getManifestManagerMock = (opts?: { savedObjectsClient = opts.savedObjectsClient; } + class ManifestManagerMock extends ManifestManager { + protected buildExceptionListArtifacts = jest.fn().mockImplementation(() => { + const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; + switch (mockType) { + case ManifestManagerMockType.InitialSystemState: + return getEmptyMockArtifacts(); + case ManifestManagerMockType.NormalFlow: + return getMockArtifactsWithDiff(); + } + }); + + public getLastComputedManifest = jest.fn().mockImplementation(() => { + const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; + switch (mockType) { + case ManifestManagerMockType.InitialSystemState: + return null; + case ManifestManagerMockType.NormalFlow: + return getMockManifest({ compress: true }); + } + }); + + protected getManifestClient = jest + .fn() + .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); + } + const manifestManager = new ManifestManagerMock({ artifactClient: getArtifactClientMock(savedObjectsClient), cache, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 80d325ece765c..ff331f7d017f4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -10,40 +10,71 @@ import { createPackageConfigServiceMock } from '../../../../../../ingest_manager import { ArtifactConstants, ManifestConstants, - Manifest, ExceptionsCache, + isCompleteArtifact, } from '../../../lib/artifacts'; import { getManifestManagerMock } from './manifest_manager.mock'; describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { - test('ManifestManager can snapshot manifest', async () => { + test('ManifestManager can retrieve and diff manifests', async () => { const manifestManager = getManifestManagerMock(); - const snapshot = await manifestManager.getSnapshot(); - expect(snapshot!.diffs).toEqual([ + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + expect(newManifest.diff(oldManifest!)).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); - expect(snapshot!.manifest).toBeInstanceOf(Manifest); }); test('ManifestManager populates cache properly', async () => { const cache = new ExceptionsCache(5); const manifestManager = getManifestManagerMock({ cache }); - const snapshot = await manifestManager.getSnapshot(); - expect(snapshot!.diffs).toEqual([ + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + expect(diffs).toEqual([ { id: - 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + 'endpoint-exceptionlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-exceptionlist-linux-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, ]); - await manifestManager.syncArtifacts(snapshot!, 'add'); - const diff = snapshot!.diffs[0]; - const entry = JSON.parse(inflateSync(cache.get(diff!.id)! as Buffer).toString()); + + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + const artifact = newManifest.getArtifact(newArtifactId)!; + + if (isCompleteArtifact(artifact)) { + await manifestManager.pushArtifacts([artifact]); // caches the artifact + } else { + throw new Error('Artifact is missing a body.'); + } + + const entry = JSON.parse(inflateSync(cache.get(newArtifactId)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -52,7 +83,7 @@ describe('manifest_manager', () => { { entries: [ { - field: 'nested.field', + field: 'some.nested.field', operator: 'included', type: 'exact_cased', value: 'some value', @@ -73,28 +104,77 @@ describe('manifest_manager', () => { }); }); + test('ManifestManager cannot dispatch incomplete (uncompressed) artifact', async () => { + const packageConfigService = createPackageConfigServiceMock(); + const manifestManager = getManifestManagerMock({ packageConfigService }); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + expect(dispatchErrors.length).toEqual(1); + expect(dispatchErrors[0].message).toEqual('Invalid manifest'); + }); + test('ManifestManager can dispatch manifest', async () => { const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); - const snapshot = await manifestManager.getSnapshot(); - const dispatchErrors = await manifestManager.dispatch(snapshot!.manifest); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + + const dispatchErrors = await manifestManager.tryDispatch(newManifest); + expect(dispatchErrors).toEqual([]); - const entries = snapshot!.manifest.getEntries(); - const artifact = Object.values(entries)[0].getArtifact(); + + // 2 policies updated... 1 is already up-to-date + expect(packageConfigService.update.mock.calls.length).toEqual(2); + expect( packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value ).toEqual({ - manifest_version: ManifestConstants.INITIAL_VERSION, + manifest_version: '520f6cf88b3f36a065c6ca81058d5f8690aadadf6fe857f8dec4cc37589e7283', schema_version: 'v1', artifacts: { - [artifact.identifier]: { - compression_algorithm: 'none', + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', encryption_algorithm: 'none', - decoded_sha256: artifact.decodedSha256, - encoded_sha256: artifact.encodedSha256, - decoded_size: artifact.decodedSize, - encoded_size: artifact.encodedSize, - relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`, + decoded_sha256: '0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + encoded_sha256: '57941169bb2c5416f9bd7224776c8462cb9a2be0fe8b87e6213e77a1d29be824', + decoded_size: 292, + encoded_size: 131, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, }, }); @@ -103,29 +183,20 @@ describe('manifest_manager', () => { test('ManifestManager fails to dispatch on conflict', async () => { const packageConfigService = createPackageConfigServiceMock(); const manifestManager = getManifestManagerMock({ packageConfigService }); - const snapshot = await manifestManager.getSnapshot(); - packageConfigService.update.mockRejectedValue({ status: 409 }); - const dispatchErrors = await manifestManager.dispatch(snapshot!.manifest); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); + + packageConfigService.update.mockRejectedValueOnce({ status: 409 }); + const dispatchErrors = await manifestManager.tryDispatch(newManifest); expect(dispatchErrors).toEqual([{ status: 409 }]); - const entries = snapshot!.manifest.getEntries(); - const artifact = Object.values(entries)[0].getArtifact(); - expect( - packageConfigService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value - ).toEqual({ - manifest_version: ManifestConstants.INITIAL_VERSION, - schema_version: 'v1', - artifacts: { - [artifact.identifier]: { - compression_algorithm: 'none', - encryption_algorithm: 'none', - decoded_sha256: artifact.decodedSha256, - encoded_sha256: artifact.encodedSha256, - decoded_size: artifact.decodedSize, - encoded_size: artifact.encodedSize, - relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`, - }, - }, - }); }); test('ManifestManager can commit manifest', async () => { @@ -134,37 +205,43 @@ describe('manifest_manager', () => { savedObjectsClient, }); - const snapshot = await manifestManager.getSnapshot(); - await manifestManager.syncArtifacts(snapshot!, 'add'); - - const diff = { - id: 'abcd', - type: 'delete', - }; - snapshot!.diffs.push(diff); - - const dispatched = await manifestManager.dispatch(snapshot!.manifest); - expect(dispatched).toEqual([]); + const oldManifest = await manifestManager.getLastComputedManifest( + ManifestConstants.SCHEMA_VERSION + ); + const newManifest = await manifestManager.buildNewManifest( + ManifestConstants.SCHEMA_VERSION, + oldManifest! + ); + const diffs = newManifest.diff(oldManifest!); + const oldArtifactId = diffs[0].id; + const newArtifactId = diffs[1].id; + await newManifest.compressArtifact(newArtifactId); - await manifestManager.commit(snapshot!.manifest); + const artifact = newManifest.getArtifact(newArtifactId)!; + if (isCompleteArtifact(artifact)) { + await manifestManager.pushArtifacts([artifact]); + } else { + throw new Error('Artifact is missing a body.'); + } - await manifestManager.syncArtifacts(snapshot!, 'delete'); + await manifestManager.commit(newManifest); + await manifestManager.deleteArtifacts([oldArtifactId]); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( ArtifactConstants.SAVED_OBJECT_TYPE ); - // deleted old artifact - expect(savedObjectsClient.delete).toHaveBeenCalledWith( - ArtifactConstants.SAVED_OBJECT_TYPE, - 'abcd' - ); - // committed new manifest expect(savedObjectsClient.create.mock.calls[1][0]).toEqual( ManifestConstants.SAVED_OBJECT_TYPE ); + + // deleted old artifact + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + ArtifactConstants.SAVED_OBJECT_TYPE, + oldArtifactId + ); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index c8cad32ab746e..2501f07cb26e0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -5,10 +5,11 @@ */ import { Logger, SavedObjectsClientContract } from 'src/core/server'; -import { createHash } from 'crypto'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; +import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/manifest'; + import { ArtifactConstants, ManifestConstants, @@ -17,11 +18,14 @@ import { getFullEndpointExceptionList, ExceptionsCache, ManifestDiff, + getArtifactId, } from '../../../lib/artifacts'; -import { InternalArtifactSchema } from '../../../schemas/artifacts'; +import { + InternalArtifactCompleteSchema, + internalArtifactCompleteSchema, +} from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; -import { compressExceptionList } from '../../../lib/artifacts/lists'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -73,82 +77,86 @@ export class ManifestManager { * state of exception-list-agnostic SOs. * * @param schemaVersion The schema version of the artifact - * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. + * @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs. * @throws Throws/rejects if there are errors building the list. */ protected async buildExceptionListArtifacts( schemaVersion: string - ): Promise { - // TODO: should wrap in try/catch? - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce( - async (acc: Promise, os) => { - const exceptionList = await getFullEndpointExceptionList( - this.exceptionListClient, - os, - schemaVersion - ); - const artifacts = await acc; - const artifact = await buildArtifact(exceptionList, os, schemaVersion); - artifacts.push(artifact); - return Promise.resolve(artifacts); - }, - Promise.resolve([]) - ); + ): Promise { + return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< + Promise + >(async (acc, os) => { + const exceptionList = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + schemaVersion + ); + const artifacts = await acc; + const artifact = await buildArtifact(exceptionList, os, schemaVersion); + return Promise.resolve([...artifacts, artifact]); + }, Promise.resolve([])); } /** - * Writes new artifact SOs based on provided snapshot. + * Writes new artifact SO. * - * @param snapshot A ManifestSnapshot to use for writing the artifacts. - * @returns {Promise} Any errors encountered. + * @param artifact An InternalArtifactCompleteSchema representing the artifact. + * @returns {Promise} An error, if encountered, or null. */ - private async writeArtifacts(snapshot: ManifestSnapshot): Promise { - const errors: Error[] = []; - for (const diff of snapshot.diffs) { - const artifact = snapshot.manifest.getArtifact(diff.id); - if (artifact === undefined) { - throw new Error( - `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.` - ); + protected async pushArtifact(artifact: InternalArtifactCompleteSchema): Promise { + const artifactId = getArtifactId(artifact); + try { + // Write the artifact SO + await this.artifactClient.createArtifact(artifact); + + // Cache the compressed body of the artifact + this.cache.set(artifactId, Buffer.from(artifact.body, 'base64')); + } catch (err) { + if (err.status === 409) { + this.logger.debug(`Tried to create artifact ${artifactId}, but it already exists.`); + } else { + return err; } + } - const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64')); - artifact.body = compressedArtifact.toString('base64'); - artifact.encodedSize = compressedArtifact.byteLength; - artifact.compressionAlgorithm = 'zlib'; - artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex'); + return null; + } - try { - // Write the artifact SO - await this.artifactClient.createArtifact(artifact); - // Cache the compressed body of the artifact - this.cache.set(diff.id, Buffer.from(artifact.body, 'base64')); - } catch (err) { - if (err.status === 409) { - this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); - } else { - // TODO: log error here? + /** + * Writes new artifact SOs. + * + * @param artifacts An InternalArtifactCompleteSchema array representing the artifacts. + * @returns {Promise} Any errors encountered. + */ + public async pushArtifacts(artifacts: InternalArtifactCompleteSchema[]): Promise { + const errors: Error[] = []; + for (const artifact of artifacts) { + if (internalArtifactCompleteSchema.is(artifact)) { + const err = await this.pushArtifact(artifact); + if (err) { errors.push(err); } + } else { + errors.push(new Error(`Incomplete artifact: ${getArtifactId(artifact)}`)); } } return errors; } /** - * Deletes old artifact SOs based on provided snapshot. + * Deletes outdated artifact SOs. + * + * The artifact may still remain in the cache. * - * @param snapshot A ManifestSnapshot to use for deleting the artifacts. + * @param artifactIds The IDs of the artifact to delete.. * @returns {Promise} Any errors encountered. */ - private async deleteArtifacts(snapshot: ManifestSnapshot): Promise { + public async deleteArtifacts(artifactIds: string[]): Promise { const errors: Error[] = []; - for (const diff of snapshot.diffs) { + for (const artifactId of artifactIds) { try { - // Delete the artifact SO - await this.artifactClient.deleteArtifact(diff.id); - // TODO: should we delete the cache entry here? - this.logger.info(`Cleaned up artifact ${diff.id}`); + await this.artifactClient.deleteArtifact(artifactId); + this.logger.info(`Cleaned up artifact ${artifactId}`); } catch (err) { errors.push(err); } @@ -157,14 +165,14 @@ export class ManifestManager { } /** - * Returns the last dispatched manifest based on the current state of the + * Returns the last computed manifest based on the state of the * user-artifact-manifest SO. * * @param schemaVersion The schema version of the manifest. - * @returns {Promise} The last dispatched manifest, or null if does not exist. + * @returns {Promise} The last computed manifest, or null if does not exist. * @throws Throws/rejects if there is an unexpected error retrieving the manifest. */ - public async getLastDispatchedManifest(schemaVersion: string): Promise { + public async getLastComputedManifest(schemaVersion: string): Promise { try { const manifestClient = this.getManifestClient(schemaVersion); const manifestSo = await manifestClient.getManifest(); @@ -173,11 +181,7 @@ export class ManifestManager { throw new Error('No version returned for manifest.'); } - const manifest = new Manifest( - new Date(manifestSo.attributes.created), - schemaVersion, - manifestSo.version - ); + const manifest = new Manifest(schemaVersion, manifestSo.version); for (const id of manifestSo.attributes.ids) { const artifactSo = await this.artifactClient.getArtifact(id); @@ -193,89 +197,42 @@ export class ManifestManager { } /** - * Snapshots a manifest based on current state of exception-list-agnostic SOs. + * Builds a new manifest based on the current user exception list. * - * @param opts Optional parameters for snapshot retrieval. - * @param opts.initialize Initialize a new Manifest when no manifest SO can be retrieved. - * @returns {Promise} A snapshot of the manifest, or null if not initialized. + * @param schemaVersion The schema version of the manifest. + * @param baselineManifest A baseline manifest to use for initializing pre-existing artifacts. + * @returns {Promise} A new Manifest object reprenting the current exception list. */ - public async getSnapshot(opts?: ManifestSnapshotOpts): Promise { - try { - let oldManifest: Manifest | null; - - // Get the last-dispatched manifest - oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION); - - if (oldManifest === null && opts !== undefined && opts.initialize) { - oldManifest = new Manifest( - new Date(), - ManifestConstants.SCHEMA_VERSION, - ManifestConstants.INITIAL_VERSION - ); // create empty manifest - } else if (oldManifest == null) { - this.logger.debug('Manifest does not exist yet. Waiting...'); - return null; - } - - // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); - - // Build new manifest - const newManifest = Manifest.fromArtifacts( - artifacts, - ManifestConstants.SCHEMA_VERSION, - oldManifest - ); - - // Get diffs - const diffs = newManifest.diff(oldManifest); + public async buildNewManifest( + schemaVersion: string, + baselineManifest?: Manifest + ): Promise { + // Build new exception list artifacts + const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); + + // Build new manifest + const manifest = Manifest.fromArtifacts( + artifacts, + ManifestConstants.SCHEMA_VERSION, + baselineManifest ?? Manifest.getDefault(schemaVersion) + ); - return { - manifest: newManifest, - diffs, - }; - } catch (err) { - this.logger.error(err); - return null; - } + return manifest; } /** - * Syncs artifacts based on provided snapshot. - * - * Creates artifacts that do not yet exist and cleans up old artifacts that have been - * superceded by this snapshot. + * Dispatches the manifest by writing it to the endpoint package config, if different + * from the manifest already in the config. * - * @param snapshot A ManifestSnapshot to use for sync. + * @param manifest The Manifest to dispatch. * @returns {Promise} Any errors encountered. */ - public async syncArtifacts( - snapshot: ManifestSnapshot, - diffType: 'add' | 'delete' - ): Promise { - const filteredDiffs = snapshot.diffs.filter((diff) => { - return diff.type === diffType; - }); - - const tmpSnapshot = { ...snapshot }; - tmpSnapshot.diffs = filteredDiffs; - - if (diffType === 'add') { - return this.writeArtifacts(tmpSnapshot); - } else if (diffType === 'delete') { - return this.deleteArtifacts(tmpSnapshot); + public async tryDispatch(manifest: Manifest): Promise { + const serializedManifest = manifest.toEndpointFormat(); + if (!manifestDispatchSchema.is(serializedManifest)) { + return [new Error('Invalid manifest')]; } - return [new Error(`Unsupported diff type: ${diffType}`)]; - } - - /** - * Dispatches the manifest by writing it to the endpoint package config. - * - * @param manifest The Manifest to dispatch. - * @returns {Promise} Any errors encountered. - */ - public async dispatch(manifest: Manifest): Promise { let paging = true; let page = 1; const errors: Error[] = []; @@ -293,16 +250,25 @@ export class ManifestManager { const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? { value: {}, }; - artifactManifest.value = manifest.toEndpointFormat(); - newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest; - - try { - await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig); - this.logger.debug( - `Updated package config ${id} with manifest version ${manifest.getVersion()}` - ); - } catch (err) { - errors.push(err); + + const oldManifest = + Manifest.fromPkgConfig(artifactManifest.value) ?? + Manifest.getDefault(ManifestConstants.SCHEMA_VERSION); + if (!manifest.equals(oldManifest)) { + newPackageConfig.inputs[0].config.artifact_manifest = { + value: serializedManifest, + }; + + try { + await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig); + this.logger.debug( + `Updated package config ${id} with manifest version ${manifest.getSha256()}` + ); + } catch (err) { + errors.push(err); + } + } else { + this.logger.debug(`No change in package config: ${id}`); } } else { errors.push(new Error(`Package config ${id} has no config.`)); @@ -317,46 +283,32 @@ export class ManifestManager { } /** - * Commits a manifest to indicate that it has been dispatched. + * Commits a manifest to indicate that a new version has been computed. * * @param manifest The Manifest to commit. - * @returns {Promise} An error if encountered, or null if successful. + * @returns {Promise} An error, if encountered, or null. */ public async commit(manifest: Manifest): Promise { try { const manifestClient = this.getManifestClient(manifest.getSchemaVersion()); // Commit the new manifest - if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) { - await manifestClient.createManifest(manifest.toSavedObject()); + const manifestSo = manifest.toSavedObject(); + const version = manifest.getSoVersion(); + + if (version == null) { + await manifestClient.createManifest(manifestSo); } else { - const version = manifest.getVersion(); - if (version === ManifestConstants.INITIAL_VERSION) { - throw new Error('Updating existing manifest with baseline version. Bad state.'); - } - await manifestClient.updateManifest(manifest.toSavedObject(), { + await manifestClient.updateManifest(manifestSo, { version, }); } - this.logger.info(`Committed manifest ${manifest.getVersion()}`); + this.logger.info(`Committed manifest ${manifest.getSha256()}`); } catch (err) { return err; } return null; } - - /** - * Confirms that a packageConfig exists with provided name. - */ - public async confirmPackageConfigExists(name: string) { - // TODO: what if there are multiple results? uh oh. - const { total } = await this.packageConfigService.list(this.savedObjectsClient, { - page: 1, - perPage: 20, - kuery: `ingest-package-configs.name:${name}`, - }); - return total > 0; - } } From 466380e3b6e5541041d6479d28f9fdf336ff5a8b Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 17 Jul 2020 13:53:54 -0700 Subject: [PATCH 62/76] [kbn/dev-utils] add RunWithCommands utility (#72311) Co-authored-by: spalger --- packages/kbn-dev-utils/src/index.ts | 2 +- packages/kbn-dev-utils/src/run/cleanup.ts | 94 +++++++++ packages/kbn-dev-utils/src/run/flags.test.ts | 18 +- packages/kbn-dev-utils/src/run/flags.ts | 79 ++++--- packages/kbn-dev-utils/src/run/help.test.ts | 199 ++++++++++++++++++ packages/kbn-dev-utils/src/run/help.ts | 150 +++++++++++++ packages/kbn-dev-utils/src/run/index.ts | 7 +- packages/kbn-dev-utils/src/run/run.ts | 119 ++++------- .../src/run/run_with_commands.test.ts | 77 +++++++ .../src/run/run_with_commands.ts | 136 ++++++++++++ 10 files changed, 743 insertions(+), 138 deletions(-) create mode 100644 packages/kbn-dev-utils/src/run/cleanup.ts create mode 100644 packages/kbn-dev-utils/src/run/help.test.ts create mode 100644 packages/kbn-dev-utils/src/run/help.ts create mode 100644 packages/kbn-dev-utils/src/run/run_with_commands.test.ts create mode 100644 packages/kbn-dev-utils/src/run/run_with_commands.ts diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 3e9e6238df9dc..582526f939e42 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -33,9 +33,9 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './repo_root'; export { KbnClient } from './kbn_client'; +export * from './run'; export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; diff --git a/packages/kbn-dev-utils/src/run/cleanup.ts b/packages/kbn-dev-utils/src/run/cleanup.ts new file mode 100644 index 0000000000000..84c3bbcb591d2 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/cleanup.ts @@ -0,0 +1,94 @@ +/* + * 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 { inspect } from 'util'; + +import exitHook from 'exit-hook'; + +import { ToolingLog } from '../tooling_log'; +import { isFailError } from './fail'; + +export type CleanupTask = () => void; + +export class Cleanup { + static setup(log: ToolingLog, helpText: string) { + const onUnhandledRejection = (error: any) => { + log.error('UNHANDLED PROMISE REJECTION'); + log.error( + error instanceof Error + ? error + : new Error(`non-Error type rejection value: ${inspect(error)}`) + ); + process.exit(1); + }; + + process.on('unhandledRejection', onUnhandledRejection); + + const cleanup = new Cleanup(log, helpText, [ + () => process.removeListener('unhandledRejection', onUnhandledRejection), + ]); + + cleanup.add(exitHook(() => cleanup.execute())); + + return cleanup; + } + + constructor( + private readonly log: ToolingLog, + public helpText: string, + private readonly tasks: CleanupTask[] + ) {} + + add(task: CleanupTask) { + this.tasks.push(task); + } + + execute(topLevelError?: any) { + const tasks = this.tasks.slice(0); + this.tasks.length = 0; + + for (const task of tasks) { + try { + task(); + } catch (error) { + this.onError(error); + } + } + + if (topLevelError) { + this.onError(topLevelError); + } + } + + private onError(error: any) { + if (isFailError(error)) { + this.log.error(error.message); + + if (error.showHelp) { + this.log.write(this.helpText); + } + + process.exitCode = error.exitCode; + } else { + this.log.error('UNHANDLED ERROR'); + this.log.error(error); + process.exitCode = 1; + } + } +} diff --git a/packages/kbn-dev-utils/src/run/flags.test.ts b/packages/kbn-dev-utils/src/run/flags.test.ts index c730067a84f46..f6ff70b7abeb4 100644 --- a/packages/kbn-dev-utils/src/run/flags.test.ts +++ b/packages/kbn-dev-utils/src/run/flags.test.ts @@ -22,14 +22,12 @@ import { getFlags } from './flags'; it('gets flags correctly', () => { expect( getFlags(['-a', '--abc=bcd', '--foo=bar', '--no-bar', '--foo=baz', '--box', 'yes', '-zxy'], { - flags: { - boolean: ['x'], - string: ['abc'], - alias: { - x: 'extra', - }, - allowUnexpected: true, + boolean: ['x'], + string: ['abc'], + alias: { + x: 'extra', }, + allowUnexpected: true, }) ).toMatchInlineSnapshot(` Object { @@ -60,10 +58,8 @@ it('gets flags correctly', () => { it('guesses types for unexpected flags', () => { expect( getFlags(['-abc', '--abc=bcd', '--no-foo', '--bar'], { - flags: { - allowUnexpected: true, - guessTypesForUnexpectedFlags: true, - }, + allowUnexpected: true, + guessTypesForUnexpectedFlags: true, }) ).toMatchInlineSnapshot(` Object { diff --git a/packages/kbn-dev-utils/src/run/flags.ts b/packages/kbn-dev-utils/src/run/flags.ts index c809a40d8512b..12642bceca15a 100644 --- a/packages/kbn-dev-utils/src/run/flags.ts +++ b/packages/kbn-dev-utils/src/run/flags.ts @@ -17,12 +17,9 @@ * under the License. */ -import { relative } from 'path'; - -import dedent from 'dedent'; import getopts from 'getopts'; -import { Options } from './run'; +import { RunOptions } from './run'; export interface Flags { verbose: boolean; @@ -36,23 +33,52 @@ export interface Flags { [key: string]: undefined | boolean | string | string[]; } -export function getFlags(argv: string[], options: Options): Flags { +export interface FlagOptions { + allowUnexpected?: boolean; + guessTypesForUnexpectedFlags?: boolean; + help?: string; + alias?: { [key: string]: string | string[] }; + boolean?: string[]; + string?: string[]; + default?: { [key: string]: any }; +} + +export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = {}): FlagOptions { + return { + alias: { + ...global.alias, + ...local.alias, + }, + boolean: [...(global.boolean || []), ...(local.boolean || [])], + string: [...(global.string || []), ...(local.string || [])], + default: { + ...global.alias, + ...local.alias, + }, + + help: local.help, + + allowUnexpected: !!(global.allowUnexpected || local.allowUnexpected), + guessTypesForUnexpectedFlags: !!(global.allowUnexpected || local.allowUnexpected), + }; +} + +export function getFlags(argv: string[], flagOptions: RunOptions['flags'] = {}): Flags { const unexpectedNames = new Set(); - const flagOpts = options.flags || {}; const { verbose, quiet, silent, debug, help, _, ...others } = getopts(argv, { - string: flagOpts.string, - boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], + string: flagOptions.string, + boolean: [...(flagOptions.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], alias: { - ...(flagOpts.alias || {}), + ...flagOptions.alias, v: 'verbose', }, - default: flagOpts.default, + default: flagOptions.default, unknown: (name: string) => { unexpectedNames.add(name); - return flagOpts.guessTypesForUnexpectedFlags; + return !!flagOptions.guessTypesForUnexpectedFlags; }, - } as any); + }); const unexpected: string[] = []; for (const unexpectedName of unexpectedNames) { @@ -119,32 +145,3 @@ export function getFlags(argv: string[], options: Options): Flags { ...others, }; } - -export function getHelp(options: Options) { - const usage = options.usage || `node ${relative(process.cwd(), process.argv[1])}`; - - const optionHelp = ( - dedent(options?.flags?.help || '') + - '\n' + - dedent` - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message - ` - ) - .split('\n') - .filter(Boolean) - .join('\n '); - - return ` - ${usage} - - ${dedent(options.description || 'Runs a dev task') - .split('\n') - .join('\n ')} - - Options: - ${optionHelp + '\n\n'}`; -} diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-utils/src/run/help.test.ts new file mode 100644 index 0000000000000..27be7ad28b81a --- /dev/null +++ b/packages/kbn-dev-utils/src/run/help.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { getCommandLevelHelp, getHelp, getHelpForAllCommands } from './help'; +import { Command } from './run_with_commands'; + +const fooCommand: Command = { + description: ` + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + `, + name: 'foo', + run: () => {}, + flags: { + help: ` + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + `, + }, + usage: 'foo [...names]', +}; + +const barCommand: Command = { + description: ` + Some other thing that we wrote to help us execute things. + `, + name: 'bar', + run: () => {}, + flags: { + help: ` + --baz, -b Hey hello + `, + }, + usage: 'bar [...names]', +}; + +describe('getHelp()', () => { + it('returns the expected output', () => { + expect( + getHelp({ + description: fooCommand.description, + flagHelp: fooCommand.flags?.help, + usage: ` + node scripts/foo --bar --baz + `, + }) + ).toMatchInlineSnapshot(` + " + node scripts/foo --bar --baz + + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + " + `); + }); +}); + +describe('getCommandLevelHelp()', () => { + it('returns the expected output', () => { + expect( + getCommandLevelHelp({ + command: fooCommand, + globalFlagHelp: ` + --global-flag some flag that applies to all commands + `, + }) + ).toMatchInlineSnapshot(` + " + node node_modules/jest-worker/build/workers/processChild.js foo [...names] + + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Command-specific options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + + Global options: + --global-flag some flag that applies to all commands + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + To see the help for other commands run: + node node_modules/jest-worker/build/workers/processChild.js help [command] + + To see the list of commands run: + node node_modules/jest-worker/build/workers/processChild.js --help + + " + `); + }); +}); + +describe('getHelpForAllCommands()', () => { + it('returns the expected output', () => { + expect( + getHelpForAllCommands({ + commands: [fooCommand, barCommand], + globalFlagHelp: ` + --global-flag some flag that applies to all commands + `, + usage: ` + node scripts/my_cli + `, + }) + ).toMatchInlineSnapshot(` + " + node scripts/my_cli [command] [...args] + + Runs a dev task + + Commands: + foo [...names] + Some thing that we wrote to help us execute things. + + Example: + + foo = bar = baz + + Are you getting it? + + Options: + --foo Some flag + --bar Another flag + Secondary info + --baz, -b Hey hello + + bar [...names] + Some other thing that we wrote to help us execute things. + + Options: + --baz, -b Hey hello + + + Global options: + --global-flag some flag that applies to all commands + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + + To show the help information about a specific command run: + node scripts/my_cli help [command] + + " + `); + }); +}); diff --git a/packages/kbn-dev-utils/src/run/help.ts b/packages/kbn-dev-utils/src/run/help.ts new file mode 100644 index 0000000000000..351c01da5ebe2 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/help.ts @@ -0,0 +1,150 @@ +/* + * 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 Path from 'path'; + +import 'core-js/features/string/repeat'; +import dedent from 'dedent'; + +import { Command } from './run_with_commands'; + +const DEFAULT_GLOBAL_USAGE = `node ${Path.relative(process.cwd(), process.argv[1])}`; +export const GLOBAL_FLAGS = dedent` + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message +`; + +export function indent(str: string, depth: number) { + const prefix = ' '.repeat(depth); + return str + .split('\n') + .map((line, i) => `${i > 0 ? `\n${prefix}` : ''}${line}`) + .join(''); +} + +export function joinAndTrimLines(...strings: Array) { + return strings.filter(Boolean).join('\n').split('\n').filter(Boolean).join(`\n`); +} + +export function getHelp({ + description, + usage, + flagHelp, +}: { + description?: string; + usage?: string; + flagHelp?: string; +}) { + const optionHelp = joinAndTrimLines(dedent(flagHelp || ''), GLOBAL_FLAGS); + + return ` + ${dedent(usage || '') || DEFAULT_GLOBAL_USAGE} + + ${indent(dedent(description || 'Runs a dev task'), 2)} + + Options: + ${indent(optionHelp, 4)}\n\n`; +} + +export function getCommandLevelHelp({ + usage, + globalFlagHelp, + command, +}: { + usage?: string; + globalFlagHelp?: string; + command: Command; +}) { + const globalUsage = dedent(usage || '') || DEFAULT_GLOBAL_USAGE; + const globalHelp = joinAndTrimLines(dedent(globalFlagHelp || ''), GLOBAL_FLAGS); + + const commandUsage = dedent(command.usage || '') || `${command.name} [...args]`; + const commandFlags = joinAndTrimLines(dedent(command.flags?.help || '')); + + return ` + ${globalUsage} ${commandUsage} + + ${indent(dedent(command.description || 'Runs a dev task'), 2)} + + Command-specific options: + ${indent(commandFlags, 4)} + + Global options: + ${indent(globalHelp, 4)} + + To see the help for other commands run: + ${globalUsage} help [command] + + To see the list of commands run: + ${globalUsage} --help\n\n`; +} + +export function getHelpForAllCommands({ + description, + usage, + globalFlagHelp, + commands, +}: { + description?: string; + usage?: string; + globalFlagHelp?: string; + commands: Array>; +}) { + const globalUsage = dedent(usage || '') || DEFAULT_GLOBAL_USAGE; + const globalHelp = joinAndTrimLines(dedent(globalFlagHelp || ''), GLOBAL_FLAGS); + + const commandsHelp = commands + .map((command) => { + const options = command.flags?.help + ? '\n' + + dedent` + Options: + ${indent( + joinAndTrimLines(dedent(command.flags?.help || '')), + ' '.length + )} + ` + + '\n' + : ''; + + return [ + dedent(command.usage || '') || command.name, + ` ${indent(dedent(command.description || 'Runs a dev task'), 2)}`, + ...([indent(options, 2)] || []), + ].join('\n'); + }) + .join('\n'); + + return ` + ${globalUsage} [command] [...args] + + ${indent(dedent(description || 'Runs a dev task'), 2)} + + Commands: + ${indent(commandsHelp, 4)} + + Global options: + ${indent(globalHelp, 4)} + + To show the help information about a specific command run: + ${globalUsage} help [command]\n\n`; +} diff --git a/packages/kbn-dev-utils/src/run/index.ts b/packages/kbn-dev-utils/src/run/index.ts index 5e1a42deefffb..070ce740bf202 100644 --- a/packages/kbn-dev-utils/src/run/index.ts +++ b/packages/kbn-dev-utils/src/run/index.ts @@ -17,6 +17,7 @@ * under the License. */ -export { run } from './run'; -export { Flags } from './flags'; -export { createFailError, createFlagError, combineErrors, isFailError } from './fail'; +export * from './run'; +export * from './run_with_commands'; +export * from './flags'; +export * from './fail'; diff --git a/packages/kbn-dev-utils/src/run/run.ts b/packages/kbn-dev-utils/src/run/run.ts index 029d428565163..2a844bcbc27eb 100644 --- a/packages/kbn-dev-utils/src/run/run.ts +++ b/packages/kbn-dev-utils/src/run/run.ts @@ -17,48 +17,37 @@ * under the License. */ -import { inspect } from 'util'; - -// @ts-ignore @types are outdated and module is super simple -import exitHook from 'exit-hook'; - import { pickLevelFromFlags, ToolingLog, LogLevel } from '../tooling_log'; -import { createFlagError, isFailError } from './fail'; -import { Flags, getFlags, getHelp } from './flags'; +import { createFlagError } from './fail'; +import { Flags, getFlags, FlagOptions } from './flags'; import { ProcRunner, withProcRunner } from '../proc_runner'; +import { getHelp } from './help'; +import { CleanupTask, Cleanup } from './cleanup'; -type CleanupTask = () => void; -type RunFn = (args: { +export interface RunContext { log: ToolingLog; flags: Flags; procRunner: ProcRunner; addCleanupTask: (task: CleanupTask) => void; -}) => Promise | void; +} +export type RunFn = (context: RunContext) => Promise | void; -export interface Options { +export interface RunOptions { usage?: string; description?: string; log?: { defaultLevel?: LogLevel; }; - flags?: { - allowUnexpected?: boolean; - guessTypesForUnexpectedFlags?: boolean; - help?: string; - alias?: { [key: string]: string | string[] }; - boolean?: string[]; - string?: string[]; - default?: { [key: string]: any }; - }; + flags?: FlagOptions; } -export async function run(fn: RunFn, options: Options = {}) { - const flags = getFlags(process.argv.slice(2), options); - - if (flags.help) { - process.stderr.write(getHelp(options)); - process.exit(1); - } +export async function run(fn: RunFn, options: RunOptions = {}) { + const flags = getFlags(process.argv.slice(2), options.flags); + const helpText = getHelp({ + description: options.description, + usage: options.usage, + flagHelp: options.flags?.help, + }); const log = new ToolingLog({ level: pickLevelFromFlags(flags, { @@ -67,67 +56,33 @@ export async function run(fn: RunFn, options: Options = {}) { writeTo: process.stdout, }); - process.on('unhandledRejection', (error) => { - log.error('UNHANDLED PROMISE REJECTION'); - log.error( - error instanceof Error - ? error - : new Error(`non-Error type rejection value: ${inspect(error)}`) - ); - process.exit(1); - }); - - const handleErrorWithoutExit = (error: any) => { - if (isFailError(error)) { - log.error(error.message); - - if (error.showHelp) { - log.write(getHelp(options)); - } - - process.exitCode = error.exitCode; - } else { - log.error('UNHANDLED ERROR'); - log.error(error); - process.exitCode = 1; - } - }; - - const doCleanup = () => { - const tasks = cleanupTasks.slice(0); - cleanupTasks.length = 0; + if (flags.help) { + log.write(helpText); + process.exit(); + } - for (const task of tasks) { - try { - task(); - } catch (error) { - handleErrorWithoutExit(error); - } - } - }; + const cleanup = Cleanup.setup(log, helpText); - const unhookExit: CleanupTask = exitHook(doCleanup); - const cleanupTasks: CleanupTask[] = [unhookExit]; + if (!options.flags?.allowUnexpected && flags.unexpected.length) { + const error = createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); + cleanup.execute(error); + return; + } try { - if (!options.flags?.allowUnexpected && flags.unexpected.length) { - throw createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); - } - - try { - await withProcRunner(log, async (procRunner) => { - await fn({ - log, - flags, - procRunner, - addCleanupTask: (task: CleanupTask) => cleanupTasks.push(task), - }); + await withProcRunner(log, async (procRunner) => { + await fn({ + log, + flags, + procRunner, + addCleanupTask: cleanup.add.bind(cleanup), }); - } finally { - doCleanup(); - } + }); } catch (error) { - handleErrorWithoutExit(error); + cleanup.execute(error); + // process.exitCode is set by `cleanup` when necessary process.exit(); + } finally { + cleanup.execute(); } } diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts new file mode 100644 index 0000000000000..eb7708998751c --- /dev/null +++ b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { RunWithCommands } from './run_with_commands'; +import { ToolingLog, ToolingLogCollectingWriter } from '../tooling_log'; +import { ProcRunner } from '../proc_runner'; + +const testLog = new ToolingLog(); +const testLogWriter = new ToolingLogCollectingWriter(); +testLog.setWriters([testLogWriter]); + +const testCli = new RunWithCommands({ + usage: 'node scripts/test_cli [...options]', + description: 'test cli', + extendContext: async () => { + return { + extraContext: true, + }; + }, + globalFlags: { + boolean: ['some-bool'], + help: ` + --some-bool description + `, + }, +}); + +beforeEach(() => { + process.argv = ['node', 'scripts/test_cli', 'foo', '--some-bool']; + jest.clearAllMocks(); +}); + +it('extends the context using extendContext()', async () => { + const context: any = await new Promise((resolve) => { + testCli.command({ name: 'foo', description: 'some command', run: resolve }).execute(); + }); + + expect(context).toEqual({ + log: expect.any(ToolingLog), + flags: expect.any(Object), + addCleanupTask: expect.any(Function), + procRunner: expect.any(ProcRunner), + extraContext: true, + }); + + expect(context.flags).toMatchInlineSnapshot(` + Object { + "_": Array [ + "foo", + ], + "debug": false, + "help": false, + "quiet": false, + "silent": false, + "some-bool": true, + "unexpected": Array [], + "v": false, + "verbose": false, + } + `); +}); diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.ts b/packages/kbn-dev-utils/src/run/run_with_commands.ts new file mode 100644 index 0000000000000..9fb069e4b2d35 --- /dev/null +++ b/packages/kbn-dev-utils/src/run/run_with_commands.ts @@ -0,0 +1,136 @@ +/* + * 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 { ToolingLog, pickLevelFromFlags } from '../tooling_log'; +import { RunContext, RunOptions } from './run'; +import { getFlags, FlagOptions, mergeFlagOptions } from './flags'; +import { Cleanup } from './cleanup'; +import { getHelpForAllCommands, getCommandLevelHelp } from './help'; +import { createFlagError } from './fail'; +import { withProcRunner } from '../proc_runner'; + +export type CommandRunFn = (context: RunContext & T) => Promise | void; + +export interface Command { + name: string; + run: CommandRunFn; + description: RunOptions['description']; + usage?: RunOptions['usage']; + flags?: FlagOptions; +} + +export interface RunWithCommandsOptions { + log?: RunOptions['log']; + description?: RunOptions['description']; + usage?: RunOptions['usage']; + globalFlags?: FlagOptions; + extendContext?(context: RunContext): Promise | T; +} + +export class RunWithCommands { + constructor( + private readonly options: RunWithCommandsOptions, + private readonly commands: Array> = [] + ) {} + + command(options: Command) { + return new RunWithCommands(this.options, this.commands.concat(options)); + } + + async execute() { + const globalFlags = getFlags(process.argv.slice(2), { + allowUnexpected: true, + }); + + const isHelpCommand = globalFlags._[0] === 'help'; + const commandName = isHelpCommand ? globalFlags._[1] : globalFlags._[0]; + const command = this.commands.find((c) => c.name === commandName); + const log = new ToolingLog({ + level: pickLevelFromFlags(globalFlags, { + default: this.options.log?.defaultLevel, + }), + writeTo: process.stdout, + }); + + const globalHelp = getHelpForAllCommands({ + description: this.options.description, + usage: this.options.usage, + globalFlagHelp: this.options.globalFlags?.help, + commands: this.commands, + }); + const cleanup = Cleanup.setup(log, globalHelp); + + if (!command) { + if (globalFlags.help) { + log.write(globalHelp); + process.exit(); + } + + const error = createFlagError( + commandName ? `unknown command [${commandName}]` : `missing command name` + ); + cleanup.execute(error); + process.exit(1); + } + + const commandFlagOptions = mergeFlagOptions(this.options.globalFlags, command.flags); + const commandFlags = getFlags(process.argv.slice(2), commandFlagOptions); + const commandHelp = getCommandLevelHelp({ + usage: this.options.usage, + globalFlagHelp: this.options.globalFlags?.help, + command, + }); + cleanup.helpText = commandHelp; + + if (commandFlags.help || isHelpCommand) { + cleanup.execute(); + log.write(commandHelp); + process.exit(); + } + + if (!commandFlagOptions.allowUnexpected && commandFlags.unexpected.length) { + cleanup.execute(createFlagError(`Unknown flag(s) "${commandFlags.unexpected.join('", "')}"`)); + return; + } + + try { + await withProcRunner(log, async (procRunner) => { + const context: RunContext = { + log, + flags: commandFlags, + procRunner, + addCleanupTask: cleanup.add, + }; + + const extendedContext = { + ...context, + ...(this.options.extendContext ? await this.options.extendContext(context) : ({} as T)), + }; + + await command.run(extendedContext); + }); + } catch (error) { + cleanup.execute(error); + // exitCode is set by `cleanup` when necessary + process.exit(); + } finally { + cleanup.execute(); + } + } +} From f487867fda2f3c5386d28525dd72958af5fbc81a Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 17 Jul 2020 17:13:34 -0400 Subject: [PATCH 63/76] [SIEM] Updates consumer in export_rule archive (#72324) --- .../es_archives/export_rule/data.json.gz | Bin 1911 -> 1931 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz index 3c1a0f663a8eeb58dbe0626b067a086f52f975ef..373251d9e4f93d8097029345aa45aba959dd7a08 100644 GIT binary patch literal 1931 zcmV;62Xy!!iwFpN;}Kr~17u-zVJ>QOZ*Bn9Sy@x#Iu?HCS9tPtmv&;ud#3IKTLYIR z&ZcOR0rP zQ2QVk9uO6B(g59I2*>4As>POSO?7>VDL8Jp<)Rq+A^0 zu@tQjiH~B`)29@ch%9*7K`FGC7UWXENfI+y{MT&p-yceJDbfYoPjdSh!J|4O{TNYJ zE3>8K6hBFhqlA3Ai*BMXp9W(Z96u+2?j~VpzkhL^-UiX({gZwijjfxZ?uQpAV*Gr0 zeE1wb+~2y*L(R&_ooziLqEQx~efcax_bG0CaW9W{@BGUP-S{*5tdYUrr{ntuxec$` z?a@u=V(i_thkPW?+C$6KlcV-t=YSfY2JH(keu%WaTVdd!VeefXSXbo!D;1$(+%-}& zW}hvy)4#g!95wE*j!i+2u59h}^8Sf_y=GQ)eR#s#G+mA8Mq^p=B#z|1-2mnhl znvf+{6SI0d@qdZ*?$7zyrB@+~vw@y)pMy!%b5+A__NmkKW(F!cS zAT%U#e~%`?usf-VSWsGF3NZ^ZlFH0nZG!}09N?Uboe{>@?wbQ~nJ~i117_=43FSC@ zg(pi!(zG~vR1I5WRlCRtaF(%Ij6kVk%i8ng8mWJ%JVP;?6y+Zkr8&GI;XdSqxv#fFjq$1JB-2_rP_ z6pf{&=$cj~VvgprtrP`)G;eCr3uOuRP@48cCaa+~;5={?q zaj#`K!Wo8gU7wK|VZ%<4WYRE8TTVI$PoehK>wzkp^m^UbT%-j})oq%FYT5=?y(WD1 zP1{6n;5k8P))r(hz=!q&&N31%#fYH?=s@985(YVlf!v%XkF}-5O?==3**Vv*|4QoDrHrCvw|XriNZovS=4T13^8=_K%Vu{b~(n>0Orwp>cR8Vd$E6co>u0<;w9 zSTImXwbrP%RyOP+Hk?G*v`p6vu%-sK>#DkrVdAj7rm7hzLP6x%x)znYK1coeB&_v% z`R0(~JsJKU30rSZoRX4R?`-9r^Tn6)=>!2z__EN{L>exhnLBf6)JCMAAz9>~XMmSy zqGC*{Id!kDElMw~$eDSw_)gT!mJ7>zXvO(&*IftB|D65%j{N_=v-559(Q$SdsuT`G z74_ppI^Tk$Ax!|@o?PB$YFweXidk_BZI&sR;~ z)l}aO4K;Lh13AdThHqBX7nV-_sV!E-9Fp56^{QmpIL#r{j`of;hY33({gz$V>Y9pZ zIzak1Nj*_h&Eb4Dy%2Jwa827OCmXo*G08@VxA?Q%;kvv3)~+(!@ttj@(6cpw3`RWM zs!k(sf^NN=xq7L|uG?F9ITbHyoU8QifG`FA_y8+~7fLSmk4k$WEWaFmgf=2F{Q5=b zpa5jS;3=vT%eIGI&VsYyQpIr}<_lFGnQCzdhw{Ld<25%qj^7lp*C2n@vTdFWR-VvO z@vdZ<%ph-g>zIj`l*4${L)CeGN$6hpaO3{vyz`hY`j;#F*H36%o~Ajv=QSxre^A8BCHKFKxU|FmnGyFaq#^D9DB_xK&wYMS z$i2Gfn!iBE%Xa+W`4+tA0>)e0vrPS08gD0t&1J>*qp2_Ga6f;y=t9g7#AMD|Jt+54 zx;P%o`}s#iC|p|dUy4?f&oN_@clISbXGt>LL&5L>EqSWWIXWT$$k>XXqncXXGo8wl zOiAcv>l%|QIK*L@m0^?xgWZVZY&8_Xr)zarTmHm1i_MB1a&qC_YV)_b5)?acdLxiq RS$ef{{sZ~)cm47m007H2!|VV6 literal 1911 zcmV--2Z;C|iwFP!000026V+K+bK5u)e)q57>S>Np0>xWf^WZDC*LP&cj<=>%APGva zAd(?KpOyc<4T@4oiL_-osmZGB2cZvie|?|;b%lg|2cfP?a=Gsl3(v#^-mt$tJATV zWJjlC)6n9*)2;pv(?5+)Pu%D(L|fOHPW+Cwb-rVs)7!5s3v~UalNb^IY#ROH`E7r% zb9=sTWNh!;LWgI!kL>FuH^a-b%i&*tEqi%-ACXkLN*LyX`ehCuu*@b?GADkPQt{Z2 zStdcQ@TCL`5^Tz7w7RH^RAj4~d7+`6VfHk$?V83-jlkAp!^R}S9wRbFBF#vNFv$ej zWDE!)#B7lg97J5;$cOMY=RkxfdFt*yBylQ~G-IT^b%Iko;gz(KSY=`qrxl$Qt-#`C zlm#>zZn4-OUrg&G${4FK1%&%4O>zJmZIA#w_$24zN0f^f_sxMgi#Zkaj_{XRDH9}p zg(rZF?3-phQ(j4A^3J-EZT_TYv z7(&FB3E&K=7h&;`FRv!pACrbn?r8)<1+5VphNEGIhk5plGm<`5v0>CF5znbr!UR9` zizd@kG=!=|BJe`Cm7<_c7ELYsqHJ9QoFs!RmDSK136b#`fCo5v<$3i%cAQkRoq)C z2}w$zTwl(}l=5-kk5g%wiRwwm;3?GJ%X*;7rhQ-Ykb{xmRh_P(tA?c$)$PJp&#(;a z_^#~-Mr%p-1bjHXBWX&5T8ucpgANqbk_gB_4CLl4zHikMSMfoNWaoUDg-!^g5vjLi zM!ledktPOcqLbt$S4CX3h^i;Eso?8!aey*DZF<^9P3pC{KO#ZK$YSHA5p*mVE~Hv> z(t1%gxWIfojj&-Fj_VVo`j+FUnnqyyFx{?-bR1$ov@H#V<*qOAa4`v6gF(JIr1(IF ze?WuY+Y_gxWZpY_dFO1^o=yI7Ua$(s3H=O@g+p*#N&)L6i$p3E}8_%nck@Le)WpEg(co@af`5qh% zNeu7~9`wYCM;b(D1m~tq)g}2J?%xdN6yWhnL z#KoY%tOlF!JTu%EiSyZL^*vuiM^G~Gz~3=R3%fKe2t3>J)h>2D)$kmodRCyTfvxG- z#wO7{qoTgBbmmWMxgr*jTsNtkl40dEhfv$-9cd2Jc1VXktBu-7#Vi?NZJnf^s;TC1 zKAWCoa-?t!%PuDyxb!hjCs_2vlicC*V*9OKWxnG(+e)ElZ2&opc%)aIM&1P7%WmfC zr6#Lwt>NWN{JF8xFGiFr@Fzw@DZEf}see`41LgVU=p(d|EG4gBbao0rmJFVuI<;(l z*ySuZA1+lA4Pm}e<&mit_i-o>V%lzZmE-tL0ecPdS1nuT`JnNHmWnqe%X9{L!@I{^ zyrdk)s{yXg>ouW!-NTjpm;KITy69hy>|Z~jak&WDn(KB=!#0rXYX5ALS`AelfQ13z zJ*Z|>g)3qT58!5jXV;|1Pn2-Lg9KguQ6j)GB`^q(3 zIR~0CuzpuIlZ25g;vG`aF?GAGTgq>T`xnPY9~4H%MA;+$IR0JPh9sPjj-x{)4GrmS zO|z6fRBB2Kv7b<@Gc@M@mLQ%lEkrbnMCn5DZMB#fy)HCx!+b|f;GzbX{XEwO8HZR5 zN{<_u^}3En^icod?e3L*7n)g;6tA2MFL%AtLqPcfjYgRu{50yrgi2)fKppF@X_Y?Z z$sx)n9!XEbZ7^vpV~Q$+UvZ194}jBlE&XQw`^PIdtegmCJAO(c*rxC4)-RF>BdF@v zBCffXQI5FtgCbroxqmX^(hloqM%*NUUJpQaVntB-W?-< xLDFU@fKNkh2h~6E&12KBLryNdYc_wKD?zdIsy70;m8n%L=YRStDGUA`001%rx*-4n From f0d3cb96a4bc453150b74ecef923e5423571c647 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 17 Jul 2020 17:15:25 -0400 Subject: [PATCH 64/76] [Monitoring] Fix issue with ES node detail status (#72298) * Fix issue with ES node detail status * Add test --- .../monitoring/public/alerts/status.tsx | 4 ++ .../components/elasticsearch/node/advanced.js | 4 +- .../elasticsearch/node_detail_status/index.js | 9 ++- .../public/directives/main/index.html | 1 + .../elasticsearch/node/advanced/index.js | 17 +++++- .../monitoring/elasticsearch/node_detail.js | 59 +++++++++++++++++++ .../monitoring/elasticsearch_node_detail.js | 4 ++ 7 files changed, 92 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx index d15dcc9974863..9c262884d7257 100644 --- a/x-pack/plugins/monitoring/public/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -20,6 +20,10 @@ interface Props { export const AlertsStatus: React.FC = (props: Props) => { const { alerts, showBadge = false, showOnlyCount = false } = props; + if (!alerts) { + return null; + } + let atLeastOneDanger = false; const count = Object.values(alerts).reduce((cnt, alertStatus) => { if (alertStatus.states.length) { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index 6fea34ed9c901..b2a17515bbb96 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -19,7 +19,7 @@ import { NodeDetailStatus } from '../node_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; import { FormattedMessage } from '@kbn/i18n/react'; -export const AdvancedNode = ({ nodeSummary, metrics, ...props }) => { +export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { const metricsToShow = [ metrics.node_gc, metrics.node_gc_time, @@ -50,7 +50,7 @@ export const AdvancedNode = ({ nodeSummary, metrics, ...props }) => { - + diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index 18533b3bd4b5e..85b4d0daddade 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -9,8 +9,9 @@ import { SummaryStatus } from '../../summary_status'; import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; +import { AlertsStatus } from '../../../alerts/status'; -export function NodeDetailStatus({ stats, alerts }) { +export function NodeDetailStatus({ stats, alerts = {} }) { const { transport_address: transportAddress, usedHeap, @@ -29,8 +30,10 @@ export function NodeDetailStatus({ stats, alerts }) { const metrics = [ { - label: 'Alerts', - value: {Object.values(alerts).length}, + label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.alerts', { + defaultMessage: 'Alerts', + }), + value: , }, { label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', { diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index 39d357813b3f2..fabd207d72b1f 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -90,6 +90,7 @@ { + describe('Active Nodes', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('monitoring/singlecluster-three-nodes-shard-relocation', { + from: 'Oct 5, 2017 @ 20:31:48.354', + to: 'Oct 5, 2017 @ 20:35:12.176', + }); + + // go to nodes listing + await overview.clickEsNodes(); + expect(await nodesList.isOnListing()).to.be(true); + }); + + after(async () => { + await tearDown(); + }); + + afterEach(async () => { + await PageObjects.monitoring.clickBreadcrumb('~breadcrumbEsNodes'); // return back for next test + }); + + it('should show node summary of master node with 20 indices and 38 shards', async () => { + await nodesList.clickRowByResolver('jUT5KdxfRbORSCWkb5zjmA'); + await nodeDetail.clickAdvanced(); + + expect(await nodeDetail.getSummary()).to.eql({ + transportAddress: 'Transport Address\n127.0.0.1:9300', + jvmHeap: 'JVM Heap\n29%', + freeDiskSpace: 'Free Disk Space\n173.9 GB (37.42%)', + documentCount: 'Documents\n24.8k', + dataSize: 'Data\n50.4 MB', + indicesCount: 'Indices\n20', + shardsCount: 'Shards\n38', + nodeType: 'Type\nMaster Node', + status: 'Status: Online', + }); + }); + + it('should show node summary of data node with 4 indices and 4 shards', async () => { + await nodesList.clickRowByResolver('bwQWH-7IQY-mFPpfoaoFXQ'); + await nodeDetail.clickAdvanced(); + + expect(await nodeDetail.getSummary()).to.eql({ + transportAddress: 'Transport Address\n127.0.0.1:9302', + jvmHeap: 'JVM Heap\n17%', + freeDiskSpace: 'Free Disk Space\n173.9 GB (37.42%)', + documentCount: 'Documents\n240', + dataSize: 'Data\n1.4 MB', + indicesCount: 'Indices\n4', + shardsCount: 'Shards\n4', + nodeType: 'Type\nNode', + status: 'Status: Online', + }); + }); + }); + }); }); } diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js b/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js index 2cfa7628c0c4b..41b69403829f7 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_node_detail.js @@ -19,6 +19,10 @@ export function MonitoringElasticsearchNodeDetailProvider({ getService }) { const SUBJ_SUMMARY_STATUS = `${SUBJ_SUMMARY} > statusIcon`; return new (class ElasticsearchNodeDetail { + async clickAdvanced() { + return testSubjects.click('esNodeDetailAdvancedLink'); + } + async getSummary() { return { transportAddress: await testSubjects.getVisibleText(SUBJ_SUMMARY_TRANSPORT_ADDRESS), From 7519c1f8c33ffea28b9e8daacdbebe05080125f3 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Fri, 17 Jul 2020 16:42:56 -0500 Subject: [PATCH 65/76] use WORKSPACE env var for stack_functional_integration tests, fix navigate path (#71908) Co-authored-by: Elastic Machine --- .../configs/config.stack_functional_integration_base.js | 8 ++++++-- .../test/functional/apps/sample_data/e_commerce.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index a34d158496ba0..96d338a04b01b 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -12,12 +12,16 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; const reportName = 'Stack Functional Integration Tests'; const testsFolder = '../test/functional/apps'; -const stateFilePath = '../../../../../integration-test/qa/envvars.sh'; -const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); +log.info(`WORKSPACE in config file ${process.env.WORKSPACE}`); +const stateFilePath = process.env.WORKSPACE + ? `${process.env.WORKSPACE}/qa/envvars.sh` + : '../../../../../integration-test/qa/envvars.sh'; + +const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { const defaultConfigs = await readConfigFile(require.resolve('../../functional/config')); diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js index 306f30133f6ee..0286f6984e89e 100644 --- a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await browser.setWindowSize(1200, 800); - await PageObjects.common.navigateToUrl('home', '/home/tutorial_directory/sampleData', { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, insertTimestamp: false, }); From 7aa600bff7250655dbbb80b026c67eed46f2969c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 17 Jul 2020 14:53:04 -0700 Subject: [PATCH 66/76] [DOCS] Removes occurrences of X-Pack Security and Reporting (#72302) --- docs/dev-tools/grokdebugger/index.asciidoc | 2 +- docs/settings/monitoring-settings.asciidoc | 2 +- docs/setup/install.asciidoc | 4 ++-- .../monitoring/monitoring-kibana.asciidoc | 15 +++++++------ docs/user/reporting/chromium-sandbox.asciidoc | 12 +++++----- .../reporting/configuring-reporting.asciidoc | 10 ++++----- .../user/reporting/development/index.asciidoc | 10 +++++---- docs/user/reporting/gs-index.asciidoc | 2 +- docs/user/reporting/index.asciidoc | 2 +- docs/user/reporting/script-example.asciidoc | 3 ++- docs/user/reporting/watch-example.asciidoc | 2 +- docs/user/security/reporting.asciidoc | 16 +++++++------- docs/user/security/securing-kibana.asciidoc | 22 +++++++++---------- 13 files changed, 54 insertions(+), 48 deletions(-) diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc index 5162e806edd07..994836de7a1a2 100644 --- a/docs/dev-tools/grokdebugger/index.asciidoc +++ b/docs/dev-tools/grokdebugger/index.asciidoc @@ -32,7 +32,7 @@ in ingest node and Logstash. This example walks you through using the *Grok Debugger*. This tool is automatically enabled in {kib}. -NOTE: If you're using {security}, you must have the `manage_pipeline` +NOTE: If you're using {stack-security-features}, you must have the `manage_pipeline` permission to use the Grok Debugger. . Open the menu, go to *Dev Tools*, then click *Grok Debugger*. diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 48b5b5eb5d0c0..5b8fa0725d96b 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -7,7 +7,7 @@ By default, the Monitoring application is enabled, but data collection is disabled. When you first start {kib} monitoring, you are prompted to -enable data collection. If you are using {security}, you must be +enable data collection. If you are using {stack-security-features}, you must be signed in as a user with the `cluster:manage` privilege to enable data collection. The built-in `superuser` role has this privilege and the built-in `elastic` user has this role. diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index 73036da8f1390..cb47210cb3f08 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -53,8 +53,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for +{ref}/elasticsearch-security.html[{stack-security-features}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index b9ec3982eb3c5..bb8b3e5d42851 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -20,9 +20,10 @@ node in the production cluster. By default, it is is disabled (`false`). + -- NOTE: You can specify this setting in either the `elasticsearch.yml` on each -node or across the cluster as a dynamic cluster setting. If {es} -{security-features} are enabled, you must have `monitor` cluster privileges to -view the cluster settings and `manage` cluster privileges to change them. +node or across the cluster as a dynamic cluster setting. If +{stack-security-features} are enabled, you must have `monitor` cluster +privileges to view the cluster settings and `manage` cluster privileges to +change them. -- @@ -33,7 +34,7 @@ view the cluster settings and `manage` cluster privileges to change them. -- By default, if you are running {kib} locally, go to `http://localhost:5601/`. -If {es} {security-features} are enabled, log in. +If {security-features} are enabled, log in. -- ... Open the menu, then go to *Stack Monitoring*. If data collection is @@ -80,13 +81,13 @@ monitoring cluster prevents production cluster outages from impacting your ability to access your monitoring data. It also prevents monitoring activities from impacting the performance of your production cluster. -If {security} is enabled on the production cluster, use an HTTPS URL such -as `https://:9200` in this setting. +If {security-features} are enabled on the production cluster, use an HTTPS +URL such as `https://:9200` in this setting. =============================== -- -. If the Elastic {security-features} are enabled on the production cluster: +. If {security-features} are enabled on the production cluster: .. Verify that there is a valid user ID and password in the `elasticsearch.username` and diff --git a/docs/user/reporting/chromium-sandbox.asciidoc b/docs/user/reporting/chromium-sandbox.asciidoc index bfef5b8b86c6b..dcb421261c067 100644 --- a/docs/user/reporting/chromium-sandbox.asciidoc +++ b/docs/user/reporting/chromium-sandbox.asciidoc @@ -2,14 +2,16 @@ [[reporting-chromium-sandbox]] === Chromium sandbox -When {reporting} uses the Chromium browser for generating PDF reports, it's recommended to use the sandbox for -an additional layer of security. The Chromium sandbox uses operating system provided mechanisms to ensure that -code execution cannot make persistent changes to the computer or access confidential information. The specific -sandboxing techniques differ for each operating system. +When {report-features} uses the Chromium browser for generating PDF reports, +it's recommended to use the sandbox for an additional layer of security. The +Chromium sandbox uses operating system provided mechanisms to ensure that +code execution cannot make persistent changes to the computer or access +confidential information. The specific sandboxing techniques differ for each +operating system. ==== Linux sandbox The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many -distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. {reporting} +distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. The {report-features} will automatically disable the sandbox when it is running on Debian and CentOS as additional steps are required to enable unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: `Chromium sandbox provides an additional layer of protection, but is not supported for your OS. diff --git a/docs/user/reporting/configuring-reporting.asciidoc b/docs/user/reporting/configuring-reporting.asciidoc index 7489e2cf51f61..ca2d79bb2dec0 100644 --- a/docs/user/reporting/configuring-reporting.asciidoc +++ b/docs/user/reporting/configuring-reporting.asciidoc @@ -2,8 +2,8 @@ [[configuring-reporting]] == Reporting configuration -You can configure settings in `kibana.yml` to control how {reporting} -communicates with the {kib} server, manages background jobs, and captures +You can configure settings in `kibana.yml` to control how the {report-features} +communicate with the {kib} server, manages background jobs, and captures screenshots. See <> for the complete list of settings. @@ -11,9 +11,9 @@ list of settings. [[encryption-keys]] === Encryption keys for multiple {kib} instances -By default, a new encryption key is generated for {reporting} each time -you start {kib}. This means if a static encryption key is not persisted in the -{kib} configuration, any pending reports will fail when you restart {kib}. +By default, a new encryption key is generated for the {report-features} each +time you start {kib}. This means if a static encryption key is not persisted in +the {kib} configuration, any pending reports will fail when you restart {kib}. If you are load balancing across multiple {kib} instances, they need to have the same reporting encryption key. Otherwise, report generation will fail if a diff --git a/docs/user/reporting/development/index.asciidoc b/docs/user/reporting/development/index.asciidoc index a64e540da0c70..4e86c803bd82d 100644 --- a/docs/user/reporting/development/index.asciidoc +++ b/docs/user/reporting/development/index.asciidoc @@ -1,9 +1,11 @@ [role="xpack"] [[reporting-integration]] == Reporting integration -Integrating a {kib} application with {reporting} requires a minimum amount of code, and the goal is to not have to -modify the Reporting code as we add additional applications. Instead, applications abide by a contract that Reporting -uses to determine the information that is required to export CSVs and PDFs. +Integrating a {kib} application with the {report-features} requires a minimum +amount of code, and the goal is to not have to modify the reporting code as we +add additional applications. Instead, applications abide by a contract that +{report-features} use to determine the information that is required to export +CSVs and PDFs. [IMPORTANT] ============================================== @@ -18,7 +20,7 @@ X-Pack uses the `share` plugin of the Kibana platform to register actions in the [float] === Generate job URL -To generate a new {reporting} job, different export types require different `jobParams` that are Rison encoded into a URL +To generate a new reporting job, different export types require different `jobParams` that are Rison encoded into a URL that abide by the following convention: `/api/reporting/generate?jobParams=${rison.encode(jobParams)}`. If you use the aforementioned <> then this detail will be abstracted away, but if you provide a custom UI for generating the report, you will have to generate the URL and create a POST request to the URL. diff --git a/docs/user/reporting/gs-index.asciidoc b/docs/user/reporting/gs-index.asciidoc index 87918ee76340e..46c1fd38b7d69 100644 --- a/docs/user/reporting/gs-index.asciidoc +++ b/docs/user/reporting/gs-index.asciidoc @@ -21,7 +21,7 @@ You can also <>. IMPORTANT: Reports are stored in the `.reporting-*` indices. Any user with access to these indices has access to every report generated by all users. -To use {reporting} in a production environment, +To use {report-features} in a production environment, <>. -- diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 6acdbbe3f0a99..e4e4b461ac2bd 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -19,7 +19,7 @@ image::user/reporting/images/share-button.png["Share"] [float] == Setup -{reporting} is automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which +The {report-features} are automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which runs on the server in headless mode to load {kib} and capture the rendered {kib} charts as images. Chromium is an open-source project not related to Elastic, but the Chromium binary for {kib} has been custom-built by Elastic to ensure it diff --git a/docs/user/reporting/script-example.asciidoc b/docs/user/reporting/script-example.asciidoc index 88f48ad1d3182..94301fc6fb448 100644 --- a/docs/user/reporting/script-example.asciidoc +++ b/docs/user/reporting/script-example.asciidoc @@ -19,7 +19,8 @@ curl \ // CONSOLE <1> `POST` method is required. -<2> Provide user credentials for a user with permission to access Kibana and X-Pack reporting. +<2> Provide user credentials for a user with permission to access Kibana and +{report-features}. <3> The `kbn-version` header is required for all `POST` requests to Kibana. **The value must match the dotted-numeral version of the Kibana instance.** <4> The POST URL. You can copy and paste the URL for any report from the Kibana UI. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 627e31017230c..253722fefecc0 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -52,7 +52,7 @@ report from the Kibana UI. <3> Optional, default is 40 <4> Optional, default is 15s <5> Provide user credentials for a user with permission to access Kibana and -{reporting}. +the {report-features}. //For more information, see <>. //<>. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 30340e1db989a..4e02759ce99cb 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -5,8 +5,8 @@ Reporting operates by creating and updating documents in {es} in response to user actions in {kib}. -To use {reporting} with {security} enabled, you need to -<>. +To use {report-features} with {security-features} enabled, you need to +<>. If you are automatically generating reports with {ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. @@ -118,10 +118,10 @@ reporting_user: === Secure the reporting endpoints In a production environment, you should restrict access to -the {reporting} endpoints to authorized users. This requires that you: +the reporting endpoints to authorized users. This requires that you: -. Enable {security} on your {es} cluster. For more information, -see {ref}/security-getting-started.html[Getting Started with Security]. +. Enable {stack-security-features} on your {es} cluster. For more information, +see {ref}/security-getting-started.html[Getting started with security]. . Configure TLS/SSL encryption for the {kib} server. For more information, see <>. . Specify the {kib} server's CA certificate chain in `elasticsearch.yml`: @@ -150,13 +150,13 @@ For more information, see {ref}/notification-settings.html#ssl-notification-sett -- . Add one or more users who have the permissions -necessary to use {kib} and {reporting}. For more information, see +necessary to use {kib} and {report-features}. For more information, see <>. -Once you've enabled SSL for {kib}, all requests to the {reporting} endpoints +Once you've enabled SSL for {kib}, all requests to the reporting endpoints must include valid credentials. For example, see the following page which includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{ref}/how-watcher-works.html[How Watcher works]. +{ref}/how-watcher-works.html[How {watcher} works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index b30acd0ed2e53..0177ac94bd402 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -5,21 +5,21 @@ Configure security ++++ -{kib} users have to log in when {security} is enabled on your cluster. You -configure {security} roles for your {kib} users to control what data those users -can access. +{kib} users have to log in when {stack-security-features} are enabled on your +cluster. You configure roles for your {kib} users to control what data those +users can access. Most requests made through {kib} to {es} are authenticated by using the credentials of the logged-in user. There are, however, a few internal requests that the {kib} server needs to make to the {es} cluster. For this reason, you must configure credentials for the {kib} server to use for those requests. -With {security} enabled, if you load a {kib} dashboard that accesses data in an -index that you are not authorized to view, you get an error that indicates the -index does not exist. {security} do not currently provide a way to control which -users can load which dashboards. +With {security-features} enabled, if you load a {kib} dashboard that accesses +data in an index that you are not authorized to view, you get an error that +indicates the index does not exist. The {security-features} do not currently +provide a way to control which users can load which dashboards. -To use {kib} with {security}: +To use {kib} with {security-features}: . {ref}/configuring-security.html[Configure security in {es}]. @@ -38,8 +38,8 @@ elasticsearch.password: "kibanapassword" The {kib} server submits requests as this user to access the cluster monitoring APIs and the `.kibana` index. The server does _not_ need access to user indices. -The password for the built-in `kibana_system` user is typically set as part of the -{security} configuration process on {es}. For more information, see +The password for the built-in `kibana_system` user is typically set as part of +the security configuration process on {es}. For more information, see {ref}/built-in-users.html[Built-in users]. -- @@ -53,7 +53,7 @@ as the encryption key. xpack.security.encryptionKey: "something_at_least_32_characters" -------------------------------------------------------------------------------- -For more information, see <>. +For more information, see <>. -- . Optional: Set a timeout to expire idle sessions. By default, a session stays From dc7db09533f62d43a70e2a903c89e19b81ae8287 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 17 Jul 2020 16:29:23 -0600 Subject: [PATCH 67/76] [Maps] convert SavedGisMap to TS (#72286) * [Maps] convert SavedGisMap to TS * i18n translate new map title --- .../{saved_gis_map.js => saved_gis_map.ts} | 65 ++++++++++++------- .../maps/public/selectors/map_selectors.ts | 4 +- 2 files changed, 42 insertions(+), 27 deletions(-) rename x-pack/plugins/maps/public/routing/bootstrap/services/{saved_gis_map.js => saved_gis_map.ts} (64%) diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts similarity index 64% rename from x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js rename to x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts index f8c783f673bab..4b474424bcdab 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts @@ -5,7 +5,13 @@ */ import _ from 'lodash'; -import { createSavedObjectClass } from '../../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectReference } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, getMapZoom, @@ -18,65 +24,74 @@ import { } from '../../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; import { copyPersistentState } from '../../../reducers/util'; +// @ts-expect-error import { extractReferences, injectReferences } from '../../../../common/migrations/references'; import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { getStore } from '../../store_operations'; +import { MapStoreState } from '../../../reducers/store'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; + +export interface ISavedGisMap extends SavedObject { + layerListJSON?: string; + mapStateJSON?: string; + uiStateJSON?: string; + getLayerList(): LayerDescriptor[]; + syncWithStore(): void; +} -export function createSavedGisMapClass(services) { +export function createSavedGisMapClass(services: SavedObjectKibanaServices) { const SavedObjectClass = createSavedObjectClass(services); - class SavedGisMap extends SavedObjectClass { - static type = MAP_SAVED_OBJECT_TYPE; + class SavedGisMap extends SavedObjectClass implements ISavedGisMap { + public static type = MAP_SAVED_OBJECT_TYPE; // Mappings are used to place object properties into saved object _source - static mapping = { + public static mapping = { title: 'text', description: 'text', mapStateJSON: 'text', layerListJSON: 'text', uiStateJSON: 'text', }; - static fieldOrder = ['title', 'description']; - static searchSource = false; + public static fieldOrder = ['title', 'description']; + public static searchSource = false; - constructor(id) { + public showInRecentlyAccessed = true; + public layerListJSON?: string; + public mapStateJSON?: string; + public uiStateJSON?: string; + + constructor(id: string) { super({ type: SavedGisMap.type, mapping: SavedGisMap.mapping, searchSource: SavedGisMap.searchSource, extractReferences, - injectReferences: (savedObject, references) => { + injectReferences: (savedObject: ISavedGisMap, references: SavedObjectReference[]) => { const { attributes } = injectReferences({ attributes: { layerListJSON: savedObject.layerListJSON }, references, }); savedObject.layerListJSON = attributes.layerListJSON; - - const indexPatternIds = references - .filter((reference) => { - return reference.type === 'index-pattern'; - }) - .map((reference) => { - return reference.id; - }); - savedObject.indexPatternIds = _.uniq(indexPatternIds); }, // if this is null/undefined then the SavedObject will be assigned the defaults - id: id, + id, // default values that will get assigned if the doc is new defaults: { - title: 'New Map', + title: i18n.translate('xpack.maps.newMapTitle', { + defaultMessage: 'New Map', + }), description: '', }, }); - this.showInRecentlyAccessed = true; - } - getFullPath() { - return getExistingMapPath(this.id); + this.getFullPath = () => { + return getExistingMapPath(this.id!); + }; } getLayerList() { @@ -84,7 +99,7 @@ export function createSavedGisMapClass(services) { } syncWithStore() { - const state = getStore().getState(); + const state: MapStoreState = getStore().getState(); const layerList = getLayerListRaw(state); const layerListConfigOnly = copyPersistentState(layerList); this.layerListJSON = JSON.stringify(layerListConfigOnly); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index fe2cfec3c761c..e082398a02a9e 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -52,6 +52,7 @@ import { ISource } from '../classes/sources/source'; import { ITMSSource } from '../classes/sources/tms_source'; import { IVectorSource } from '../classes/sources/vector_source'; import { ILayer } from '../classes/layers/layer'; +import { ISavedGisMap } from '../routing/bootstrap/services/saved_gis_map'; function createLayerInstance( layerDescriptor: LayerDescriptor, @@ -419,12 +420,11 @@ export const areLayersLoaded = createSelector( export function hasUnsavedChanges( state: MapStoreState, - savedMap: unknown, + savedMap: ISavedGisMap, initialLayerListConfig: LayerDescriptor[] ) { const layerListConfigOnly = copyPersistentState(getLayerListRaw(state)); - // @ts-expect-error const savedLayerList = savedMap.getLayerList(); return !savedLayerList From ef875cf9fe3ff27f9014a7c5d7418c27acbbacef Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 17 Jul 2020 16:41:52 -0700 Subject: [PATCH 68/76] [esArchiver] actually re-delete the .kibana index if we lose recreate race (#72354) Co-authored-by: spalger --- src/es_archiver/lib/indices/create_index_stream.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/es_archiver/lib/indices/create_index_stream.ts b/src/es_archiver/lib/indices/create_index_stream.ts index 5629f95c7c9c6..fa4c95dc73166 100644 --- a/src/es_archiver/lib/indices/create_index_stream.ts +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -20,7 +20,6 @@ import { Transform, Readable } from 'stream'; import { inspect } from 'util'; -import { get, once } from 'lodash'; import { Client } from 'elasticsearch'; import { ToolingLog } from '@kbn/dev-utils'; @@ -54,7 +53,7 @@ export function createCreateIndexStream({ // If we're trying to import Kibana index docs, we need to ensure that // previous indices are removed so we're starting w/ a clean slate for // migrations. This only needs to be done once per archive load operation. - const deleteKibanaIndicesOnce = once(deleteKibanaIndices); + let kibanaIndexAlreadyDeleted = false; async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { @@ -70,8 +69,9 @@ export function createCreateIndexStream({ async function attemptToCreate(attemptNumber = 1) { try { - if (isKibana) { - await deleteKibanaIndicesOnce({ client, stats, log }); + if (isKibana && !kibanaIndexAlreadyDeleted) { + await deleteKibanaIndices({ client, stats, log }); + kibanaIndexAlreadyDeleted = true; } await client.indices.create({ @@ -90,6 +90,7 @@ export function createCreateIndexStream({ err?.body?.error?.reason?.includes('index exists with the same name as the alias') && attemptNumber < 3 ) { + kibanaIndexAlreadyDeleted = false; const aliasStr = inspect(aliases); log.info( `failed to create aliases [${aliasStr}] because ES indicated an index/alias already exists, trying again` @@ -98,10 +99,7 @@ export function createCreateIndexStream({ return; } - if ( - get(err, 'body.error.type') !== 'resource_already_exists_exception' || - attemptNumber >= 3 - ) { + if (err?.body?.error?.type !== 'resource_already_exists_exception' || attemptNumber >= 3) { throw err; } From 105e3a6c7ecc39442f10c8ec030f6297e0b56cd4 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Sat, 18 Jul 2020 17:47:53 +0200 Subject: [PATCH 69/76] update chromedriver to 84 (#72228) Co-authored-by: Elastic Machine --- package.json | 4 ++-- yarn.lock | 32 ++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index a22871e314bae..ceb3ac4cca937 100644 --- a/package.json +++ b/package.json @@ -322,7 +322,7 @@ "@types/browserslist-useragent": "^3.0.0", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.10", - "@types/chromedriver": "^2.38.0", + "@types/chromedriver": "^81.0.0", "@types/classnames": "^2.2.9", "@types/color": "^3.0.0", "@types/d3": "^3.5.43", @@ -411,7 +411,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^83.0.0", + "chromedriver": "^84.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index e5975efe0b7d5..3924655b5e43e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4784,10 +4784,10 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== -"@types/chromedriver@^2.38.0": - version "2.38.0" - resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-2.38.0.tgz#971032b73eb7f44036f4f5bed59a7fd5b468014f" - integrity sha512-vcPGkZt1y2YVXKAY8SwCvU0u9mgw9+7tBV4HGb0YX/6bu1WXbb61bf8Y/N+xNCYwEj/Ug1UAMnhCcsSohXzRXw== +"@types/chromedriver@^81.0.0": + version "81.0.0" + resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-81.0.0.tgz#d7c97bd2b1de34270f44e60f4eee43bfdba3a8e2" + integrity sha512-Oqwo24DPn5lYI66aA74ApKrfAqVFEjC66raiB/2eHhhryYiumlMpRTR/++riaRcXmfrLXrIiNTtE+Op4vGCIFQ== dependencies: "@types/node" "*" @@ -6726,6 +6726,13 @@ agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" @@ -9970,15 +9977,16 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^83.0.0: - version "83.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-83.0.0.tgz#75d7d838e58014658c3990089464166fef951926" - integrity sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ== +chromedriver@^84.0.0: + version "84.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-84.0.0.tgz#980d72bf0990bbfbce282074d15448296c55d89d" + integrity sha512-fNX9eT1C38D1W8r5ss9ty42eDK+GIkCZVKukfeDs0XSBeKfyT0o/vbMdPr9MUkWQ+vIcFAS5hFGp9E3+xoaMeQ== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" del "^5.1.0" extract-zip "^2.0.0" + https-proxy-agent "^2.2.4" mkdirp "^1.0.4" tcp-port-used "^1.0.1" @@ -17448,6 +17456,14 @@ https-proxy-agent@2.2.1, https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" +https-proxy-agent@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + https-proxy-agent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" From a28463d82a38bac9b04470c8836c14536a528220 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 20 Jul 2020 10:22:05 +0200 Subject: [PATCH 70/76] Fix float percentiles line chart (#71902) Co-authored-by: Elastic Machine --- .../public/vislib/lib/types/point_series.js | 18 +- .../vislib/lib/types/point_series.test.js | 20 +- .../types/testdata_linechart_percentile.json | 608 +++++++++--------- ...data_linechart_percentile_float_value.json | 463 +++++++++++++ ...nechart_percentile_float_value_result.json | 456 +++++++++++++ .../testdata_linechart_percentile_result.json | 333 +++++++++- 6 files changed, 1586 insertions(+), 312 deletions(-) create mode 100644 src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json create mode 100644 src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js index 438c071d74532..03b5af2572d94 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js @@ -21,7 +21,23 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; function getSeriId(seri) { - return seri.id && seri.id.indexOf('.') !== -1 ? seri.id.split('.')[0] : seri.id; + if (!seri.id) { + return; + } + // Ideally the format should be either ID or "ID.SERIES" + // but for some values the SERIES components gets a bit more complex + + // Float values are serialized as strings tuples (i.e. ['99.1']) rather than regular numbers (99.1) + // so the complete ids are in the format ID.['SERIES']: hence the specific brackets handler + const bracketsMarker = seri.id.indexOf('['); + if (bracketsMarker > -1) { + return seri.id.substring(0, bracketsMarker); + } + // Here's the dot check is enough + if (seri.id.indexOf('.') > -1) { + return seri.id.split('.')[0]; + } + return seri.id; } const createSeriesFromParams = (cfg, seri) => { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js index 62ff873f28134..b46054f3cd983 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js @@ -20,6 +20,8 @@ import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_s import { vislibPointSeriesTypes } from './point_series'; import percentileTestdata from './testdata_linechart_percentile.json'; import percentileTestdataResult from './testdata_linechart_percentile_result.json'; +import percentileTestdataFloatValue from './testdata_linechart_percentile_float_value.json'; +import percentileTestdataFloatValueResult from './testdata_linechart_percentile_float_value_result.json'; const maxBucketData = { get: (prop) => { @@ -215,18 +217,26 @@ describe('Point Series Config Type Class Test Suite', function () { }); describe('line chart', function () { - beforeEach(function () { + function prepareData({ cfg, data }) { const percentileDataObj = { get: (prop) => { return maxBucketData[prop] || maxBucketData.data[prop] || null; }, getLabels: () => [], - data: percentileTestdata.data, + data: data, }; - parsedConfig = vislibPointSeriesTypes.line(percentileTestdata.cfg, percentileDataObj); - }); + const parsedConfig = vislibPointSeriesTypes.line(cfg, percentileDataObj); + return parsedConfig; + } + it('should render a percentile line chart', function () { - expect(JSON.stringify(parsedConfig)).toEqual(JSON.stringify(percentileTestdataResult)); + const parsedConfig = prepareData(percentileTestdata); + expect(parsedConfig).toMatchObject(percentileTestdataResult); + }); + + it('should render a percentile line chart when value is float', function () { + const parsedConfig = prepareData(percentileTestdataFloatValue); + expect(parsedConfig).toMatchObject(percentileTestdataFloatValueResult); }); }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json index 818d9133938fa..d52cb18727c05 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json @@ -140,320 +140,320 @@ } }, "yAxisLabel": "", - "series": [ - { - "id": "1.1", - "rawId": "col-1-1.1", - "label": "1st percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 + "hits": 2 + }, + "series": [ + { + "id": "1.1", + "rawId": "col-1-1.1", + "label": "1st percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" + "column": 1, + "row": 0, + "value": 116.33676605224609 }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - } - ] - }, - { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + } + ] + }, + { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" + "column": 2, + "row": 0, + "value": 658 }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - } - ], - "hits": 2 - }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + } + ], "type": "series", "labels": [ "1st percentile of AvgTicketPrice", diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json new file mode 100644 index 0000000000000..6e1a707229974 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json @@ -0,0 +1,463 @@ +{ + "cfg": { + "addLegend": true, + "addTimeMarker": false, + "addTooltip": true, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "labels": { + "show": true, + "truncate": 100 + }, + "position": "bottom", + "scale": { + "type": "linear" + }, + "show": true, + "style": {}, + "title": {}, + "type": "category" + } + ], + "dimensions": { + "x": { + "accessor": 0, + "format": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "params": { + "date": true, + "interval": "P1D", + "format": "YYYY-MM-DD", + "bounds": { + "min": "2019-05-10T04:00:00.000Z", + "max": "2019-05-12T10:18:57.342Z" + } + }, + "aggType": "date_histogram" + }, + "y": [ + { + "accessor": 1, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + }, + { + "accessor": 2, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + } + ] + }, + "grid": { + "categoryLines": false, + "style": { + "color": "#eee" + } + }, + "legendPosition": "right", + "seriesParams": [ + { + "data": { + "id": "1", + "label": "Percentiles of AvgTicketPrice" + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ], + "times": [], + "type": "area", + "valueAxes": [ + { + "id": "ValueAxis-1", + "labels": { + "filter": false, + "rotate": 0, + "show": true, + "truncate": 100 + }, + "name": "LeftAxis-1", + "position": "left", + "scale": { + "mode": "normal", + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Percentiles of AvgTicketPrice" + }, + "type": "value" + } + ] + }, + "data": { + "uiState": {}, + "data": { + "xAxisOrderedValues": [ + 1557460800000, + 1557547200000 + ], + "xAxisFormat": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "xAxisLabel": "timestamp per day", + "ordered": { + "interval": "P1D", + "date": true, + "min": 1557460800000, + "max": 1557656337342 + }, + "yAxisFormat": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "yAxisLabel": "", + "hits": 2 + }, + "series": [ + { + "id": "1.['1.1']", + "rawId": "col-1-1.['1.1']", + "label": "1.1th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + } + ] + }, + { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + } + ], + "type": "series", + "labels": [ + "1.1th percentile of AvgTicketPrice", + "50th percentile of AvgTicketPrice" + ] + } +} \ No newline at end of file diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json new file mode 100644 index 0000000000000..f7dd18f5eb712 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json @@ -0,0 +1,456 @@ +{ + "addLegend": true, + "addTimeMarker": false, + "addTooltip": true, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "labels": { + "show": true, + "truncate": 100 + }, + "position": "bottom", + "scale": { + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Date Histogram" + }, + "type": "category" + } + ], + "dimensions": { + "x": { + "accessor": 0, + "format": { + "id": "date", + "params": { + "pattern": "YYYY-MM-DD" + } + }, + "params": { + "date": true, + "interval": "P1D", + "format": "YYYY-MM-DD", + "bounds": { + "min": "2019-05-10T04:00:00.000Z", + "max": "2019-05-12T10:18:57.342Z" + } + }, + "aggType": "date_histogram" + }, + "y": [ + { + "accessor": 1, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + }, + { + "accessor": 2, + "format": { + "id": "number", + "params": { + "pattern": "$0,0.[00]" + } + }, + "params": {}, + "aggType": "percentiles" + } + ] + }, + "grid": { + "categoryLines": false, + "style": { + "color": "#eee" + } + }, + "legendPosition": "right", + "seriesParams": [ + { + "data": { + "id": "1", + "label": "Percentiles of AvgTicketPrice" + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ], + "times": [], + "type": "point_series", + "valueAxes": [ + { + "id": "ValueAxis-1", + "labels": { + "filter": false, + "rotate": 0, + "show": true, + "truncate": 100 + }, + "name": "LeftAxis-1", + "position": "left", + "scale": { + "mode": "normal", + "type": "linear" + }, + "show": true, + "style": {}, + "title": { + "text": "Percentiles of AvgTicketPrice" + }, + "type": "value" + } + ], + "chartTitle": {}, + "mode": "normal", + "tooltip": { + "show": true + }, + "charts": [ + { + "type": "point_series", + "addTimeMarker": false, + "series": [ + { + "show": true, + "type": "area", + "mode": "normal", + "drawLinesBetweenPoints": true, + "showCircles": true, + "data": { + "id": "1.['1.1']", + "rawId": "col-1-1.['1.1']", + "label": "1.1th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1.1th percentile of AvgTicketPrice", + "seriesId": "col-1-1.['1.1']" + } + ] + } + }, + { + "data": { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.['1.1']", + "name": "1.1th percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.['1.1']": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.['1.1']": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ] + } + ], + "enableHover": true +} \ No newline at end of file diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json index d50d20a70608b..02062c987564e 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json @@ -122,8 +122,337 @@ { "type": "point_series", "addTimeMarker": false, - "series": [] + "series": [ + { + "data": { + "id": "1.1", + "rawId": "col-1-1.1", + "label": "1st percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 116.33676605224609, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 0, + "value": 116.33676605224609 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + }, + { + "x": 1557547200000, + "y": 223, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 1, + "row": 1, + "value": 223 + }, + "parent": null, + "series": "1st percentile of AvgTicketPrice", + "seriesId": "col-1-1.1" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + }, + { + "data": { + "id": "1.50", + "rawId": "col-2-1.50", + "label": "50th percentile of AvgTicketPrice", + "values": [ + { + "x": 1557460800000, + "y": 658.8453063964844, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 0, + "value": 1557460800000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 0, + "value": 658 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + }, + { + "x": 1557547200000, + "y": 756, + "extraMetrics": [], + "xRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 0, + "row": 1, + "value": 1557547200000 + }, + "yRaw": { + "table": { + "columns": [ + { + "id": "col-0-2", + "name": "timestamp per day" + }, + { + "id": "col-1-1.1", + "name": "1st percentile of AvgTicketPrice" + }, + { + "id": "col-2-1.50", + "name": "50th percentile of AvgTicketPrice" + } + ], + "rows": [ + { + "col-0-2": 1557460800000, + "col-1-1.1": 116, + "col-2-1.50": 658 + }, + { + "col-0-2": 1557547200000, + "col-1-1.1": 223, + "col-2-1.50": 756 + } + ] + }, + "column": 2, + "row": 1, + "value": 756.2283554077148 + }, + "parent": null, + "series": "50th percentile of AvgTicketPrice", + "seriesId": "col-2-1.50" + } + ] + }, + "drawLinesBetweenPoints": true, + "interpolate": "cardinal", + "mode": "normal", + "show": "true", + "showCircles": true, + "type": "line", + "valueAxis": "ValueAxis-1" + } + ] } ], "enableHover": true -} +} \ No newline at end of file From 7cee2a6b15175928b38bb3d5d5531d7b887f0343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 20 Jul 2020 09:25:39 +0100 Subject: [PATCH 71/76] [Observability] Remove app logos (#72259) * removing app logos * fixing TS error --- .../public/components/app/empty_section/index.tsx | 2 -- x-pack/plugins/observability/public/pages/landing/index.tsx | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx index e19bf1678bc01..4c830b2b2f094 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx @@ -15,8 +15,6 @@ export const EmptySection = ({ section }: Props) => { return ( {section.title}} titleSize="xs" body={{section.description}} diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index da46791d9e855..81485953f8713 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -10,7 +10,6 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiImage, EuiSpacer, EuiText, @@ -19,10 +18,10 @@ import { import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; +import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { appsSection } from '../home/section'; -import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; const EuiCardWithoutPadding = styled(EuiCard)` padding: 0; @@ -68,7 +67,6 @@ export const LandingPage = () => { } title={

{app.title}

From b39c46eeac36d65eaaf18bdbaa29ba86d9cea26c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 20 Jul 2020 09:30:06 +0100 Subject: [PATCH 72/76] [ML] Disabling secondary auth headers when security is disabled (#72371) Co-authored-by: Elastic Machine --- x-pack/plugins/ml/server/lib/request_authorization.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/server/lib/request_authorization.ts b/x-pack/plugins/ml/server/lib/request_authorization.ts index 01df0900b96f4..d40909a3a3a4c 100644 --- a/x-pack/plugins/ml/server/lib/request_authorization.ts +++ b/x-pack/plugins/ml/server/lib/request_authorization.ts @@ -7,7 +7,9 @@ import { KibanaRequest } from 'kibana/server'; export function getAuthorizationHeader(request: KibanaRequest) { - return { - headers: { 'es-secondary-authorization': request.headers.authorization }, - }; + return request.headers.authorization === undefined + ? {} + : { + headers: { 'es-secondary-authorization': request.headers.authorization }, + }; } From ec4f9d50ba311122cf5f6583e790a86db13e462a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 20 Jul 2020 10:43:12 +0200 Subject: [PATCH 73/76] Migrate and cleanup legacy scss (#69369) --- src/core/public/index.scss | 3 +- .../public/styles}/_ace_overrides.scss | 5 +- src/core/public/styles/_base.scss | 58 +++ src/core/public/styles/_index.scss | 2 + src/legacy/ui/public/_index.scss | 3 - .../ui/public/styles/_legacy/_base.scss | 153 -------- .../ui/public/styles/_legacy/_index.scss | 7 - .../styles/_legacy/components/_config.scss | 11 - .../_legacy/components/_control_group.scss | 67 ---- .../styles/_legacy/components/_hintbox.scss | 59 --- .../styles/_legacy/components/_index.scss | 13 - .../styles/_legacy/components/_input.scss | 12 - .../styles/_legacy/components/_kui_forms.scss | 29 -- .../styles/_legacy/components/_navbar.scss | 86 ----- .../_legacy/components/_pagination.scss | 56 --- .../styles/_legacy/components/_spinner.scss | 6 - .../styles/_legacy/components/_table.scss | 46 --- .../styles/_legacy/components/_truncate.scss | 3 - .../styles/_legacy/components/_ui_select.scss | 357 ------------------ src/legacy/ui/public/styles/_mixins.scss | 2 - .../public/application/_discover.scss | 12 + .../discover/public/application/_hacks.scss | 4 - .../discover/public/application/_mixins.scss | 27 -- .../angular/doc_table/_doc_table.scss | 77 +++- .../application/angular/doc_table/index.scss | 2 - .../discover/public/application/index.scss | 13 +- .../public/paginate/_paginate.scss | 57 +++ .../kibana_legacy/public/paginate/paginate.js | 1 + src/plugins/timelion/public/_base.scss | 19 + .../timelion/public/directives/_form.scss} | 40 +- .../timelion/public/directives/_index.scss | 3 + .../directives/_saved_object_finder.scss} | 6 +- src/plugins/timelion/public/index.scss | 1 + .../public/agg_table/_agg_table.scss | 4 + .../plugins/ml/public/application/_hacks.scss | 36 -- .../server/lib/layouts/preserve_layout.css | 1 - .../reporting/server/lib/layouts/print.css | 1 - 37 files changed, 251 insertions(+), 1031 deletions(-) rename src/{legacy/ui/public/styles/_legacy/components => core/public/styles}/_ace_overrides.scss (96%) create mode 100644 src/core/public/styles/_base.scss create mode 100644 src/core/public/styles/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/_base.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_config.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_control_group.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_hintbox.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_index.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_input.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_navbar.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_pagination.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_spinner.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_table.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_truncate.scss delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_ui_select.scss delete mode 100644 src/plugins/discover/public/application/_hacks.scss delete mode 100644 src/plugins/discover/public/application/_mixins.scss create mode 100644 src/plugins/kibana_legacy/public/paginate/_paginate.scss create mode 100644 src/plugins/timelion/public/_base.scss rename src/{legacy/ui/public/styles/_legacy/_mixins.scss => plugins/timelion/public/directives/_form.scss} (58%) rename src/{legacy/ui/public/styles/_legacy/components/_list_group_menu.scss => plugins/timelion/public/directives/_saved_object_finder.scss} (85%) diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 87825350b4e98..c2ad2841d5a77 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -2,6 +2,5 @@ @import './chrome/index'; @import './overlays/index'; @import './rendering/index'; +@import './styles/index'; -// Global styles need to be migrated -@import '../../legacy/ui/public/styles/_legacy/_index'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss b/src/core/public/styles/_ace_overrides.scss similarity index 96% rename from src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss rename to src/core/public/styles/_ace_overrides.scss index 2f0bc011f6a5c..30acdbbc80975 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_ace_overrides.scss +++ b/src/core/public/styles/_ace_overrides.scss @@ -1,6 +1,3 @@ -@import '@elastic/eui/src/components/call_out/variables'; -@import '@elastic/eui/src/components/call_out/mixins'; - // SASSTODO: Replace with an EUI editor // Intentionally not using the EuiCodeBlock colors here because they actually change // hue from light to dark theme. So some colors would change while others wouldn't. @@ -181,7 +178,7 @@ } &.ace_multiselect .ace_selection.ace_start { - box-shadow: 0 0 3px 0px $euiColorEmptyShade; + box-shadow: 0 0 3px 0 $euiColorEmptyShade; } .ace_marker-layer .ace_step { diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss new file mode 100644 index 0000000000000..9b06b526fc7dd --- /dev/null +++ b/src/core/public/styles/_base.scss @@ -0,0 +1,58 @@ +@import '@elastic/eui/src/components/collapsible_nav/variables'; +// Application Layout + +// chrome-context +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) .content { + display: flex; + flex-flow: row nowrap; + width: 100%; + height: 100%; + overflow: hidden; +} + +.application, +.app-container { + > * { + position: relative; + } +} + +.application { + position: relative; + z-index: 0; + display: flex; + flex-grow: 1; + flex-shrink: 0; + flex-basis: auto; + flex-direction: column; + + > * { + flex-shrink: 0; + } +} + +// We apply brute force focus states to anything not coming from Eui +// which has focus states designed at the component level. +// You can also use "kbn-resetFocusState" to not apply the default focus +// state. This is useful when you've already hand crafted your own +// focus states in Kibana. +:focus { + &:not([class^='eui']):not(.kbn-resetFocusState) { + @include euiFocusRing; + } +} + +// A necessary hack so that the above focus policy doesn't pollute some EUI +// entrenched inputs. +.euiComboBox { + // :not() specificity needed to override the above + input:not([class^='eui']):focus { + animation: none !important; + } +} + +.euiBody--collapsibleNavIsDocked .euiBottomBar { + margin-left: $euiCollapsibleNavWidth; +} diff --git a/src/core/public/styles/_index.scss b/src/core/public/styles/_index.scss new file mode 100644 index 0000000000000..600414402c278 --- /dev/null +++ b/src/core/public/styles/_index.scss @@ -0,0 +1,2 @@ +@import './base'; +@import './ace_overrides'; diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 323de2ea7d263..a441b773d4a4e 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -1,6 +1,3 @@ -// Legacy styles to come before all -@import './styles/_legacy/index'; - // Prefix all styles with "kbn" to avoid conflicts. // Examples // kbnChart diff --git a/src/legacy/ui/public/styles/_legacy/_base.scss b/src/legacy/ui/public/styles/_legacy/_base.scss deleted file mode 100644 index 877ae033ae584..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/_base.scss +++ /dev/null @@ -1,153 +0,0 @@ -@import '@elastic/eui/src/components/collapsible_nav/variables'; - -// Forms - -// Angular form states -input.ng-invalid, -textarea.ng-invalid, -select.ng-invalid { - &.ng-dirty, - &.ng-touched { - border-color: $euiColorDanger !important; - } -} - -input[type='radio'], -input[type='checkbox'], -.radio, -.radio-inline, -.checkbox, -.checkbox-inline { - &[disabled], - fieldset[disabled] & { - cursor: default; - opacity: 0.8; - } -} - -.checkbox label { - display: flex; - align-items: center; - padding-left: 0 !important; - - input[type='checkbox'] { - float: none; - margin: 0 $euiSizeXS; - position: static; - } -} - -// Application Layout - -// chrome-context -// TODO #64541 -// Delete this block -.chrHeaderWrapper:not(.headerWrapper) .content { - display: flex; - flex-flow: row nowrap; - width: 100%; - height: 100%; - overflow: hidden; -} - -.application, -.app-container { - > * { - position: relative; - } - - > config { - z-index: 1; - } - - > navbar { - padding-bottom: $euiSizeS; - } - - > nav, - > navbar { - z-index: 2 !important; - } -} - -.application { - position: relative; - z-index: 0; - display: flex; - flex-grow: 1; - flex-shrink: 0; - flex-basis: auto; - flex-direction: column; - - > * { - flex-shrink: 0; - } -} - -[fixed-scroll] { - overflow-x: auto; - padding-bottom: 0; - - + .fixed-scroll-scroller { - position: fixed; - bottom: 0; - overflow-x: auto; - overflow-y: hidden; - } -} - -// Too overused in many places to be moved elsewhere - -.page-row { - padding: 0 $euiSize; - margin: $euiSize 0; -} - -.page-row-text { - color: $euiColorDarkShade; - font-size: $euiFontSizeS; -} - -// We apply brute force focus states to anything not coming from Eui -// which has focus states designed at the component level. -// You can also use "kbn-resetFocusState" to not apply the default focus -// state. This is useful when you've already hand crafted your own -// focus states in Kibana. -:focus { - &:not([class^='eui']):not(.kbn-resetFocusState) { - @include euiFocusRing; - } -} - -// A necessary hack so that the above focus policy doesn't pollute some EUI -// entrenched inputs. -.euiComboBox { - // :not() specificity needed to override the above - input:not([class^='eui']):focus { - animation: none !important; - } -} - -.euiBody--collapsibleNavIsDocked .euiBottomBar { - margin-left: $euiCollapsibleNavWidth; -} - -// Utility classes - -.fullWidth { - width: 100% !important; -} - -.small { - font-size: 0.9em !important; -} -.smaller { - font-size: 0.8em !important; -} -.smallest { - font-size: 0.7em !important; -} - -.text-monospace { - font-family: $euiCodeFontFamily; -} diff --git a/src/legacy/ui/public/styles/_legacy/_index.scss b/src/legacy/ui/public/styles/_legacy/_index.scss deleted file mode 100644 index a0b1a98b09b7d..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/_index.scss +++ /dev/null @@ -1,7 +0,0 @@ -// // -// // KIBANA THEME -@import './base'; -@import './mixins'; - -// // Components -@import './components/index'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_config.scss b/src/legacy/ui/public/styles/_legacy/components/_config.scss deleted file mode 100644 index b56826f1e7088..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_config.scss +++ /dev/null @@ -1,11 +0,0 @@ -// SASSTODO: Selector is so generic it's hard to find if it's actually used -.config { - @extend .navbar !optional; - @extend .navbar-default !optional; - border-bottom: 1px solid transparent; - - .container-fluid { - background-color: $euiPageBackgroundColor; - padding: $euiSizeS; - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_control_group.scss b/src/legacy/ui/public/styles/_legacy/components/_control_group.scss deleted file mode 100644 index ce958a9aae77f..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_control_group.scss +++ /dev/null @@ -1,67 +0,0 @@ -.control-group { - display: flex; - flex: 0 0 auto; - flex-direction: row; - flex-wrap: wrap; - align-items: stretch; - padding: $euiSizeXS $euiSize; - - > * { - padding-right: $euiSize; - flex: 0 0 auto; - - &:last-child { - padding-right: 0; - } - } - - // horizontal group of buttons/form elements - .inline-form .input-group { - margin-bottom: 0; - display: flex; - - > * { - border-radius: 0; - } - - > :first-child { - border-bottom-left-radius: $euiBorderRadius; - border-top-left-radius: $euiBorderRadius; - } - - > :last-child { - border-bottom-right-radius: $euiBorderRadius; - border-top-right-radius: $euiBorderRadius; - } - } - - .inline-form { - @include flex-parent(0, 0, auto); - display: flex; - - > .typeahead { - @include flex-parent(); - - > .input-group { - display: flex; - flex: 1 0 auto; - - > * { - float: none; - height: auto; - width: auto; - flex: 0 0 auto; - } - - input[type="text"] { - flex: 1 1 100%; - } - } - } - } - - // the element should take up an even share of available space - > .fill { - flex: 1 1 1%; - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss b/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss deleted file mode 100644 index 0c447031636ac..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_hintbox.scss +++ /dev/null @@ -1,59 +0,0 @@ -.hintbox { - padding: $euiSizeS $euiSizeM; - border-radius: $euiBorderRadius; - margin-bottom: $euiSizeS; - background-color: $euiColorLightShade; - line-height: $euiLineHeight; - - a { - color: $euiLinkColor !important; - - &:hover { - color: darken($euiLinkColor, 10%) !important; - } - } - - pre { - background-color: $euiColorEmptyShade; - } - - ul, ol { - padding-left: $euiSizeL; - } - - // inspired by Bootstrap alerts component - // https://github.com/twbs/bootstrap/blob/063c1b0780ea0240e4adce4c88d57fc23e099475/less/alerts.less#L27-L35 - > * { - margin: 0; - } - - > * + * { - margin-top: $euiSizeS; - } - - // https://github.com/twbs/bootstrap/blob/2aa102bfd40859d15790febed1939e0111a6fb1a/less/tables.less#L88-L106 - .table-bordered { - border: $euiBorderThin; - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - border: $euiBorderThin; - } - } - } - > thead > tr { - > th, - > td { - border-bottom-width: 2px; - } - } - } -} - - .hintbox-label, - .hintbox-label[ng-click] { - cursor: help; - } diff --git a/src/legacy/ui/public/styles/_legacy/components/_index.scss b/src/legacy/ui/public/styles/_legacy/components/_index.scss deleted file mode 100644 index cfae0700bb71e..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_index.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import './ace_overrides'; -@import './control_group'; -@import './hintbox'; -@import './input'; -@import './kui_forms'; -@import './list_group_menu'; -@import './navbar'; -@import './config'; -@import './pagination'; -@import './spinner'; -@import './table'; -@import './truncate'; -@import './ui_select'; diff --git a/src/legacy/ui/public/styles/_legacy/components/_input.scss b/src/legacy/ui/public/styles/_legacy/components/_input.scss deleted file mode 100644 index 13efc9646e820..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_input.scss +++ /dev/null @@ -1,12 +0,0 @@ -i.input-error { - position: absolute; - margin-left: -$euiSizeL; - color: $euiColorDanger; - margin-top: $euiSizeS; - z-index: 5; -} - -select { - color: $euiTextColor; - background-color: $euiColorEmptyShade; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss b/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss deleted file mode 100644 index 2e1b814d647e3..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_kui_forms.scss +++ /dev/null @@ -1,29 +0,0 @@ -.form-control { - @include __legacyInputStyles__bad; -} - -select.form-control { - @include __legacySelectStyles__bad; -} - -.kuiFormSection { - margin-bottom: $euiSize; -} - -.kuiFormLabel { - @include __legacyLabelStyles__bad; - display: block; - margin-bottom: 5px; -} - -.kuiInputNote { - margin: $euiSizeXS 0 $euiSizeS; -} - -.kuiInputNote--danger { - color: $euiColorDanger; -} - -.kuiInputNote--warning { - color: $euiColorWarning; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_navbar.scss b/src/legacy/ui/public/styles/_legacy/components/_navbar.scss deleted file mode 100644 index b06c655789a50..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_navbar.scss +++ /dev/null @@ -1,86 +0,0 @@ -navbar { - @extend .control-group; - - max-height: 340px; - margin-bottom: 0; - padding: 0 $euiSizeS $euiSizeXS; - color: $euiColorDarkShade; - background-color: $euiColorLightShade; - border: none; - z-index: $euiZLevel1; - - > * { - padding-right: $euiSizeS; - } - - .navbar-text { - margin-top: $euiSizeXS; - margin-bottom: $euiSizeXS; - } - - // the "brand" that is displayed, usually on the left of the navbar - > .name { - @include euiTextTruncate; - - align-self: center; - font-size: $euiFontSizeL; - } - - button { - color: $euiColorDarkShade; - background-color: transparent; - - &:hover, - &:focus { - color: $euiColorDarkShade; - background-color: transparent; - } - - &:active, &.active { - color: $euiColorDarkestShade; - background-color: transparent; - box-shadow: none; - - &:focus { - outline: none; - } - } - - &[disabled] { - color: $euiColorMediumShade; - background-color: transparent; - } - } - - .inline-form .input-group { - button { - color: $euiColorEmptyShade; - background-color: $euiColorDarkShade; - border: none; - } - } - - // responsive modifications - - // desktop - @include euiBreakpoint('l', 'xl') { - > .name { - // 500px is sort of arbitrary, not sure how to deal with lots of buttons - max-width: 500px; - } - } - - // tablets/phones - @include euiBreakpoint('xs', 's', 'm') { - > .fill { - flex: 1 1 map-get($euiBreakpoints, 'l'); - } - } - - // phones - @include euiBreakpoint('xs', 's') { - > .name { - max-width: 100%; - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_pagination.scss b/src/legacy/ui/public/styles/_legacy/components/_pagination.scss deleted file mode 100644 index cf68f2ac8253f..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_pagination.scss +++ /dev/null @@ -1,56 +0,0 @@ -paginate { - display: block; - - paginate-controls { - display: flex; - align-items: center; - padding: $euiSizeXS $euiSizeXS $euiSizeS; - text-align: center; - - .pagination-other-pages { - flex: 1 0 auto; - display: flex; - justify-content: center; - } - - .pagination-other-pages-list { - flex: 0 0 auto; - display: flex; - justify-content: center; - padding: 0; - margin: 0; - list-style: none; - - > li { - flex: 0 0 auto; - user-select: none; - - a { - text-decoration: none; - background-color: $euiColorLightestShade; - margin-left: $euiSizeXS / 2; - padding: $euiSizeS $euiSizeM; - } - - a:hover { - text-decoration: underline; - } - - &.active a { - text-decoration: none !important; - font-weight: $euiFontWeightBold; - color: $euiColorDarkShade; - cursor: default; - } - } - } - - .pagination-size { - flex: 0 0 auto; - - input[type=number] { - width: 3em; - } - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_spinner.scss b/src/legacy/ui/public/styles/_legacy/components/_spinner.scss deleted file mode 100644 index 7b3b1bd615ae0..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_spinner.scss +++ /dev/null @@ -1,6 +0,0 @@ -.spinner.ng-hide { - visibility: hidden; - display: block !important; - opacity: 0; - transition-delay: 0.25s; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_table.scss b/src/legacy/ui/public/styles/_legacy/components/_table.scss deleted file mode 100644 index d0ac9d6f79862..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_table.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import '../../../../../../plugins/discover/public/application/mixins'; - -.table { - // Nesting - .table { - background-color: $euiColorEmptyShade; - } -} - -kbn-table, .kbn-table, tbody[kbn-rows] { - @include dscDocSourceStyle; - // sub tables should not have a leading border - .table .table { - margin-bottom: 0; - - tr:first-child > td { - border-top: none; - } - - td.field-name { - font-weight: $euiFontWeightBold; - } - } -} - -table { - th { - i.fa-sort { - color: $euiColorLightShade; - } - - button.fa-sort-asc, - button.fa-sort-down, - i.fa-sort-asc, - i.fa-sort-down { - color: $euiColorPrimary; - } - - button.fa-sort-desc, - button.fa-sort-up, - i.fa-sort-desc, - i.fa-sort-up { - color: $euiColorPrimary; - } - } -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_truncate.scss b/src/legacy/ui/public/styles/_legacy/components/_truncate.scss deleted file mode 100644 index 30ba5fea2a4ea..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_truncate.scss +++ /dev/null @@ -1,3 +0,0 @@ -.truncate-by-height { - overflow: hidden; -} diff --git a/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss b/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss deleted file mode 100644 index 691ec17b5b967..0000000000000 --- a/src/legacy/ui/public/styles/_legacy/components/_ui_select.scss +++ /dev/null @@ -1,357 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.19.5 - 2016-10-24T23:13:59.551Z - * License: MIT - */ - -/* Style when highlighting a search. */ -.ui-select-highlight { - font-weight: bold; -} - -.ui-select-offscreen { - clip: rect(0 0 0 0) !important; - width: 1px !important; - height: 1px !important; - border: 0 !important; - margin: 0 !important; - padding: 0 !important; - overflow: hidden !important; - position: absolute !important; - outline: 0 !important; - left: 0px !important; - top: 0px !important; -} - -.ui-select-choices-row:hover { - background-color: $euiColorLightestShade; -} - -/* Select2 theme */ - -/* Mark invalid Select2 */ -.ng-dirty.ng-invalid > a.select2-choice { - border-color: $euiColorDanger; -} - -.select2-result-single { - padding-left: 0; -} - -.select2-locked > .select2-search-choice-close { - display: none; -} - -.select-locked > .ui-select-match-close { - display: none; -} - -body > .select2-container.open { - z-index: 9999; /* The z-index Select2 applies to the select2-drop */ -} - -/* Handle up direction Select2 */ -.ui-select-container[theme='select2'].direction-up .ui-select-match, -.ui-select-container.select2.direction-up .ui-select-match { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.ui-select-container[theme='select2'].direction-up .ui-select-dropdown, -.ui-select-container.select2.direction-up .ui-select-dropdown { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - border-top-width: 1px; /* FIXME hardcoded value :-/ */ - border-top-style: solid; - - box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); - - margin-top: -4px; /* FIXME hardcoded value :-/ */ -} -.ui-select-container[theme='select2'].direction-up .ui-select-dropdown .select2-search, -.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search { - margin-top: 4px; /* FIXME hardcoded value :-/ */ -} -.ui-select-container[theme='select2'].direction-up.select2-dropdown-open .ui-select-match, -.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match { - border-bottom-color: $euiColorPrimary; -} - -.ui-select-container[theme='select2'] .ui-select-dropdown .ui-select-search-hidden, -.ui-select-container[theme='select2'] .ui-select-dropdown .ui-select-search-hidden input { - opacity: 0; - height: 0; - min-height: 0; - padding: 0; - margin: 0; - border: 0; -} - -/* Bootstrap theme */ - -/* Helper class to show styles when focus */ -.btn-default-focus { - color: $euiTextColor; - background-color: $euiColorEmptyShade; - border-color: $euiColorPrimary; - text-decoration: none; - outline: none; - box-shadow: none; -} - -.ui-select-bootstrap .ui-select-toggle { - @include __legacyInputStyles__bad; - @include __legacySelectStyles__bad; -} - -.ui-select-bootstrap .ui-select-toggle > .caret { - display: none; -} - -/* Fix Bootstrap dropdown position when inside a input-group */ -.input-group > .ui-select-bootstrap.dropdown { - /* Instead of relative */ - position: static; -} - -.input-group > .ui-select-bootstrap > input.ui-select-search.form-control { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up { - border-radius: 4px !important; /* FIXME hardcoded value :-/ */ - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -.ui-select-bootstrap .ui-select-search-hidden { - opacity: 0; - height: 0; - min-height: 0; - padding: 0; - margin: 0; - border: 0; -} - -.ui-select-bootstrap > .ui-select-match > .btn { - @include __legacyInputStyles__bad; - @include __legacySelectStyles__bad; - text-align: left !important; // Instead of center because of .btn - - .ui-select-placeholder { - color: $euiColorMediumShade; - } - - &:focus, - &:active { - background-color: $euiColorEmptyShade; - color: $euiTextColor; - outline: none; - } -} - -.ui-select-bootstrap > .ui-select-match > .caret { - display: none; -} - -/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ -.ui-select-bootstrap > .ui-select-choices, -.ui-select-bootstrap > .ui-select-no-choice { - width: 100%; - height: auto; - max-height: $euiSize * 14; - overflow-x: hidden; -} - -body > .ui-select-bootstrap.open { - z-index: $euiZContentMenu; -} - -.ui-select-multiple.ui-select-bootstrap { - height: auto; - padding: 3px 5px 2px; - border: $euiBorderThin; - background-color: $euiFormBackgroundColor; - - &.kuiInputError { - border-color: $euiColorDanger; - } -} - -.ui-select-multiple.ui-select-bootstrap input.ui-select-search { - background-color: transparent !important; /* To prevent double background when disabled */ - border: none; - outline: none; - height: 1.666666em; - margin-bottom: 3px; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match .close { - font-size: 1.6em; - line-height: 0.75; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match-item { - outline: 0; - margin: 0 3px 3px 0; -} - -.ui-select-multiple .ui-select-match-item { - position: relative; -} - -.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close { - pointer-events: none; -} - -.ui-select-multiple:hover .ui-select-match-item.dropping-before:before { - content: ''; - position: absolute; - top: 0; - right: 100%; - height: 100%; - margin-right: 2px; - border-left: 1px solid $euiColorPrimary; -} - -.ui-select-multiple:hover .ui-select-match-item.dropping-after:after { - content: ''; - position: absolute; - top: 0; - left: 100%; - height: 100%; - margin-left: 2px; - border-right: 1px solid $euiColorPrimary; -} - -.ui-select-bootstrap .ui-select-choices-row > span { - @include euiFontSizeS; - @include euiTextTruncate; - font-weight: inherit; - cursor: pointer; - display: block; - padding: $euiSizeXS $euiSize; - clear: both; - color: $euiTextColor; - white-space: nowrap; - - &:hover, - &:focus { - text-decoration: none; - color: $euiTextColor; - background-color: $euiFocusBackgroundColor; - } -} - -.ui-select-bootstrap .ui-select-choices-row.active > span { - color: $euiTextColor; - text-decoration: none; - outline: 0; - background-color: $euiFocusBackgroundColor; -} - -.ui-select-bootstrap .ui-select-choices-row.disabled > span, -.ui-select-bootstrap .ui-select-choices-row.active.disabled > span { - color: $euiButtonColorDisabled; - cursor: not-allowed; - background-color: transparent; -} - -/* fix hide/show angular animation */ -.ui-select-match.ng-hide-add, -.ui-select-search.ng-hide-add { - display: none !important; -} - -/* Mark invalid Bootstrap */ -.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { - border-color: $euiColorDanger; -} - -/* Handle up direction Bootstrap */ -.ui-select-container[theme='bootstrap'].direction-up .ui-select-dropdown { - @include euiBottomShadowMedium; -} - -.ui-select-bootstrap .ui-select-match-text { - width: 100%; - padding-right: 1em; -} -.ui-select-bootstrap .ui-select-match-text span { - display: inline-block; - width: 100%; - overflow: hidden; -} -.ui-select-bootstrap .ui-select-toggle > a.btn { - position: absolute; - height: 10px; - right: 10px; - margin-top: -2px; -} - -/* Spinner */ -.ui-select-refreshing { - position: absolute; - right: 0; - padding: 8px 27px; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} - -@-webkit-keyframes ui-select-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes ui-select-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -.ui-select-spin { - -webkit-animation: ui-select-spin 2s infinite linear; - animation: ui-select-spin 2s infinite linear; -} - -.ui-select-refreshing.ng-animate { - -webkit-animation: none 0s; -} - -// Other Custom - -/** - * 1. Fix appearance of ui-select in the Filtering UI. - */ -.btn-default .ui-select-placeholder { - color: $euiColorMediumShade; /* 1 */ -} - -.uiSelectMatch--ellipsis { - @include euiTextTruncate; -} - -.ui-select-choices-group-label { - @include euiTitle('xxxs'); - @include euiTextTruncate; - padding: $euiSizeXS; -} diff --git a/src/legacy/ui/public/styles/_mixins.scss b/src/legacy/ui/public/styles/_mixins.scss index c0dc456000dcc..e335ef88a01b5 100644 --- a/src/legacy/ui/public/styles/_mixins.scss +++ b/src/legacy/ui/public/styles/_mixins.scss @@ -105,12 +105,10 @@ @keyframes kibanaFullScreenGraphics_FadeIn { from { opacity: 0; - transform: translateY(200px), scale(.75); } to { opacity: 1; - transform: translateY(0), scale(1); } } } diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index 1aaa0a24357ed..69df2a75b8d75 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -103,3 +103,15 @@ discover-app { right: $euiSizeM; top: $euiSizeXS; } + +[fixed-scroll] { + overflow-x: auto; + padding-bottom: 0; + + + .fixed-scroll-scroller { + position: fixed; + bottom: 0; + overflow-x: auto; + overflow-y: hidden; + } +} diff --git a/src/plugins/discover/public/application/_hacks.scss b/src/plugins/discover/public/application/_hacks.scss deleted file mode 100644 index 9bbe9cd14fd91..0000000000000 --- a/src/plugins/discover/public/application/_hacks.scss +++ /dev/null @@ -1,4 +0,0 @@ -// SASSTODO: the classname is dynamically generated with ng-class -.tab-discover { - overflow: hidden; -} diff --git a/src/plugins/discover/public/application/_mixins.scss b/src/plugins/discover/public/application/_mixins.scss deleted file mode 100644 index 100f81ae92bf0..0000000000000 --- a/src/plugins/discover/public/application/_mixins.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** -* Style ES document _source in table view
key:
value
-* Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted -* row in the Context Log. -*/ -@mixin dscDocSourceStyle { - dl.source { - margin-bottom: 0; - line-height:2em; - word-break: break-word; - - dt, dd { - display: inline; - } - - dt { - background-color: transparentize(shade($euiColorPrimary, 20%), .9); - color: $euiTextColor; - padding: ($euiSizeXS / 2) $euiSizeXS; - margin-right: $euiSizeXS; - word-break: normal; - border-radius: $euiBorderRadius; - } - } -} - - diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss index 3e30214acd2a9..7d05171622e7b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss @@ -27,7 +27,6 @@ doc-table { } .kbnDocTable { - @include dscDocSourceStyle; font-size: $euiFontSizeXS; th { @@ -40,6 +39,35 @@ doc-table { } } +.kbn-table, +.kbnDocTable { + /** + * Style ES document _source in table view
key:
value
+ * Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted + * row in the Context Log. + */ + + dl.source { + margin-bottom: 0; + line-height: 2em; + word-break: break-word; + + dt, + dd { + display: inline; + } + + dt { + background-color: transparentize(shade($euiColorPrimary, 20%), 0.9); + color: $euiTextColor; + padding: ($euiSizeXS / 2) $euiSizeXS; + margin-right: $euiSizeXS; + word-break: normal; + border-radius: $euiBorderRadius; + } + } +} + .kbnDocTable__row { td { position: relative; @@ -80,3 +108,50 @@ doc-table { text-align: center; } +.truncate-by-height { + overflow: hidden; +} + +.table { + // Nesting + .table { + background-color: $euiColorEmptyShade; + } +} + +.kbn-table { + // sub tables should not have a leading border + .table .table { + margin-bottom: 0; + + tr:first-child > td { + border-top: none; + } + + td.field-name { + font-weight: $euiFontWeightBold; + } + } +} + +table { + th { + i.fa-sort { + color: $euiColorLightShade; + } + + button.fa-sort-asc, + button.fa-sort-down, + i.fa-sort-asc, + i.fa-sort-down { + color: $euiColorPrimary; + } + + button.fa-sort-desc, + button.fa-sort-up, + i.fa-sort-desc, + i.fa-sort-up { + color: $euiColorPrimary; + } + } +} diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover/public/application/angular/doc_table/index.scss index 4e6cb83c5fe5a..3663d807851c4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.scss +++ b/src/plugins/discover/public/application/angular/doc_table/index.scss @@ -1,4 +1,2 @@ -@import '../../mixins'; - @import 'doc_table'; @import 'components/index'; diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss index aaec7ab387e96..5aa353828274c 100644 --- a/src/plugins/discover/public/application/index.scss +++ b/src/plugins/discover/public/application/index.scss @@ -1,13 +1,2 @@ -// Discover plugin styles -@import 'mixins'; -@import 'discover'; -@import 'hacks'; - -// Prefix all styles with "dsc" to avoid conflicts. -// Examples -// dscTable -// dscTable__footer -// monChart__legend--small -// monChart__legend-isLoading - @import 'angular/index'; +@import 'discover'; diff --git a/src/plugins/kibana_legacy/public/paginate/_paginate.scss b/src/plugins/kibana_legacy/public/paginate/_paginate.scss new file mode 100644 index 0000000000000..e9c1acaf9ee0d --- /dev/null +++ b/src/plugins/kibana_legacy/public/paginate/_paginate.scss @@ -0,0 +1,57 @@ +paginate { + display: block; + + paginate-controls { + display: flex; + align-items: center; + padding: $euiSizeXS $euiSizeXS $euiSizeS; + text-align: center; + + .pagination-other-pages { + flex: 1 0 auto; + display: flex; + justify-content: center; + } + + .pagination-other-pages-list { + flex: 0 0 auto; + display: flex; + justify-content: center; + padding: 0; + margin: 0; + list-style: none; + + > li { + flex: 0 0 auto; + user-select: none; + + a { + text-decoration: none; + background-color: $euiColorLightestShade; + margin-left: $euiSizeXS / 2; + padding: $euiSizeS $euiSizeM; + } + + a:hover { + text-decoration: underline; + } + + &.active a { + text-decoration: none !important; + font-weight: $euiFontWeightBold; + color: $euiColorDarkShade; + cursor: default; + } + } + } + + .pagination-size { + flex: 0 0 auto; + + input[type=number] { + width: 3em; + } + } + } +} + diff --git a/src/plugins/kibana_legacy/public/paginate/paginate.js b/src/plugins/kibana_legacy/public/paginate/paginate.js index ea93a969d08c7..f424c33ba7b02 100644 --- a/src/plugins/kibana_legacy/public/paginate/paginate.js +++ b/src/plugins/kibana_legacy/public/paginate/paginate.js @@ -19,6 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import './_paginate.scss'; import paginateControlsTemplate from './paginate_controls.html'; export function PaginateDirectiveProvider($parse, $compile) { diff --git a/src/plugins/timelion/public/_base.scss b/src/plugins/timelion/public/_base.scss new file mode 100644 index 0000000000000..616ac9b3486e7 --- /dev/null +++ b/src/plugins/timelion/public/_base.scss @@ -0,0 +1,19 @@ +// Angular form states +.ng-invalid { + &.ng-dirty, + &.ng-touched { + border-color: $euiColorDanger; + } +} + +input[type='radio'], +input[type='checkbox'], +.radio, +.checkbox { + &[disabled], + fieldset[disabled] & { + cursor: default; + opacity: .8; + } +} + diff --git a/src/legacy/ui/public/styles/_legacy/_mixins.scss b/src/plugins/timelion/public/directives/_form.scss similarity index 58% rename from src/legacy/ui/public/styles/_legacy/_mixins.scss rename to src/plugins/timelion/public/directives/_form.scss index 2834f60555070..3fcf70700a864 100644 --- a/src/legacy/ui/public/styles/_legacy/_mixins.scss +++ b/src/plugins/timelion/public/directives/_form.scss @@ -1,23 +1,19 @@ -// These mixins are temporary helpers to consolidate styles of elements that -// are not yet converted to use EUI. - -// DO NOT CONTINUE TO USE THESE MIXINS - -@mixin __legacyInputStyles__bad { - &:not([type='range']) { - appearance: none; - } +.form-control { + @include euiFontSizeS; display: block; width: 100%; height: $euiFormControlCompressedHeight; padding: $euiSizeXS $euiSizeM; - @include euiFontSizeS; border: $euiBorderThin; background-color: $euiFormBackgroundColor; color: $euiTextColor; border-radius: $euiBorderRadius; cursor: pointer; + &:not([type='range']) { + appearance: none; + } + &:focus { border-color: $euiColorPrimary; outline: none; @@ -25,30 +21,16 @@ } } -@mixin __legacySelectStyles__bad { +// sass-lint:disable-block no-qualifying-elements +select.form-control { // Makes the select arrow similar to EUI's arrowDown icon - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M13.0688508,5.15725038 L8.38423975,9.76827428 C8.17054415,9.97861308 7.82999214,9.97914095 7.61576025,9.76827428 L2.93114915,5.15725038 C2.7181359,4.94758321 2.37277319,4.94758321 2.15975994,5.15725038 C1.94674669,5.36691756 1.94674669,5.70685522 2.15975994,5.9165224 L6.84437104,10.5275463 C7.48517424,11.1582836 8.51644979,11.1566851 9.15562896,10.5275463 L13.8402401,5.9165224 C14.0532533,5.70685522 14.0532533,5.36691756 13.8402401,5.15725038 C13.6272268,4.94758321 13.2818641,4.94758321 13.0688508,5.15725038 Z'/%3E%3C/svg%3E"); + background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"%3E%3Cpath fill="#{hexToRGB($euiTextColor)}" d="M13.0688508,5.15725038 L8.38423975,9.76827428 C8.17054415,9.97861308 7.82999214,9.97914095 7.61576025,9.76827428 L2.93114915,5.15725038 C2.7181359,4.94758321 2.37277319,4.94758321 2.15975994,5.15725038 C1.94674669,5.36691756 1.94674669,5.70685522 2.15975994,5.9165224 L6.84437104,10.5275463 C7.48517424,11.1582836 8.51644979,11.1566851 9.15562896,10.5275463 L13.8402401,5.9165224 C14.0532533,5.70685522 14.0532533,5.36691756 13.8402401,5.15725038 C13.6272268,4.94758321 13.2818641,4.94758321 13.0688508,5.15725038 Z"/%3E%3C/svg%3E'); background-size: $euiSize; background-repeat: no-repeat; background-position: calc(100% - #{$euiSizeS}); padding-right: $euiSizeXL; - - &::-ms-expand { - display: none; - } - - &:focus::-ms-value { - color: $euiTextColor; - background: transparent; - } } -@mixin __legacyLabelStyles__bad { - font-size: $euiFontSizeXS; - font-weight: $euiFontWeightSemiBold; - color: $euiTextColor; - - &[for] { - cursor: pointer; - } +.fullWidth { + width: 100%; } diff --git a/src/plugins/timelion/public/directives/_index.scss b/src/plugins/timelion/public/directives/_index.scss index cd46a1a0a369e..a407c1dfabdeb 100644 --- a/src/plugins/timelion/public/directives/_index.scss +++ b/src/plugins/timelion/public/directives/_index.scss @@ -3,3 +3,6 @@ @import './timelion_expression_suggestions/index'; @import './timelion_help/index'; @import './timelion_interval/index'; +@import './saved_object_finder'; +@import './form'; + diff --git a/src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss similarity index 85% rename from src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss rename to src/plugins/timelion/public/directives/_saved_object_finder.scss index 3bac75cb19d9d..b97dace5e9e00 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_list_group_menu.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -1,7 +1,7 @@ .list-group-menu { &.select-mode a { - outline: none; - color: tintOrShade($euiColorPrimary, 10%, 10%); + outline: none; + color: tintOrShade($euiColorPrimary, 10%, 10%); } .list-group-menu-item { @@ -12,9 +12,11 @@ font-weight: bold; background-color: $euiColorLightShade; } + &:hover { background-color: tintOrShade($euiColorPrimary, 90%, 90%); } + li { list-style: none; color: tintOrShade($euiColorPrimary, 10%, 10%); diff --git a/src/plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss index cf2a7859a505d..6bf7133287c51 100644 --- a/src/plugins/timelion/public/index.scss +++ b/src/plugins/timelion/public/index.scss @@ -8,4 +8,5 @@ // timChart__legend-isLoading @import './app'; +@import './base'; @import './directives/index'; diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss index 0fffb21eab0fb..4bbc4eb034f8d 100644 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss @@ -36,3 +36,7 @@ kbn-agg-table-group { padding: 0; } } + +.small { + font-size: 0.9em !important; +} diff --git a/x-pack/plugins/ml/public/application/_hacks.scss b/x-pack/plugins/ml/public/application/_hacks.scss index 39740360d8a84..63fec4e74b796 100644 --- a/x-pack/plugins/ml/public/application/_hacks.scss +++ b/x-pack/plugins/ml/public/application/_hacks.scss @@ -18,42 +18,6 @@ cursor: not-allowed; } -// ML bootstrap-select hacks that sit on top of Kibana hacks that often fight with KUI -// Should go away when EUI is fully adopted -.ui-select-match { - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus { - background-color: $euiColorLightShade; - border-color: $euiColorLightShade; - opacity: 1; - - .ui-select-placeholder { - color: $euiColorDarkShade; - } - } - - .btn { - border: 1px solid $euiColorLightShade; - background-color: $euiColorEmptyShade; - color: $euiColorDarkestShade; - } - } - -.ui-select-container input[type="search"]::placeholder { - color: $euiColorDarkShade; -} - -.ui-select-container input[type="search"]:focus { - box-shadow: none; -} - -.ui-select-multiple.ui-select-bootstrap input.ui-select-search { - font-size: $euiFontSizeS; - padding: 5px 10px; // Matches current padding hacks from other parts of Kibana -} - - // SASSTODO: Remove all the floats .clear, .clearfix { clear: both; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css index 0ea9f3079de82..12ac5b27c7a4a 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css @@ -37,7 +37,6 @@ filter-bar, discover-app .dscTimechart, discover-app .dscSidebar__container, discover-app .kbnCollapsibleSidebar__collapseButton, -discover-app navbar[name=discover-search], discover-app .discover-table-footer { display: none; } diff --git a/x-pack/plugins/reporting/server/lib/layouts/print.css b/x-pack/plugins/reporting/server/lib/layouts/print.css index 4f1e3f4e5abd0..9b07e3c923138 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print.css +++ b/x-pack/plugins/reporting/server/lib/layouts/print.css @@ -36,7 +36,6 @@ filter-bar, discover-app .dscTimechart, discover-app .dscSidebar__container, discover-app .kbnCollapsibleSidebar__collapseButton, -discover-app navbar[name="discover-search"], discover-app .discover-table-footer { display: none; } From b29e8ee9c746dd2eff95d950ae4dbca564b11441 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 20 Jul 2020 10:55:44 +0200 Subject: [PATCH 74/76] migrate retryCallCluster for new ES client (#71412) * adapt retryCallCluster for new ES client * review comments * retry on 408 ResponseError * use error name instead of instanceof base check * use error name instead of instanceof base check bis * use mockImplementationOnce chaining Co-authored-by: restrry --- src/core/server/elasticsearch/client/index.ts | 1 + .../client/retry_call_cluster.test.ts | 283 ++++++++++++++++++ .../client/retry_call_cluster.ts | 103 +++++++ .../legacy/retry_call_cluster.ts | 4 +- 4 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.test.ts create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.ts diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index 18e84482024ca..b8125de2ee498 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -22,3 +22,4 @@ export { IScopedClusterClient, ScopedClusterClient } from './scoped_cluster_clie export { ElasticsearchClientConfig } from './client_config'; export { IClusterClient, ICustomClusterClient, ClusterClient } from './cluster_client'; export { configureClient } from './configure_client'; +export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts new file mode 100644 index 0000000000000..a7177c0b29047 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import { elasticsearchClientMock } from './mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; + +const dummyBody = { foo: 'bar' }; +const createErrorReturn = (err: any) => elasticsearchClientMock.createClientError(err); + +describe('retryCallCluster', () => { + let client: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + }); + + it('returns response from ES API call in case of success', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + client.asyncSearch.get.mockReturnValue(successReturn); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + client.asyncSearch.get + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => successReturn); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('rejects when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); +}); + +describe('migrationRetryCallCluster', () => { + let client: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + const mockClientPingWithErrorBeforeSuccess = (error: any) => { + client.ping + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + }; + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.NoLivingConnectionsError('no living connections', {} as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `ConnectionError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.ConnectionError('connection error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `TimeoutError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.TimeoutError('timeout error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 503 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 503, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects 401 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 401, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 403 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 403, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 408 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 408, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 410 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 410, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `snapshot_in_progress_exception` `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('logs only once for each unique error message', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await migrationRetryCallCluster(() => client.ping(), logger, 1); + + expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unable to connect to Elasticsearch. Error: Response Error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: connection error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: snapshot_in_progress_exception", + ], + ] + `); + }); + + it('rejects when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 418, + body: { + error: { + type: `I'm a teapot`, + }, + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[ResponseError: I'm a teapot]`); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); +}); diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts new file mode 100644 index 0000000000000..1ad039e512215 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -0,0 +1,103 @@ +/* + * 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 { defer, throwError, iif, timer } from 'rxjs'; +import { concatMap, retryWhen } from 'rxjs/operators'; +import { Logger } from '../../logging'; + +const retryResponseStatuses = [ + 503, // ServiceUnavailable + 401, // AuthorizationException + 403, // AuthenticationException + 408, // RequestTimeout + 410, // Gone +]; + +/** + * Retries the provided Elasticsearch API call when a `NoLivingConnectionsError` error is + * encountered. The API call will be retried once a second, indefinitely, until + * a successful response or a different error is received. + * + * @example + * ```ts + * const response = await retryCallCluster(() => client.ping()); + * ``` + * + * @internal + */ +export const retryCallCluster = >(apiCaller: () => T): T => { + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error) => + iif(() => error.name === 'NoLivingConnectionsError', timer(1000), throwError(error)) + ) + ) + ) + ) + .toPromise() as T; +}; + +/** + * Retries the provided Elasticsearch API call when an error such as + * `AuthenticationException` `NoConnections`, `ConnectionFault`, + * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will + * be retried once a second, indefinitely, until a successful response or a + * different error is received. + * + * @example + * ```ts + * const response = await migrationRetryCallCluster(() => client.ping(), logger); + * ``` + * + * @internal + */ +export const migrationRetryCallCluster = >( + apiCaller: () => T, + log: Logger, + delay: number = 2500 +): T => { + const previousErrors: string[] = []; + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error) => { + if (!previousErrors.includes(error.message)) { + log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); + previousErrors.push(error.message); + } + return iif( + () => + error.name === 'NoLivingConnectionsError' || + error.name === 'ConnectionError' || + error.name === 'TimeoutError' || + (error.name === 'ResponseError' && + retryResponseStatuses.includes(error.statusCode)) || + error?.body?.error?.type === 'snapshot_in_progress_exception', + timer(delay), + throwError(error) + ); + }) + ) + ) + ) + .toPromise() as T; +}; diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts index 475a76d406017..1b05cb2bf13cd 100644 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts @@ -53,7 +53,7 @@ export function migrationsRetryCallCluster( .pipe( retryWhen((error$) => error$.pipe( - concatMap((error, i) => { + concatMap((error) => { if (!previousErrors.includes(error.message)) { log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); previousErrors.push(error.message); @@ -100,7 +100,7 @@ export function retryCallCluster(apiCaller: LegacyAPICaller) { .pipe( retryWhen((errors) => errors.pipe( - concatMap((error, i) => + concatMap((error) => iif( () => error instanceof legacyElasticsearch.errors.NoConnections, timer(1000), From ade93f0780c3a7fabb836c00021b7cc32a8a6b4d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 20 Jul 2020 11:40:39 +0200 Subject: [PATCH 75/76] Disable indexing of unnecessary Saved Object fields (#70409) * Disable indexing of unnecessary SO fields * Add doc_values * Add no doc_values to discover saved object Co-authored-by: Elastic Machine --- .../server/saved_objects/dashboard.ts | 24 ++++++++++--------- .../discover/server/saved_objects/search.ts | 8 +++---- .../server/saved_objects/visualization.ts | 10 ++++---- x-pack/plugins/lens/server/saved_objects.ts | 1 + 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index 14d2c822a421e..850b2470dd475 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -44,21 +44,23 @@ export const dashboardSavedObjectType: SavedObjectsType = { mappings: { properties: { description: { type: 'text' }, - hits: { type: 'integer' }, - kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, - optionsJSON: { type: 'text' }, - panelsJSON: { type: 'text' }, + hits: { type: 'integer', index: false, doc_values: false }, + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + }, + optionsJSON: { type: 'text', index: false, doc_values: false }, + panelsJSON: { type: 'text', index: false, doc_values: false }, refreshInterval: { properties: { - display: { type: 'keyword' }, - pause: { type: 'boolean' }, - section: { type: 'integer' }, - value: { type: 'integer' }, + display: { type: 'keyword', index: false, doc_values: false }, + pause: { type: 'boolean', index: false, doc_values: false }, + section: { type: 'integer', index: false, doc_values: false }, + value: { type: 'integer', index: false, doc_values: false }, }, }, - timeFrom: { type: 'keyword' }, - timeRestore: { type: 'boolean' }, - timeTo: { type: 'keyword' }, + timeFrom: { type: 'keyword', index: false, doc_values: false }, + timeRestore: { type: 'boolean', index: false, doc_values: false }, + timeTo: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, version: { type: 'integer' }, }, diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 2348d89c4f4dd..c13550e543ab6 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -43,15 +43,15 @@ export const searchSavedObjectType: SavedObjectsType = { }, mappings: { properties: { - columns: { type: 'keyword', index: false }, + columns: { type: 'keyword', index: false, doc_values: false }, description: { type: 'text' }, - hits: { type: 'integer', index: false }, + hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { - searchSourceJSON: { type: 'text', index: false }, + searchSourceJSON: { type: 'text', index: false, doc_values: false }, }, }, - sort: { type: 'keyword', index: false }, + sort: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, version: { type: 'integer' }, }, diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index c4756de0a8386..ad7618a8640ba 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -44,12 +44,14 @@ export const visualizationSavedObjectType: SavedObjectsType = { mappings: { properties: { description: { type: 'text' }, - kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, - savedSearchRefName: { type: 'keyword' }, + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + }, + savedSearchRefName: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, - uiStateJSON: { type: 'text' }, + uiStateJSON: { type: 'text', index: false, doc_values: false }, version: { type: 'integer' }, - visState: { type: 'text' }, + visState: { type: 'text', index: false, doc_values: false }, }, }, migrations: visualizationSavedObjectTypeMigrations, diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index a16cc3dab7967..82ee490bb22af 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -40,6 +40,7 @@ export function setupSavedObjects(core: CoreSetup) { }, expression: { index: false, + doc_values: false, type: 'keyword', }, }, From 3442451aacb0d5e469019c470d5cf494276eabae Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 20 Jul 2020 13:04:42 +0300 Subject: [PATCH 76/76] [Security Solution][Case] IBM Resilient content fixes (#72271) --- .../public/common/lib/connectors/resilient/index.tsx | 2 +- .../common/lib/connectors/resilient/translations.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx index d3daf195582a8..ba4879e87a1f6 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx @@ -30,7 +30,7 @@ const validateConnector = (action: ResilientActionConnector): ValidationResult = }; if (!action.config.orgId) { - errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_LABEL]; + errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_REQUIRED]; } if (!action.secrets.apiKeyId) { diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts index f8aec2eea3d4b..2ff97ad354095 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const RESILIENT_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new issue in resilient', + defaultMessage: 'Push or update Security case data to a new issue in Resilient', } ); @@ -25,28 +25,28 @@ export const RESILIENT_TITLE = i18n.translate( export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.orgId', { - defaultMessage: 'Organization Id', + defaultMessage: 'Organization ID', } ); export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField', { - defaultMessage: 'Organization Id', + defaultMessage: 'Organization ID is required', } ); export const RESILIENT_API_KEY_ID_LABEL = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.apiKeyId', { - defaultMessage: 'API key id', + defaultMessage: 'API key ID', } ); export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate( 'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField', { - defaultMessage: 'API key id is required', + defaultMessage: 'API key ID is required', } );