diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index f306f2b8f763f..35f1160ee834d 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -115,7 +115,7 @@ KQL supports `>`, `>=`, `<`, and `<=`. For example: [source,yaml] ------------------- -account_number:>=100 and items_sold:<=200 +account_number >= 100 and items_sold <= 200 ------------------- [discrete] diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 917821ad09e2f..f48dbeab9d61a 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -76,6 +76,9 @@ a|`monitoring.cluster_alerts.` health checks. By default, it matches the <> setting, which has a default value of `30000`. +| `monitoring.ui.elasticsearch.ssl` + | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster. + |=== [float] diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc index 5e928fd731bb4..bdff7355d7467 100644 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -57,8 +57,8 @@ TIP: If you don’t see data for a panel, try changing the time range. . Set a search and filter. + [%hardbreaks] -Search: `extension.keyword:( “gz” or “css” or “deb”)` -Filter: `geo.src : CN` +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` *Create the drilldown* @@ -94,4 +94,3 @@ image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to an + You are navigated to your destination dashboard. Verify that the search query, filters, and time range are carried over. - diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 1ac5c385f8ed5..300497126c3e5 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -31,6 +31,33 @@ default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running checks on a schedule time of 1 minute with a re-notify internal of 1 day. +[discrete] +[[kibana-alerts-disk-usage-threshold]] +== Disk usage threshold + +This alert is triggered when a node is nearly at disk capacity. By +default, the trigger condition is set at 80% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-jvm-memory-threshold]] +== JVM memory threshold + +This alert is triggered when a node runs a consistently high JVM memory usage. By +default, the trigger condition is set at 85% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-missing-monitoring-data]] +== Missing monitoring data + +This alert is triggered when any stack products nodes or instances stop sending +monitoring data. By default, the trigger condition is set to missing for 15 minutes +looking back 1 day. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 6 hours. + NOTE: Some action types are subscription features, while others are free. For a comparison of the Elastic subscription levels, see the alerting section of the {subscriptions}[Subscriptions page]. diff --git a/package.json b/package.json index b965f50cd468c..84e9c0e2762eb 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ }, "dependencies": { "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@elastic/eui": "29.5.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", @@ -124,6 +124,7 @@ "@elastic/safer-lodash-set": "0.0.0", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", + "@kbn/ace": "1.0.0", "@kbn/analytics": "1.0.0", "@kbn/apm-config-loader": "1.0.0", "@kbn/config": "1.0.0", @@ -131,11 +132,11 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", + "@kbn/monaco": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-framework": "1.0.0", - "@kbn/ace": "1.0.0", - "@kbn/monaco": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/pdfmake": "^0.1.15", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 6ed3ae2eb2fb7..0f9b917e7f05a 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -12,7 +12,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@kbn/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index a2c4e1e2134e7..1f86122d7e129 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -4,6 +4,9 @@ "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 557f38ec740fc..9684201c72384 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -12,6 +12,9 @@ }, "author": "", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "bugs": { "url": "https://github.com/jbudz/spec-to-console/issues" }, diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index dc2647b7b5757..c3bc65059d4a6 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,12 +20,7 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: [ - '@kbn/storybook/preset', - '@storybook/addon-a11y', - '@storybook/addon-knobs', - '@storybook/addon-essentials', - ], + addons: ['@kbn/storybook/preset', '@storybook/addon-a11y', '@storybook/addon-essentials'], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 1390c44d84a07..a95215a0044f1 100644 --- a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -24,13 +24,13 @@ import { readFileSync } from 'fs'; import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { REPO_ROOT } from '@kbn/utils'; const MINUTE = 1000 * 60; const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); +const XML_PATH = getUniqueJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); afterAll(async () => { await del(TARGET_DIR); diff --git a/packages/kbn-test/src/jest/junit_reporter.ts b/packages/kbn-test/src/jest/junit_reporter.ts index 0712584122e05..b6e964c22adfc 100644 --- a/packages/kbn-test/src/jest/junit_reporter.ts +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -27,7 +27,7 @@ import type { Config } from '@jest/types'; import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; import { escapeCdata } from '../mocha/xml'; -import { makeJunitReportPath } from './report_path'; +import { getUniqueJunitReportPath } from './report_path'; interface ReporterOptions { reportName?: string; @@ -115,7 +115,7 @@ export default class JestJUnitReporter extends BaseReporter { }); }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = root.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-test/src/jest/report_path.ts b/packages/kbn-test/src/jest/report_path.ts index fe122c349c193..c9cf3ce454e6a 100644 --- a/packages/kbn-test/src/jest/report_path.ts +++ b/packages/kbn-test/src/jest/report_path.ts @@ -17,14 +17,24 @@ * under the License. */ -import { resolve } from 'path'; +import Fs from 'fs'; +import Path from 'path'; + import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; -export function makeJunitReportPath(rootDirectory: string, reportName: string) { - return resolve( +export function getUniqueJunitReportPath( + rootDirectory: string, + reportName: string, + counter?: number +): string { + const path = Path.resolve( rootDirectory, 'target/junit', process.env.JOB || '.', - `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}.xml` + `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}${counter ? `-${counter}` : ''}.xml` ); + + return Fs.existsSync(path) + ? getUniqueJunitReportPath(rootDirectory, reportName, (counter ?? 0) + 1) + : path; } diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 00a11432dd9e8..dc7d161eca5a3 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -25,13 +25,14 @@ import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; import expect from '@kbn/expect'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { setupJUnitReportGeneration } from '../junit_report_generation'; const PROJECT_DIR = resolve(__dirname, 'fixtures/project'); const DURATION_REGEX = /^\d+\.\d{3}$/; const ISO_DATE_SEC_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/; +const XML_PATH = getUniqueJunitReportPath(PROJECT_DIR, 'test'); describe('dev/mocha/junit report generation', () => { afterEach(() => { @@ -50,9 +51,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise((resolve) => mocha.run(resolve)); - const report = await fcb((cb) => - parseString(readFileSync(makeJunitReportPath(PROJECT_DIR, 'test')), cb) - ); + const report = await fcb((cb) => parseString(readFileSync(XML_PATH), cb)); // test case results are wrapped in expect(report).to.eql({ diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 7e39c32ee4db8..9ac9bd18548f4 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -22,7 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; import xmlBuilder from 'xmlbuilder'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../'; @@ -140,7 +140,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { } }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = builder.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index fba3dcee0a7cb..bd30efcb1c6d3 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -53,9 +53,15 @@ RUN for iter in {1..10}; do \ (exit $exit_code) # Add an init process, check the checksum to make sure it's a match -RUN curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 -RUN echo "37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 /usr/local/bin/dumb-init" | sha256sum -c - -RUN chmod +x /usr/local/bin/dumb-init +RUN set -e ; \ + TINI_VERSION='v0.19.0' ; \ + TINI_BIN='tini-amd64' ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}" ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}.sha256sum" ; \ + sha256sum -c "${TINI_BIN}.sha256sum" ; \ + rm "${TINI_BIN}.sha256sum" ; \ + mv "${TINI_BIN}" /bin/tini ; \ + chmod +x /bin/tini RUN mkdir /usr/share/fonts/local RUN curl -L -o /usr/share/fonts/local/NotoSansCJK-Regular.ttc https://github.com/googlefonts/noto-cjk/raw/NotoSansV2.001/NotoSansCJK-Regular.ttc @@ -125,6 +131,6 @@ RUN mkdir /licenses && \ USER kibana -ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] +ENTRYPOINT ["/bin/tini", "--"] CMD ["/usr/local/bin/kibana-docker"] diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 9fd43be8dc5b3..9085ae07bbe3e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -44,7 +44,6 @@ function create(id: string) { return new IndexPattern({ spec: { id, type, version, timeFieldName, fields, title }, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -214,7 +213,6 @@ describe('IndexPattern', () => { const spec = indexPattern.toSpec(); const restoredPattern = new IndexPattern({ spec, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index d38df68e9f428..a0f27078543a9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,7 +18,6 @@ */ import _, { each, reject } from 'lodash'; -import { SavedObjectsClientCommon } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -31,10 +30,9 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; interface IndexPatternDeps { spec?: IndexPatternSpec; - savedObjectsClient: SavedObjectsClientCommon; fieldFormats: FieldFormatsStartCommon; - shortDotsEnable: boolean; - metaFields: string[]; + shortDotsEnable?: boolean; + metaFields?: string[]; } interface SavedObjectBody { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index bfd0dc9d946c2..fd3d7a1d138fd 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -435,7 +435,6 @@ export class IndexPatternsService { const indexPattern = new IndexPattern({ spec, - savedObjectsClient: this.savedObjectsClient, fieldFormats: this.fieldFormats, shortDotsEnable, metaFields, diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap new file mode 100644 index 0000000000000..d5ddaa31b8ac3 --- /dev/null +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tabifyDocs converts fields by default 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs converts source if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + ], + "rows": Array [ + Object { + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs skips nested fields if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested", + "meta": Object { + "field": "nested", + "index": "test-index", + "params": undefined, + "type": "object", + }, + "name": "nested", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs works without provided index pattern 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 90ac3f2fb730b..9e6657f5e8d83 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -17,6 +17,26 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; +import { SearchSource } from '../search_source'; +import { tabifyAggResponse } from './tabify'; +import { tabifyDocs, TabifyDocsOptions } from './tabify_docs'; +import { TabbedResponseWriterOptions } from './types'; + +export const tabify = ( + searchSource: SearchSource, + esResponse: SearchResponse, + opts: Partial | TabifyDocsOptions +) => { + return !esResponse.aggregations + ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions) + : tabifyAggResponse( + searchSource.getField('aggs'), + esResponse, + opts as Partial + ); +}; + export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts new file mode 100644 index 0000000000000..a1218928561c6 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.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 { tabifyDocs } from './tabify_docs'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { SearchResponse } from 'elasticsearch'; + +describe('tabifyDocs', () => { + const fieldFormats = { + getInstance: (id: string) => ({ toJSON: () => ({ id }) }), + getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), + }; + + const index = new IndexPattern({ + spec: { + id: 'test-index', + fields: { + sourceTest: { name: 'sourceTest', type: 'number', searchable: true, aggregatable: true }, + fieldTest: { name: 'fieldTest', type: 'number', searchable: true, aggregatable: true }, + 'nested.field': { + name: 'nested.field', + type: 'number', + searchable: true, + aggregatable: true, + }, + }, + }, + fieldFormats: fieldFormats as any, + }); + + const response = { + hits: { + hits: [ + { + _source: { sourceTest: 123 }, + fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + }, + ], + }, + } as SearchResponse; + + it('converts fields by default', () => { + const table = tabifyDocs(response, index); + expect(table).toMatchSnapshot(); + }); + + it('converts source if option is set', () => { + const table = tabifyDocs(response, index, { source: true }); + expect(table).toMatchSnapshot(); + }); + + it('skips nested fields if option is set', () => { + const table = tabifyDocs(response, index, { shallow: true }); + expect(table).toMatchSnapshot(); + }); + + it('works without provided index pattern', () => { + const table = tabifyDocs(response); + expect(table).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts new file mode 100644 index 0000000000000..78ebee9e65717 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -0,0 +1,113 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { isPlainObject } from 'lodash'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; + +export function flattenHit( + hit: Record, + indexPattern?: IndexPattern, + shallow: boolean = false +) { + const flat = {} as Record; + + function flatten(obj: Record, keyPrefix: string = '') { + for (const [k, val] of Object.entries(obj)) { + const key = keyPrefix + k; + + const field = indexPattern?.fields.getByName(key); + + if (!shallow) { + const isNestedField = field?.type === 'nested'; + if (Array.isArray(val) && !isNestedField) { + val.forEach((v) => isPlainObject(v) && flatten(v, key + '.')); + continue; + } + } else if (flat[key] !== undefined) { + continue; + } + + const hasValidMapping = field?.type !== 'conflict'; + const isValue = !isPlainObject(val); + + if (hasValidMapping || isValue) { + if (!flat[key]) { + flat[key] = val; + } else if (Array.isArray(flat[key])) { + flat[key].push(val); + } else { + flat[key] = [flat[key], val]; + } + continue; + } + + flatten(val, key + '.'); + } + } + + flatten(hit); + return flat; +} + +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; +} + +export const tabifyDocs = ( + esResponse: SearchResponse, + index?: IndexPattern, + params: TabifyDocsOptions = {} +): Datatable => { + const columns: DatatableColumn[] = []; + + const rows = esResponse.hits.hits + .map((hit) => { + const toConvert = params.source ? hit._source : hit.fields; + const flat = flattenHit(toConvert, index, params.shallow); + for (const [key, value] of Object.entries(flat)) { + const field = index?.fields.getByName(key); + const fieldName = field?.name || key; + if (!columns.find((c) => c.id === fieldName)) { + const fieldType = (field?.type as DatatableColumnType) || typeof value; + const formatter = field && index?.getFormatterForField(field); + columns.push({ + id: fieldName, + name: fieldName, + meta: { + type: fieldType, + field: fieldName, + index: index?.id, + params: formatter ? formatter.toJSON() : undefined, + }, + }); + } + } + return flat; + }) + .filter((hit) => hit); + + return { + type: 'datatable', + columns, + rows, + }; +}; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 1390b28ec830d..d2439e3f1573c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2283,7 +2283,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 65313adfc0e0f..3143d5baa5b77 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1113,8 +1113,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js index 32fc2873d7f2a..d5c72d34006e2 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.js @@ -25,7 +25,7 @@ import { getServices } from '../../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; -import { FAILURE_REASONS, LOADING_STATUS } from './constants'; +import { FAILURE_REASONS, LOADING_STATUS } from './index'; import { MarkdownSimple } from '../../../../../../kibana_react/public'; export function QueryActionsProvider(Promise) { diff --git a/src/plugins/discover/public/application/angular/context/query/index.js b/src/plugins/discover/public/application/angular/context/query/index.js index f9b1a35e32fea..59d1f165d19d4 100644 --- a/src/plugins/discover/public/application/angular/context/query/index.js +++ b/src/plugins/discover/public/application/angular/context/query/index.js @@ -18,5 +18,5 @@ */ export { QueryActionsProvider } from './actions'; -export { FAILURE_REASONS, LOADING_STATUS } from './constants'; +export { FAILURE_REASONS, LOADING_STATUS } from '../../../components/context_app/constants'; export { createInitialLoadingStatusState } from './state'; diff --git a/src/plugins/discover/public/application/angular/context/query/state.js b/src/plugins/discover/public/application/angular/context/query/state.js index 06fd0680d347f..142b5746249bf 100644 --- a/src/plugins/discover/public/application/angular/context/query/state.js +++ b/src/plugins/discover/public/application/angular/context/query/state.js @@ -17,7 +17,7 @@ * under the License. */ -import { LOADING_STATUS } from './constants'; +import { LOADING_STATUS } from './index'; export function createInitialLoadingStatusState() { return { diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 6adcaeeae94f5..d609a497c4ba1 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -12,8 +12,8 @@ - @@ -35,39 +35,17 @@ type="'predecessors'" > - - -
-
-
- -
-
- -
-
+ , -
-
-
-
-
-
-
-
-

- Refine your query -

-

- The search bar at the top uses Elasticsearch’s support for Lucene - - Query String syntax - - . Here are some examples of how you can search for web server logs that have been parsed into a few fields. -

-
-
-
-
-
- - Find requests that contain the number 200, in any field - -
-
-
- - - 200 - - -
-
-
- - Find 200 in the status field - -
-
-
- - - status:200 - - -
-
-
- - Find all status codes between 400-499 - -
-
-
- - - status:[400 TO 499] - - -
-
-
- - Find status codes 400-499 with the extension php - -
-
-
- - - status:[400 TO 499] AND extension:PHP - - -
-
-
- - Find status codes 400-499 with the extension php or html - -
-
-
- - - status:[400 TO 499] AND (extension:php OR extension:html) - - -
-
-
-
-
, -] -`; - -exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = ` -Array [ -
, -
-
-
-
-
-
-
-
-

- Expand your time range -

-

- One or more of the indices you’re looking at contains a date field. Your query may not match anything in the current time range, or there may not be any data at all in the currently selected time range. You can try changing the time range to one which contains data. -

-
-
-
, -] -`; diff --git a/src/plugins/discover/public/application/angular/directives/_index.scss b/src/plugins/discover/public/application/angular/directives/_index.scss index d4b365547b40c..dfacdf45c9d7b 100644 --- a/src/plugins/discover/public/application/angular/directives/_index.scss +++ b/src/plugins/discover/public/application/angular/directives/_index.scss @@ -1,2 +1 @@ -@import 'no_results'; @import 'histogram'; diff --git a/src/plugins/discover/public/application/angular/directives/index.js b/src/plugins/discover/public/application/angular/directives/index.ts similarity index 59% rename from src/plugins/discover/public/application/angular/directives/index.js rename to src/plugins/discover/public/application/angular/directives/index.ts index 5d8969a78f018..2e120995cce07 100644 --- a/src/plugins/discover/public/application/angular/directives/index.js +++ b/src/plugins/discover/public/application/angular/directives/index.ts @@ -17,15 +17,5 @@ * under the License. */ -import { DiscoverNoResults } from './no_results'; -import { DiscoverUninitialized } from './uninitialized'; -import { DiscoverHistogram } from './histogram'; -import { getAngularModule } from '../../../kibana_services'; - -const app = getAngularModule(); - -app.directive('discoverNoResults', (reactDirective) => reactDirective(DiscoverNoResults)); - -app.directive('discoverUninitialized', (reactDirective) => reactDirective(DiscoverUninitialized)); - -app.directive('discoverHistogram', (reactDirective) => reactDirective(DiscoverHistogram)); +export { DiscoverUninitialized } from './uninitialized'; +export { DiscoverHistogram } from './histogram'; diff --git a/src/plugins/discover/public/application/angular/directives/no_results.js b/src/plugins/discover/public/application/angular/directives/no_results.js deleted file mode 100644 index d8a39d9178e93..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/no_results.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Component, Fragment } from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import PropTypes from 'prop-types'; - -import { - EuiCallOut, - EuiCode, - EuiDescriptionList, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; - -// eslint-disable-next-line react/prefer-stateless-function -export class DiscoverNoResults extends Component { - static propTypes = { - timeFieldName: PropTypes.string, - queryLanguage: PropTypes.string, - }; - - render() { - const { timeFieldName, queryLanguage } = this.props; - - let timeFieldMessage; - - if (timeFieldName) { - timeFieldMessage = ( - - - - -

- -

- -

- -

-
-
- ); - } - - let luceneQueryMessage; - - if (queryLanguage === 'lucene') { - const searchExamples = [ - { - description: 200, - title: ( - - - - - - ), - }, - { - description: status:200, - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499], - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499] AND extension:PHP, - title: ( - - - - - - ), - }, - { - description: status:[400 TO 499] AND (extension:php OR extension:html), - title: ( - - - - - - ), - }, - ]; - - luceneQueryMessage = ( - - - - -

- -

- -

- - - - ), - }} - /> -

-
- - - - - - -
- ); - } - - return ( - - - - - - - - } - color="warning" - iconType="help" - data-test-subj="discoverNoResults" - /> - {timeFieldMessage} - {luceneQueryMessage} - - - - - ); - } -} diff --git a/src/plugins/discover/public/application/angular/directives/no_results.test.js b/src/plugins/discover/public/application/angular/directives/no_results.test.js deleted file mode 100644 index 60c50048a39ef..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/no_results.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { renderWithIntl } from 'test_utils/enzyme_helpers'; - -import { DiscoverNoResults } from './no_results'; - -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - docLinks: { - links: { - query: { - luceneQuerySyntax: 'documentation-link', - }, - }, - }, - }), - }; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('DiscoverNoResults', () => { - describe('props', () => { - describe('timeFieldName', () => { - test('renders time range feedback', () => { - const component = renderWithIntl(); - - expect(component).toMatchSnapshot(); - }); - }); - - describe('queryLanguage', () => { - test('supports lucene and renders doc link', () => { - const component = renderWithIntl( - 'documentation-link'} /> - ); - - expect(component).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 612cedb7780bd..ebd086dd1e38a 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -87,6 +87,7 @@ const fetchStatuses = { UNINITIALIZED: 'uninitialized', LOADING: 'loading', COMPLETE: 'complete', + ERROR: 'error', }; const app = getAngularModule(); @@ -620,6 +621,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise config: config, fixedScroll: createFixedScroll($scope, $timeout), setHeaderActionMenu: getHeaderActionMenuMounter(), + data, }; const shouldSearchOnPageLoad = () => { @@ -685,7 +687,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise function pick(rows, oldRows, fetchStatus) { // initial state, pretend we're already loading if we're about to execute a search so // that the uninitilized message doesn't flash on screen - if (rows == null && oldRows == null && shouldSearchOnPageLoad()) { + if (!$scope.fetchError && rows == null && oldRows == null && shouldSearchOnPageLoad()) { return status.LOADING; } @@ -814,7 +816,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (error instanceof Error && error.name === 'AbortError') return; $scope.fetchStatus = fetchStatuses.NO_RESULTS; - $scope.rows = []; + $scope.fetchError = error; data.search.showError(error); }); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index ad2b674af014c..f191fa2dc89e8 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -68,6 +68,7 @@ export function convertDirectiveToRenderFn( let rejected = false; const cleanupFnPromise = injectAngularElement(domNode, directive.template, props, getInjector); + cleanupFnPromise.catch(() => { rejected = true; render(
error
, domNode); @@ -91,10 +92,10 @@ export interface DocTableLegacyProps { rows: Array>; indexPattern: IIndexPattern; minimumVisibleRows: number; - onAddColumn: (column: string) => void; - onSort: (sort: string[][]) => void; - onMoveColumn: (columns: string, newIdx: number) => void; - onRemoveColumn: (column: string) => void; + onAddColumn?: (column: string) => void; + onSort?: (sort: string[][]) => void; + onMoveColumn?: (columns: string, newIdx: number) => void; + onRemoveColumn?: (column: string) => void; sort?: string[][]; } diff --git a/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap new file mode 100644 index 0000000000000..58305ee23cb21 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap @@ -0,0 +1,741 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContextAppLegacy test renders correctly 1`] = ` + + + + + +
+
+ +
+ +
+
+ + + + + +`; + +exports[`ContextAppLegacy test renders loading indicator 1`] = ` + + + + + +
+ +
+ +
+ + Loading... + +
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/discover/public/application/angular/context/query/constants.js b/src/plugins/discover/public/application/components/context_app/constants.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/constants.js rename to src/plugins/discover/public/application/components/context_app/constants.ts diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx new file mode 100644 index 0000000000000..16d8cd78004f9 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { ContextAppLegacy } from './context_app_legacy'; +import { IIndexPattern } from '../../../../../data/common/index_patterns'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('ContextAppLegacy test', () => { + const hit = { + _id: '123', + _index: 'test_index', + _score: null, + _version: 1, + _source: { + category: ["Men's Clothing"], + currency: 'EUR', + customer_first_name: 'Walker', + customer_full_name: 'Walker Texas Ranger', + customer_gender: 'MALE', + customer_last_name: 'Ranger', + }, + fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], + sort: [1603114502000, 2092], + }; + const indexPattern = { + id: 'test_index_pattern', + } as IIndexPattern; + const defaultProps = { + columns: ['_source'], + filter: () => {}, + hits: [hit], + infiniteScroll: true, + sorting: ['order_date', 'desc'], + minimumVisibleRows: 5, + indexPattern, + status: 'loaded', + }; + + it('renders correctly', () => { + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + expect(component.find(DocTableLegacy).length).toBe(1); + const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(loadingIndicator.length).toBe(0); + }); + + it('renders loading indicator', () => { + const props = { ...defaultProps }; + props.status = 'loading'; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + expect(component.find('DocTableLegacy').length).toBe(0); + const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(loadingIndicator.length).toBe(1); + }); +}); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx new file mode 100644 index 0000000000000..ee8b2f590f71c --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + DocTableLegacy, + DocTableLegacyProps, +} from '../../angular/doc_table/create_doc_table_react'; +import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { LOADING_STATUS } from './constants'; + +export interface ContextAppProps { + columns: string[]; + hits: Array>; + indexPattern: IIndexPattern; + filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + minimumVisibleRows: number; + sorting: string[]; + status: string; +} + +export function ContextAppLegacy(renderProps: ContextAppProps) { + const { hits, filter, sorting, status } = renderProps; + const props = ({ ...renderProps } as unknown) as DocTableLegacyProps; + props.rows = hits; + props.onFilter = filter; + props.sort = sorting.map((el) => [el]); + const isLoaded = status === LOADING_STATUS.LOADED; + const loadingFeedback = () => { + if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { + return ( + + + + + + ); + } + return null; + }; + return ( + + + {loadingFeedback()} + {isLoaded ? ( + +
+ +
+
+ ) : null} +
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts new file mode 100644 index 0000000000000..af94c5537da28 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -0,0 +1,32 @@ +/* + * 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 { ContextAppLegacy } from './context_app_legacy'; + +export function createContextAppLegacy(reactDirective: any) { + return reactDirective(ContextAppLegacy, [ + ['filter', { watchDepth: 'reference' }], + ['hits', { watchDepth: 'reference' }], + ['indexPattern', { watchDepth: 'reference' }], + ['sorting', { watchDepth: 'reference' }], + ['columns', { watchDepth: 'collection' }], + ['infiniteScroll', { watchDepth: 'reference' }], + ['minimumVisibleRows', { watchDepth: 'reference' }], + ['status', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 139b2ca69d9e4..3ca421f809640 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -26,10 +26,8 @@ import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; import { DiscoverSidebar } from './sidebar'; import { getServices, IndexPattern } from '../../kibana_services'; -// @ts-ignore -import { DiscoverNoResults } from '../angular/directives/no_results'; -import { DiscoverUninitialized } from '../angular/directives/uninitialized'; -import { DiscoverHistogram } from '../angular/directives/histogram'; +import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; +import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; @@ -40,6 +38,7 @@ import { TimeRange, Query, IndexPatternAttributes, + DataPublicPluginStart, } from '../../../../data/public'; import { Chart } from '../angular/helpers/point_series'; import { AppState } from '../angular/discover_state'; @@ -53,6 +52,7 @@ export interface DiscoverLegacyProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; + fetchError: Error; fieldCounts: Record; histogramData: Chart; hits: number; @@ -73,6 +73,7 @@ export interface DiscoverLegacyProps { sampleSize: number; fixedScroll: (el: HTMLElement) => void; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + data: DataPublicPluginStart; }; resetQuery: () => void; resultState: string; @@ -94,6 +95,7 @@ export function DiscoverLegacy({ fetch, fetchCounter, fieldCounts, + fetchError, histogramData, hits, indexPattern, @@ -208,6 +210,8 @@ export function DiscoverLegacy({ )} {resultState === 'uninitialized' && } diff --git a/src/plugins/discover/public/application/angular/directives/_no_results.scss b/src/plugins/discover/public/application/components/no_results/_no_results.scss similarity index 100% rename from src/plugins/discover/public/application/angular/directives/_no_results.scss rename to src/plugins/discover/public/application/components/no_results/_no_results.scss diff --git a/src/plugins/discover/public/application/components/no_results/index.ts b/src/plugins/discover/public/application/components/no_results/index.ts new file mode 100644 index 0000000000000..afe35b1fd7c18 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/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 { DiscoverNoResults } from './no_results'; diff --git a/src/plugins/discover/public/application/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/components/no_results/no_results.test.tsx new file mode 100644 index 0000000000000..dde75236eb15e --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results.test.tsx @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results'; + +jest.mock('../../../kibana_services', () => { + return { + getServices: () => ({ + docLinks: { + links: { + query: { + luceneQuerySyntax: 'documentation-link', + }, + }, + }, + }), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function mountAndFindSubjects(props: DiscoverNoResultsProps) { + const component = mountWithIntl(); + return { + mainMsg: findTestSubject(component, 'discoverNoResults').length > 0, + timeFieldMsg: findTestSubject(component, 'discoverNoResultsTimefilter').length > 0, + luceneMsg: findTestSubject(component, 'discoverNoResultsLucene').length > 0, + errorMsg: findTestSubject(component, 'discoverNoResultsError').length > 0, + }; +} + +describe('DiscoverNoResults', () => { + describe('props', () => { + describe('no props', () => { + test('renders default feedback', () => { + const result = mountAndFindSubjects({}); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": false, + "mainMsg": true, + "timeFieldMsg": false, + } + `); + }); + }); + describe('timeFieldName', () => { + test('renders time range feedback', () => { + const result = mountAndFindSubjects({ + timeFieldName: 'awesome_time_field', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": false, + "mainMsg": true, + "timeFieldMsg": true, + } + `); + }); + }); + + describe('queryLanguage', () => { + test('supports lucene and renders doc link', () => { + const result = mountAndFindSubjects({ queryLanguage: 'lucene' }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": false, + "luceneMsg": true, + "mainMsg": true, + "timeFieldMsg": false, + } + `); + }); + }); + + describe('error message', () => { + test('renders error message', () => { + const error = new Error('Fatal error'); + const result = mountAndFindSubjects({ + timeFieldName: 'awesome_time_field', + error, + queryLanguage: 'lucene', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "errorMsg": true, + "luceneMsg": false, + "mainMsg": false, + "timeFieldMsg": false, + } + `); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx new file mode 100644 index 0000000000000..fcc2912d16dd5 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { getServices } from '../../../kibana_services'; +import { DataPublicPluginStart } from '../../../../../data/public'; +import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper'; +import './_no_results.scss'; + +export interface DiscoverNoResultsProps { + timeFieldName?: string; + queryLanguage?: string; + error?: Error; + data?: DataPublicPluginStart; +} + +export function DiscoverNoResults({ + timeFieldName, + queryLanguage, + error, + data, +}: DiscoverNoResultsProps) { + const callOut = !error ? ( + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> + {timeFieldName ? getTimeFieldMessage() : null} + {queryLanguage === 'lucene' + ? getLuceneQueryMessage(getServices().docLinks.links.query.luceneQuerySyntax) + : null} + + ) : ( + + + } + color="danger" + iconType="alert" + data-test-subj="discoverNoResultsError" + > + (data ? data.search.showError(error) : void 0)} + > + + + + + ); + + return ( + + + {callOut} + + ); +} diff --git a/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx new file mode 100644 index 0000000000000..fbc655e01bdf5 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiDescriptionList, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; + +export function getTimeFieldMessage() { + return ( + + + +

+ +

+

+ +

+
+
+ ); +} + +export function getLuceneQueryMessage(link: string) { + const searchExamples = [ + { + description: 200, + title: ( + + + + + + ), + }, + { + description: status:200, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499], + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND extension:PHP, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND (extension:php OR extension:html), + title: ( + + + + + + ), + }, + ]; + return ( + + + +

+ +

+

+ + + + ), + }} + /> +

+
+ + + +
+ ); +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 1ca0bb20e8723..55a75240909bf 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -36,6 +36,7 @@ import { createToolBarPagerButtonsDirective, createToolBarPagerTextDirective, } from './application/angular/doc_table/components/pager'; +import { createContextAppLegacy } from './application/components/context_app/context_app_legacy_directive'; import { createTableRowDirective } from './application/angular/doc_table/components/table_row'; import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; @@ -55,7 +56,6 @@ import { createContextErrorMessageDirective } from './application/components/con import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; - /** * returns the main inner angular module, it contains all the parts of Angular Discover * needs to render, so in the end the current 'kibana' angular module is no longer necessary @@ -190,5 +190,6 @@ function createDocTableModule() { .directive('kbnTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('kbnInfiniteScroll', createInfiniteScrollDirective) - .directive('docViewer', createDocViewerDirective); + .directive('docViewer', createDocViewerDirective) + .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx index 33724068a6ba8..551caa28d2291 100644 --- a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx +++ b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx @@ -17,37 +17,45 @@ * under the License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { withKnobs, boolean } from '@storybook/addon-knobs'; +import * as React from 'react'; import { PanelOptionsMenu } from '..'; -const euiContextDescriptors = { - id: 'mainMenu', - title: 'Options', - items: [ - { - name: 'Inspect', - icon: 'inspect', - onClick: action('onClick(inspect)'), - }, - { - name: 'Full screen', - icon: 'expand', - onClick: action('onClick(expand)'), +export default { + title: 'components/PanelOptionsMenu', + component: PanelOptionsMenu, + argTypes: { + isViewMode: { + control: { type: 'boolean' }, }, + }, + decorators: [ + (Story: React.ComponentType) => ( +
+ +
+ ), ], }; -storiesOf('components/PanelOptionsMenu', module) - .addDecorator(withKnobs) - .add('default', () => { - const isViewMode = boolean('isViewMode', false); +export function Default({ isViewMode }: React.ComponentProps) { + const euiContextDescriptors = { + id: 'mainMenu', + title: 'Options', + items: [ + { + name: 'Inspect', + icon: 'inspect', + onClick: action('onClick(inspect)'), + }, + { + name: 'Full screen', + icon: 'expand', + onClick: action('onClick(expand)'), + }, + ], + }; - return ( -
- -
- ); - }); + return ; +} +Default.args = { isViewMode: false } as React.ComponentProps; diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json new file mode 100644 index 0000000000000..d664d936f6667 --- /dev/null +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/*", + "server/**/**/*", + "../../../typings/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/usage_collection/tsconfig.json" }, + ] +} diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json new file mode 100644 index 0000000000000..66244a22336c7 --- /dev/null +++ b/src/plugins/newsfeed/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "common/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" } + ] +} diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap index e32425a095429..88ed7c66a79a2 100644 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; +exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap index dbc3dd1202cbd..d7707f64d8a4f 100644 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; +exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js index e48515d243844..b1de60f854a1c 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js @@ -48,7 +48,11 @@ export class TagCloud extends EventEmitter { this.resize(); //SETTING (non-configurable) - this._fontFamily = 'Open Sans, sans-serif'; + /** + * the fontFamily should be set explicitly for calculating a layout + * and to avoid words overlapping + */ + this._fontFamily = 'Inter UI, sans-serif'; this._fontStyle = 'normal'; this._fontWeight = 'normal'; this._spiral = 'archimedean'; //layout shape diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index 18a09ec9f4969..cb0daa6d29382 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -62,10 +62,10 @@ export const TagCloudChart = ({ () => throttle(() => { if (visController.current) { - visController.current.render().then(renderComplete); + visController.current.render(visData, visParams).then(renderComplete); } }, 300), - [renderComplete] + [renderComplete, visData, visParams] ); return ( diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js index 5ec22d2c6a4d9..0d64c9d02eafd 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js @@ -85,11 +85,8 @@ export class TagCloudVisualization { } async render(data, visParams) { - if (data && visParams) { - this._updateParams(visParams); - this._updateData(data); - } - + this._updateParams(visParams); + this._updateData(data); this._resize(); await this._renderComplete$.pipe(take(1)).toPromise(); diff --git a/tasks/config/run.js b/tasks/config/run.js index eddcb0bdd59d0..e96011816ed4d 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -177,6 +177,10 @@ module.exports = function () { 'test/server_integration/http/ssl_redirect/config.js', '--config', 'test/server_integration/http/cache/config.js', + '--config', + 'test/server_integration/http/ssl_with_p12/config.js', + '--config', + 'test/server_integration/http/ssl_with_p12_intermediate/config.js', '--bail', '--debug', '--kibana-install-dir', diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index d45b8f4841cb6..118234d54626c 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - describe('discover tab', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/78689 + describe.skip('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/tsconfig.json b/test/tsconfig.json index ec6612d0dd00d..a6cc2d34639b7 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -23,6 +23,8 @@ { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" } + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 4f22168b67a09..73646291e3d08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,9 @@ "src/plugins/kibana_react/**/*", "src/plugins/usage_collection/**/*", "src/plugins/telemetry_collection_manager/**/*", - "src/plugins/telemetry/**/*" + "src/plugins/telemetry/**/*", + "src/plugins/kibana_usage_collection/**/*", + "src/plugins/newsfeed/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -27,6 +29,8 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" } + { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 4b6ed4a0db813..bb1bdc08cafd6 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -8,5 +8,7 @@ { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/newsfeed/tsconfig.json" }, ] } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts index 55c980d5edeb4..314254883b2fd 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -12,9 +12,7 @@ When('the user changes the selected percentile', () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); - getDataTestSubj('uxPercentileSelect').click(); - - getDataTestSubj('p95Percentile').click(); + getDataTestSubj('uxPercentileSelect').select('95'); }); Then(`it displays client metric related to that percentile`, () => { @@ -22,8 +20,5 @@ Then(`it displays client metric related to that percentile`, () => { verifyClientMetrics(metrics, false); - // reset to median - getDataTestSubj('uxPercentileSelect').click(); - - getDataTestSubj('p50Percentile').click(); + getDataTestSubj('uxPercentileSelect').select('50'); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 75a018afa13d7..18cd7d79cc69f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -6,19 +6,14 @@ import React, { useCallback, useEffect } from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; +import { EuiSelect } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { I18LABELS } from '../translations'; const DEFAULT_P = 50; -const StyledSpan = styled.span` - font-weight: 600; -`; - export function UserPercentile() { const history = useHistory(); @@ -49,32 +44,29 @@ export function UserPercentile() { const options = [ { value: '50', - inputDisplay: I18LABELS.percentile50thMedian, + text: I18LABELS.percentile50thMedian, dropdownDisplay: I18LABELS.percentile50thMedian, 'data-test-subj': 'p50Percentile', }, { value: '75', - inputDisplay: {I18LABELS.percentile75th}, + text: I18LABELS.percentile75th, dropdownDisplay: I18LABELS.percentile75th, 'data-test-subj': 'p75Percentile', }, { value: '90', - inputDisplay: {I18LABELS.percentile90th}, - dropdownDisplay: I18LABELS.percentile90th, + text: I18LABELS.percentile90th, 'data-test-subj': 'p90Percentile', }, { value: '95', - inputDisplay: {I18LABELS.percentile95th}, - dropdownDisplay: I18LABELS.percentile95th, + text: I18LABELS.percentile95th, 'data-test-subj': 'p95Percentile', }, { value: '99', - inputDisplay: {I18LABELS.percentile99th}, - dropdownDisplay: I18LABELS.percentile99th, + text: I18LABELS.percentile99th, 'data-test-subj': 'p99Percentile', }, ]; @@ -84,13 +76,12 @@ export function UserPercentile() { }; return ( - onChange(value)} + onChange={(evt) => onChange(evt.target.value)} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 1198c014f5921..77afe92a8f521 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -20,7 +20,7 @@ import { ViewMode, isErrorEmbeddable, } from '../../../../../../../../src/plugins/embeddable/public'; -import { getLayerList } from './LayerList'; +import { useLayerList } from './useLayerList'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; @@ -55,6 +55,8 @@ export function EmbeddedMapComponent() { const mapFilters = useMapFilters(); + const layerList = useLayerList(); + const [embeddable, setEmbeddable] = useState< MapEmbeddable | ErrorEmbeddable | undefined >(); @@ -148,7 +150,7 @@ export function EmbeddedMapComponent() { if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - await embeddableObject.setLayerList(getLayerList()); + await embeddableObject.setLayerList(layerList); } setEmbeddable(embeddableObject); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx index 07b40addedec3..467e31f15a8de 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -18,7 +18,7 @@ import { REGION_NAME, TRANSACTION_DURATION_COUNTRY, TRANSACTION_DURATION_REGION, -} from './LayerList'; +} from './useLayerList'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index 023f5d61a964e..1053dd611d519 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { EuiThemeProvider } from '../../../../../../../observability/public'; import { MapToolTip } from '../MapToolTip'; -import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; storiesOf('app/RumDashboard/VisitorsRegionMap', module) .addDecorator((storyFn) => {storyFn()}) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts index c45f8b27d7d3e..e564332583375 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -25,6 +25,11 @@ export const mockLayerList = [ id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', term: 'client.geo.country_iso_code', + whereQuery: { + language: 'kuery', + query: + 'transaction.type : "page-load" and service.name : "undefined"', + }, metrics: [ { type: 'avg', @@ -95,6 +100,11 @@ export const mockLayerList = [ id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', indexPatternTitle: 'apm-*', term: 'client.geo.region_iso_code', + whereQuery: { + language: 'kuery', + query: + 'transaction.type : "page-load" and service.name : "undefined"', + }, metrics: [{ type: 'avg', field: 'transaction.duration.us' }], indexPatternId: 'apm_static_index_pattern_id', }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts similarity index 51% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts index eb149ee2a132d..872553452b263 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { renderHook } from '@testing-library/react-hooks'; import { mockLayerList } from './__mocks__/regions_layer.mock'; -import { getLayerList } from '../LayerList'; +import { useLayerList } from '../useLayerList'; -describe('LayerList', () => { - describe('getLayerList', () => { - test('it returns the region layer', () => { - const layerList = getLayerList(); - expect(layerList).toStrictEqual(mockLayerList); - }); +describe('useLayerList', () => { + test('it returns the region layer', () => { + const { result } = renderHook(() => useLayerList()); + expect(result.current).toStrictEqual(mockLayerList); }); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts similarity index 79% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index 138a3f4018c65..bc45d58329f49 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -22,8 +22,14 @@ import { } from '../../../../../../maps/common/constants'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types'; -const ES_TERM_SOURCE: ESTermSourceDescriptor = { +const ES_TERM_SOURCE_COUNTRY: ESTermSourceDescriptor = { type: 'ES_TERM_SOURCE', id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', @@ -39,6 +45,26 @@ const ES_TERM_SOURCE: ESTermSourceDescriptor = { applyGlobalQuery: true, }; +const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], + whereQuery: { + query: 'transaction.type : "page-load"', + language: 'kuery', + }, + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, +}; + +const getWhereQuery = (serviceName: string) => { + return { + query: `${TRANSACTION_TYPE} : "${TRANSACTION_PAGE_LOAD}" and ${SERVICE_NAME} : "${serviceName}"`, + language: 'kuery', + }; +}; + export const REGION_NAME = 'region_name'; export const COUNTRY_NAME = 'name'; @@ -56,7 +82,11 @@ interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { sourceDescriptor: EMSFileSourceDescriptor; } -export function getLayerList() { +export function useLayerList() { + const { urlParams } = useUrlParams(); + + const { serviceName } = urlParams; + const baseLayer: LayerDescriptor = { sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, id: 'b7af286d-2580-4f47-be93-9653d594ce7e', @@ -69,6 +99,8 @@ export function getLayerList() { type: 'VECTOR_TILE', }; + ES_TERM_SOURCE_COUNTRY.whereQuery = getWhereQuery(serviceName!); + const getLayerStyle = (fieldName: string): VectorStyleDescriptor => { return { type: 'VECTOR', @@ -119,7 +151,7 @@ export function getLayerList() { joins: [ { leftField: 'iso2', - right: ES_TERM_SOURCE, + right: ES_TERM_SOURCE_COUNTRY, }, ], sourceDescriptor: { @@ -138,18 +170,13 @@ export function getLayerList() { type: 'VECTOR', }; + ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); + const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = { joins: [ { leftField: 'region_iso_code', - right: { - type: 'ES_TERM_SOURCE', - id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', - indexPatternTitle: 'apm-*', - term: 'client.geo.region_iso_code', - metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], - indexPatternId: APM_STATIC_INDEX_PATTERN_ID, - }, + right: ES_TERM_SOURCE_REGION, }, ], sourceDescriptor: { @@ -166,6 +193,7 @@ export function getLayerList() { visible: true, type: 'VECTOR', }; + return [ baseLayer, pageLoadDurationByCountryLayer, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx index a5393995f0864..0e517f6bf6212 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boolean, withKnobs } from '@storybook/addon-knobs'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { SyncBadge } from './SyncBadge'; -storiesOf('app/TransactionDetails/SyncBadge', module) - .addDecorator(withKnobs) - .add( - 'example', - () => { - return ; +export default { + title: 'app/TransactionDetails/SyncBadge', + component: SyncBadge, + argTypes: { + sync: { + control: { type: 'inline-radio', options: [true, false, undefined] }, }, - { - showPanel: true, - info: { source: false }, - } - ) - .add( - 'sync=undefined', - () => { - return ; - }, - { info: { source: false } } - ); + }, +}; + +export function Example({ sync }: ComponentProps) { + return ; +} +Example.args = { sync: true } as ComponentProps; diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index c68dc49cd9370..3549f378efced 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.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@octokit/rest": "^16.35.0", "console-stamp": "^0.2.9", "hdr-histogram-js": "^1.2.0" diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 1c3a770da79f5..45005f3f5e422 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -22,6 +22,7 @@ interface CloudSetupDependencies { export interface CloudSetup { cloudId?: string; + cloudDeploymentUrl?: string; isCloudEnabled: boolean; } @@ -33,7 +34,7 @@ export class CloudPlugin implements Plugin { } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id, resetPasswordUrl } = this.config; + const { id, resetPasswordUrl, deploymentUrl } = this.config; const isCloudEnabled = getIsCloudEnabled(id); if (home) { @@ -45,6 +46,7 @@ export class CloudPlugin implements Plugin { return { cloudId: id, + cloudDeploymentUrl: deploymentUrl, isCloudEnabled, }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 2c833bcfeaf4c..1516aa9096eca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -28,6 +28,7 @@ jest.mock('react-router-dom', () => ({ ...(jest.requireActual('react-router-dom') as object), useHistory: jest.fn(() => mockHistory), useLocation: jest.fn(() => mockLocation), + useParams: jest.fn(() => ({})), })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts deleted file mode 100644 index 05c60ebced088..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { useDidUpdateEffect } from './use_did_update_effect'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx deleted file mode 100644 index e3d2ffb44f01e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx +++ /dev/null @@ -1,33 +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, { useState } from 'react'; -import { mount } from 'enzyme'; - -import { EuiLink } from '@elastic/eui'; - -import { useDidUpdateEffect } from './use_did_update_effect'; - -const fn = jest.fn(); - -const TestHook = ({ value }: { value: number }) => { - const [inputValue, setValue] = useState(value); - useDidUpdateEffect(fn, [inputValue]); - return setValue(2)} />; -}; - -const wrapper = mount(); - -describe('useDidUpdateEffect', () => { - it('should not fire function when value unchanged', () => { - expect(fn).not.toHaveBeenCalled(); - }); - - it('should fire function when value changed', () => { - wrapper.find(EuiLink).simulate('click'); - expect(fn).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx deleted file mode 100644 index 4c3e10fc84b84..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Sometimes we don't want to fire the initial useEffect call. - * This custom Hook only fires after the intial render has completed. - */ -import { useEffect, useRef, DependencyList } from 'react'; - -export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => { - const didMountRef = useRef(false); - - useEffect(() => { - if (didMountRef.current) { - fn(); - } else { - didMountRef.current = true; - } - }, inputs); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 7789d0caba345..c305ae9d5f7a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -20,7 +20,7 @@ export const contentSources = [ boost: 1, }, { - id: '123', + id: '124', serviceType: 'jira', searchable: true, supportedByLicense: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts new file mode 100644 index 0000000000000..18e7851485222 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IGroupValues } from '../group_logic'; + +import { IGroupDetails, ISourcePriority } from '../../../types'; + +export const mockGroupValues = { + group: {} as IGroupDetails, + dataLoading: true, + manageUsersModalVisible: false, + managerModalFormErrors: [], + sharedSourcesModalVisible: false, + confirmDeleteModalVisible: false, + groupNameInputValue: '', + selectedGroupSources: [], + selectedGroupUsers: [], + groupPrioritiesUnchanged: true, + activeSourcePriorities: {} as ISourcePriority, + cachedSourcePriorities: {} as ISourcePriority, +} as IGroupValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts new file mode 100644 index 0000000000000..0483799b73093 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.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 { IGroupsValues } from '../groups_logic'; + +import { IContentSource, IUser, IGroup } from '../../../types'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +export const mockGroupsValues = { + groups: [] as IGroup[], + contentSources: [] as IContentSource[], + users: [] as IUser[], + groupsDataLoading: true, + groupListLoading: true, + newGroupModalOpen: false, + newGroupName: '', + hasFiltersSet: false, + newGroup: null, + newGroupNameErrors: [], + filterSourcesDropdownOpen: false, + filteredSources: [], + filterUsersDropdownOpen: false, + filteredUsers: [], + allGroupUsersLoading: false, + allGroupUsers: [], + filterValue: '', + groupsMeta: DEFAULT_META, +} as IGroupsValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts new file mode 100644 index 0000000000000..b4724c8ed3f0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -0,0 +1,509 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { + FlashMessagesLogic, + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, +} from '../../../shared/flash_messages'; + +jest.mock('../../../shared/kibana', () => ({ + KibanaLogic: { values: { navigateToUrl: jest.fn() } }, +})); +import { KibanaLogic } from '../../../shared/kibana'; + +import { groups } from '../../__mocks__/groups.mock'; +import { mockGroupValues } from './__mocks__/group_logic.mock'; +import { GroupLogic } from './group_logic'; + +import { GROUPS_PATH } from '../../routes'; + +describe('GroupLogic', () => { + const group = groups[0]; + const sourceIds = ['123', '124']; + const userIds = ['1z1z']; + const sourcePriorities = { [sourceIds[0]]: 1, [sourceIds[1]]: 0.5 }; + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupLogic.values).toEqual(mockGroupValues); + }); + + describe('actions', () => { + describe('onInitializeGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.onInitializeGroup(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.groupNameInputValue).toEqual(group.name); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(true); + }); + }); + + describe('onGroupNameChanged', () => { + it('sets reducers', () => { + const renamedGroup = { + ...group, + name: 'changed', + }; + GroupLogic.actions.onGroupNameChanged(renamedGroup); + + expect(GroupLogic.values.group).toEqual(renamedGroup); + expect(GroupLogic.values.groupNameInputValue).toEqual(renamedGroup.name); + }); + }); + + describe('onGroupPrioritiesChanged', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupPrioritiesChanged(group); + + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupNameInputChange', () => { + it('sets reducers', () => { + const name = 'new name'; + GroupLogic.actions.onGroupNameInputChange(name); + + expect(GroupLogic.values.groupNameInputValue).toEqual(name); + }); + }); + + describe('addGroupSource', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupSource', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + GroupLogic.actions.addGroupSource(sourceIds[1]); + GroupLogic.actions.removeGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[1]]); + }); + }); + + describe('addGroupUser', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupUser', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + GroupLogic.actions.addGroupUser(sourceIds[1]); + GroupLogic.actions.removeGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[1]]); + }); + }); + + describe('onGroupSourcesSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupSourcesSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupUsersSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupUsersSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('setGroupModalErrors', () => { + it('sets reducers', () => { + const errors = ['this is an error']; + GroupLogic.actions.setGroupModalErrors(errors); + + expect(GroupLogic.values.managerModalFormErrors).toEqual(errors); + }); + }); + + describe('hideSharedSourcesModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideSharedSourcesModal(group); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('hideManageUsersModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideManageUsersModal(group); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.managerModalFormErrors).toEqual([]); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('selectAllSources', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllSources(group.contentSources); + + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('selectAllUsers', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllUsers(group.users); + + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('updatePriority', () => { + it('sets reducers', () => { + const PRIORITY_VALUE = 4; + GroupLogic.actions.updatePriority(sourceIds[0], PRIORITY_VALUE); + + expect(GroupLogic.values.activeSourcePriorities).toEqual({ + [sourceIds[0]]: PRIORITY_VALUE, + }); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(false); + }); + }); + + describe('resetGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.resetGroup(); + + expect(GroupLogic.values.group).toEqual({}); + expect(GroupLogic.values.dataLoading).toEqual(true); + }); + }); + + describe('hideConfirmDeleteModal', () => { + it('sets reducer', () => { + GroupLogic.actions.showConfirmDeleteModal(); + GroupLogic.actions.hideConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroup', () => { + it('calls API and sets values', async () => { + const onInitializeGroupSpy = jest.spyOn(GroupLogic.actions, 'onInitializeGroup'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups/123'); + await promise; + expect(onInitializeGroupSpy).toHaveBeenCalledWith(group); + }); + + it('handles 404 error', async () => { + const promise = Promise.reject({ response: { status: 404 } }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'Unable to find group with ID: "123".', + }); + } + }); + + it('handles non-404 error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'this is an error', + }); + } + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('deletes a group', async () => { + const promise = Promise.resolve(true); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( + '/api/workplace_search/groups/123' + ); + + await promise; + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Group "group" was successfully deleted.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('updateGroupName', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.onGroupNameInputChange('new name'); + }); + it('updates name', async () => { + const onGroupNameChangedSpy = jest.spyOn(GroupLogic.actions, 'onGroupNameChanged'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith('/api/workplace_search/groups/123', { + body: JSON.stringify({ group: { name: 'new name' } }), + }); + + await promise; + expect(onGroupNameChangedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully renamed this group to "group".' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSources', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.selectAllSources(group.contentSources); + }); + it('updates name', async () => { + const onGroupSourcesSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupSourcesSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/share', + { + body: JSON.stringify({ content_source_ids: sourceIds }), + } + ); + + await promise; + expect(onGroupSourcesSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared content sources.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupUsers', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupUsersSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupUsersSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/assign', + { + body: JSON.stringify({ user_ids: userIds }), + } + ); + + await promise; + expect(onGroupUsersSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated the users of this group.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSourcePrioritization', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupPrioritiesChangedSpy = jest.spyOn( + GroupLogic.actions, + 'onGroupPrioritiesChanged' + ); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/boosts', + { + body: JSON.stringify({ + content_source_boosts: [ + [sourceIds[0], 1], + [sourceIds[1], 0.5], + ], + }), + } + ); + + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared source prioritization.' + ); + expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('showConfirmDeleteModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showSharedSourcesModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showSharedSourcesModal(); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showManageUsersModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showManageUsersModal(); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetFlashMessages', () => { + it('clears flash messages', () => { + GroupLogic.actions.resetFlashMessages(); + + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 1ce0fe53726d4..b895200d3fc22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -56,13 +56,11 @@ export interface IGroupActions { } export interface IGroupValues { - contentSources: IContentSourceDetails[]; - users: IUser[]; group: IGroupDetails; dataLoading: boolean; manageUsersModalVisible: boolean; managerModalFormErrors: string[]; - sharedSourcesModalModalVisible: boolean; + sharedSourcesModalVisible: boolean; confirmDeleteModalVisible: boolean; groupNameInputValue: string; selectedGroupSources: string[]; @@ -138,7 +136,7 @@ export const GroupLogic = kea>({ hideManageUsersModal: () => [], }, ], - sharedSourcesModalModalVisible: [ + sharedSourcesModalVisible: [ false, { showSharedSourcesModal: () => true, @@ -225,8 +223,7 @@ export const GroupLogic = kea>({ } ); - const error = e.response.status === 404 ? NOT_FOUND_MESSAGE : e; - + const error = e.response?.status === 404 ? NOT_FOUND_MESSAGE : e; FlashMessagesLogic.actions.setQueuedMessages({ type: 'error', message: error, @@ -321,7 +318,7 @@ export const GroupLogic = kea>({ const GROUP_USERS_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated', { - defaultMessage: 'Successfully updated the users of this group', + defaultMessage: 'Successfully updated the users of this group.', } ); setSuccessMessage(GROUP_USERS_UPDATED_MESSAGE); @@ -353,7 +350,7 @@ export const GroupLogic = kea>({ const GROUP_PRIORITIZATION_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupPrioritizationUpdated', { - defaultMessage: 'Successfully updated shared source prioritization', + defaultMessage: 'Successfully updated shared source prioritization.', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx new file mode 100644 index 0000000000000..6f293920fa387 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx @@ -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 '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { groups } from '../../__mocks__/groups.mock'; + +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { GroupOverview } from './components/group_overview'; +import { GroupSourcePrioritization } from './components/group_source_prioritization'; + +import { GroupRouter } from './group_router'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { ManageUsersModal } from './components/manage_users_modal'; +import { SharedSourcesModal } from './components/shared_sources_modal'; + +describe('GroupRouter', () => { + const initializeGroup = jest.fn(); + const resetGroup = jest.fn(); + + beforeEach(() => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: groups[0], + }); + + setMockActions({ + initializeGroup, + resetGroup, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FlashMessages)).toHaveLength(1); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupOverview)).toHaveLength(1); + expect(wrapper.find(GroupSourcePrioritization)).toHaveLength(1); + }); + + it('renders modals', () => { + setMockValues({ + sharedSourcesModalVisible: true, + manageUsersModalVisible: true, + group: groups[0], + }); + + const wrapper = shallow(); + + expect(wrapper.find(ManageUsersModal)).toHaveLength(1); + expect(wrapper.find(SharedSourcesModal)).toHaveLength(1); + }); + + it('handles breadcrumbs while loading', () => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: {}, + }); + + const loadingBreadcrumbs = ['Groups', '...']; + + const wrapper = shallow(); + + const firstBreadCrumb = wrapper.find(SetPageChrome).first(); + const lastBreadCrumb = wrapper.find(SetPageChrome).last(); + + expect(firstBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, 'Source Prioritization']); + expect(lastBreadCrumb.prop('trail')).toEqual(loadingBreadcrumbs); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 0a637497a5b05..1b6f0c4c49a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { Route, Switch, useParams } from 'react-router-dom'; -import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -26,16 +26,13 @@ import { GroupSourcePrioritization } from './components/group_source_prioritizat export const GroupRouter: React.FC = () => { const { groupId } = useParams() as { groupId: string }; - const { messages } = useValues(FlashMessagesLogic); const { initializeGroup, resetGroup } = useActions(GroupLogic); const { - sharedSourcesModalModalVisible, + sharedSourcesModalVisible, manageUsersModalVisible, group: { name }, } = useValues(GroupLogic); - const hasMessages = messages.length > 0; - useEffect(() => { initializeGroup(groupId); return resetGroup; @@ -43,7 +40,7 @@ export const GroupRouter: React.FC = () => { return ( <> - {hasMessages && } + @@ -55,7 +52,7 @@ export const GroupRouter: React.FC = () => { - {sharedSourcesModalModalVisible && } + {sharedSourcesModalVisible && } {manageUsersModalVisible && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx new file mode 100644 index 0000000000000..98f40acb96469 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Groups } from './groups'; + +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { FlashMessages } from '../../../shared/flash_messages'; + +import { AddGroupModal } from './components/add_group_modal'; +import { ClearFiltersLink } from './components/clear_filters_link'; +import { GroupsTable } from './components/groups_table'; +import { TableFilters } from './components/table_filters'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton as EuiLinkButton } from '../../../shared/react_router_helpers'; + +const getSearchResults = jest.fn(); +const openNewGroupModal = jest.fn(); +const resetGroups = jest.fn(); +const setFilterValue = jest.fn(); +const setActivePage = jest.fn(); + +const mockMeta = { + ...DEFAULT_META, + page: { + current: 1, + total_results: 50, + total_pages: 5, + }, +}; + +const mockSuccessMessage = { + type: 'success', + message: 'group added', +}; + +const mockValues = { + groups, + groupsDataLoading: false, + newGroupModalOpen: false, + newGroup: null, + groupListLoading: false, + hasFiltersSet: false, + groupsMeta: mockMeta, + filteredSources: [], + filteredUsers: [], + filterValue: '', + isFederatedAuth: false, +}; + +describe('GroupOverview', () => { + beforeEach(() => { + setMockActions({ + getSearchResults, + openNewGroupModal, + resetGroups, + setFilterValue, + setActivePage, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(GroupsTable)).toHaveLength(1); + expect(wrapper.find(TableFilters)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, groupsDataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('gets search results when filters changed', () => { + const wrapper = shallow(); + + const filters = wrapper.find(TableFilters).dive().shallow(); + const input = filters.find(EuiFieldSearch); + + input.simulate('change', { target: { value: 'Query' } }); + + expect(getSearchResults).toHaveBeenCalledWith(true); + }); + + it('renders manage button when new group added', () => { + setMockValues({ + ...mockValues, + newGroup: { name: 'group', id: '123' }, + messages: [mockSuccessMessage], + }); + const wrapper = shallow(); + const flashMessages = wrapper.find(FlashMessages).dive().shallow(); + + expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1); + }); + + it('renders ClearFiltersLink when filters set', () => { + setMockValues({ + ...mockValues, + hasFiltersSet: true, + groupsMeta: DEFAULT_META, + }); + + const wrapper = shallow(); + + expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); + }); + + it('renders inviteUsersButton when not federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: false, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1); + expect(action.find(EuiLinkButton)).toHaveLength(1); + }); + + it('does not render inviteUsersButton when federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: true, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0); + }); + + it('renders EuiLoadingSpinner when loading', () => { + setMockValues({ + ...mockValues, + groupListLoading: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + }); + + it('renders AddGroupModal', () => { + setMockValues({ + ...mockValues, + newGroupModalOpen: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(AddGroupModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 34a66282a312d..ae87ef735bb9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -19,7 +19,6 @@ import { ViewContentHeader } from '../../components/shared/view_content_header'; import { getGroupPath, USERS_PATH } from '../../routes'; -import { useDidUpdateEffect } from '../../../shared/use_did_update_effect'; import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; import { GroupsLogic } from './groups_logic'; @@ -40,7 +39,7 @@ export const Groups: React.FC = () => { groupListLoading, hasFiltersSet, groupsMeta: { - page: { current: activePage, total_results: numGroups }, + page: { total_results: numGroups }, }, filteredSources, filteredUsers, @@ -56,18 +55,17 @@ export const Groups: React.FC = () => { return resetGroups; }, [filteredSources, filteredUsers, filterValue]); - // Because the initial search happens above, we want to skip the initial search and use the custom hook to do so. - useDidUpdateEffect(() => { - getSearchResults(); - }, [activePage]); - if (groupsDataLoading) { return ; } if (newGroup && hasMessages) { messages[0].description = ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action', { defaultMessage: 'Manage Group', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts new file mode 100644 index 0000000000000..18206573a3978 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -0,0 +1,432 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { FlashMessagesLogic, flashAPIErrors } from '../../../shared/flash_messages'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { JSON_HEADER as headers } from '../../../../../common/constants'; + +import { groups } from '../../__mocks__/groups.mock'; +import { contentSources } from '../../__mocks__/content_sources.mock'; +import { users } from '../../__mocks__/users.mock'; +import { mockGroupsValues } from './__mocks__/groups_logic.mock'; +import { GroupsLogic } from './groups_logic'; + +// We need to mock out the debounced functionality +const TIMEOUT = 400; +const delay = () => new Promise((resolve) => setTimeout(resolve, TIMEOUT)); + +describe('GroupsLogic', () => { + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + const groupsResponse = { + results: groups, + meta: DEFAULT_META, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupsLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupsLogic.values).toEqual(mockGroupsValues); + }); + + describe('actions', () => { + describe('onInitializeGroups', () => { + it('sets reducers', () => { + GroupsLogic.actions.onInitializeGroups({ contentSources, users }); + + expect(GroupsLogic.values.groupsDataLoading).toEqual(false); + expect(GroupsLogic.values.contentSources).toEqual(contentSources); + expect(GroupsLogic.values.users).toEqual(users); + }); + }); + + describe('setSearchResults', () => { + it('sets reducers', () => { + GroupsLogic.actions.setSearchResults(groupsResponse); + + expect(GroupsLogic.values.groups).toEqual(groups); + expect(GroupsLogic.values.groupListLoading).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('addFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + GroupsLogic.actions.removeFilteredSource('foo'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz']); + }); + }); + + describe('addFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + GroupsLogic.actions.removeFilteredUser('foo'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz']); + }); + }); + + describe('setGroupUsers', () => { + it('sets reducers', () => { + GroupsLogic.actions.setGroupUsers(users); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(false); + expect(GroupsLogic.values.allGroupUsers).toEqual(users); + }); + }); + + describe('setAllGroupLoading', () => { + it('sets reducer', () => { + GroupsLogic.actions.setAllGroupLoading(true); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(true); + expect(GroupsLogic.values.allGroupUsers).toEqual([]); + }); + }); + + describe('setFilterValue', () => { + it('sets reducer', () => { + GroupsLogic.actions.setFilterValue('foo'); + + expect(GroupsLogic.values.filterValue).toEqual('foo'); + }); + }); + + describe('setNewGroupName', () => { + it('sets reducer', () => { + const NEW_NAME = 'new name'; + GroupsLogic.actions.setNewGroupName(NEW_NAME); + + expect(GroupsLogic.values.newGroupName).toEqual(NEW_NAME); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('setNewGroup', () => { + it('sets reducer', () => { + GroupsLogic.actions.setNewGroup(groups[0]); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroup).toEqual(groups[0]); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('setNewGroupFormErrors', () => { + it('sets reducer', () => { + const errors = ['this is an error']; + GroupsLogic.actions.setNewGroupFormErrors(errors); + + expect(GroupsLogic.values.newGroupNameErrors).toEqual(errors); + }); + }); + + describe('closeNewGroupModal', () => { + it('sets reducer', () => { + GroupsLogic.actions.closeNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('closeFilterSourcesDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterSourcesDropdown(); + GroupsLogic.actions.closeFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(false); + }); + }); + + describe('closeFilterUsersDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterUsersDropdown(); + GroupsLogic.actions.closeFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(false); + }); + }); + + describe('setGroupsLoading', () => { + it('sets reducer', () => { + // Set to false first + GroupsLogic.actions.setSearchResults(groupsResponse); + GroupsLogic.actions.setGroupsLoading(); + + expect(GroupsLogic.values.groupListLoading).toEqual(true); + }); + }); + + describe('resetGroups', () => { + it('sets reducer', () => { + GroupsLogic.actions.resetGroups(); + + expect(GroupsLogic.values.newGroup).toEqual(null); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroups', () => { + it('calls API and sets values', async () => { + const onInitializeGroupsSpy = jest.spyOn(GroupsLogic.actions, 'onInitializeGroups'); + const promise = Promise.resolve(groupsResponse); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups'); + await promise; + expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('getSearchResults', () => { + const search = { + query: '', + content_source_ids: [], + user_ids: [], + }; + + const payload = { + body: JSON.stringify({ + page: { + current: 1, + size: 10, + }, + search, + }), + headers, + }; + + it('calls API and sets values', async () => { + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('calls API and resets pagination', async () => { + // Set active page to 2 to confirm resetting sends the `payload` value of 1 for the current page. + GroupsLogic.actions.setActivePage(2); + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(true); + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + try { + await promise; + } catch { + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('fetchGroupUsers', () => { + it('calls API and sets values', async () => { + const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); + const promise = Promise.resolve(users); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/group_users' + ); + await promise; + expect(setGroupUsersSpy).toHaveBeenCalledWith(users); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveNewGroup', () => { + it('calls API and sets values', async () => { + const GROUP_NAME = 'new group'; + GroupsLogic.actions.setNewGroupName(GROUP_NAME); + const setNewGroupSpy = jest.spyOn(GroupsLogic.actions, 'setNewGroup'); + const promise = Promise.resolve(groups[0]); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith('/api/workplace_search/groups', { + body: JSON.stringify({ group_name: GROUP_NAME }), + headers, + }); + await promise; + expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('setActivePage', () => { + it('sets reducer', () => { + const getSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'getSearchResults'); + const activePage = 3; + GroupsLogic.actions.setActivePage(activePage); + + expect(GroupsLogic.values.groupsMeta).toEqual({ + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: activePage, + }, + }); + + expect(getSearchResultsSpy).toHaveBeenCalled(); + }); + }); + + describe('openNewGroupModal', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.openNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(true); + expect(GroupsLogic.values.newGroup).toEqual(null); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetGroupsFilters', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.resetGroupsFilters(); + + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.filterValue).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterSourcesDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterUsersDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index 35d4387b4cf3d..685a2651cb24a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -335,6 +335,9 @@ export const GroupsLogic = kea>({ flashAPIErrors(e); } }, + setActivePage: () => { + actions.getSearchResults(); + }, openNewGroupModal: () => { FlashMessagesLogic.actions.clearFlashMessages(); }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx new file mode 100644 index 0000000000000..0b2b1ad05dfd7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { GroupsRouter } from './groups_router'; + +import { GroupRouter } from './group_router'; +import { Groups } from './groups'; + +describe('GroupsRouter', () => { + const initializeGroups = jest.fn(); + + beforeEach(() => { + setMockActions({ initializeGroups }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupRouter)).toHaveLength(1); + expect(wrapper.find(Groups)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index adfa10d37c524..a4fe472065d90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -37,7 +37,9 @@ export const GroupsRouter: React.FC = () => { - + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index d9af20763657b..c7664f2d837b9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -792,7 +792,7 @@ describe('edit policy', () => { httpRequestsMockHelpers.setPoliciesResponse(policies); }); - describe('with legacy data role config', () => { + describe('with deprecated data role config', () => { test('should hide data tier option on cloud using legacy node role configuration', async () => { http.setupNodeListResponse({ nodesByAttributes: { test: ['123'] }, @@ -830,6 +830,8 @@ describe('edit policy', () => { expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); }); test('should show cloud notice when cold tier nodes do not exist', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx index 2dff55ac10de1..fc87b553ba521 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx @@ -5,22 +5,50 @@ */ import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { useKibana } from '../../../../../shared_imports'; + +const deployment = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body.elasticDeploymentLink', + { + defaultMessage: 'deployment', + } +); const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.coldTierTitle', { defaultMessage: 'Create a cold tier', }), - body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', - }), + body: (deploymentUrl?: string) => { + return ( + + {deployment} + + ) : ( + deployment + ), + }} + /> + ); + }, }; export const CloudDataTierCallout: FunctionComponent = () => { + const { + services: { cloud }, + } = useKibana(); + return ( - {i18nTexts.body} + {i18nTexts.body(cloud?.cloudDeploymentUrl)} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx index de7f321e5f15d..df59efcbfd299 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx @@ -61,20 +61,19 @@ export const DataTierAllocationField: FunctionComponent = ({ switch (phaseData.dataTierAllocationType) { case 'default': const isCloudEnabled = cloud?.isCloudEnabled ?? false; - const isUsingNodeRoles = !isUsingDeprecatedDataRoleConfig; - if ( - isCloudEnabled && - isUsingNodeRoles && - phase === 'cold' && - !nodesByRoles.data_cold?.length - ) { - // Tell cloud users they can deploy cold tier nodes. - return ( - <> - - - - ); + if (isCloudEnabled && phase === 'cold') { + const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig; + const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; + + if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { + // Tell cloud users they can deploy nodes on cloud. + return ( + <> + + + + ); + } } const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx new file mode 100644 index 0000000000000..3e1a8ae8f698e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/date_range_datatype.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; + +// Parameters automatically added to the date range datatype when saved (with the default values) +export const defaultDateRangeParameters = { + type: 'date_range', + coerce: true, + index: true, + store: false, +}; + +describe('Mappings editor: date range datatype', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + + test('should require a scaling factor to be provided', async () => { + const defaultMappings = { + properties: { + myField: { + type: 'double_range', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + + const { + component, + find, + exists, + actions: { startEditField, updateFieldAndCloseFlyout, toggleFormRow }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + + expect(exists('formatParameter')).toBe(false); + + // Change the type to "date_range" + await act(async () => { + find('mappingsEditorFieldEdit.fieldSubType').simulate('change', [ + { + label: 'Date range', + value: 'date_range', + }, + ]); + }); + component.update(); + + expect(exists('formatParameter')).toBe(true); + expect(exists('formatParameter.formatInput')).toBe(false); + toggleFormRow('formatParameter'); + expect(exists('formatParameter.formatInput')).toBe(true); + + await act(async () => { + find('formatParameter.formatInput').simulate('change', [{ label: 'customDateFormat' }]); + }); + component.update(); + + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added, plus the scaling factor + updatedMappings.properties.myField = { + ...defaultDateRangeParameters, + format: 'customDateFormat', + } as any; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx new file mode 100644 index 0000000000000..117695a43e6b4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; + +// Parameters automatically added to the scaled float datatype when saved (with the default values) +export const defaultScaledFloatParameters = { + type: 'scaled_float', + coerce: true, + doc_values: true, + ignore_malformed: false, + index: true, + store: false, +}; + +describe('Mappings editor: scaled float datatype', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + + test('should require a scaling factor to be provided', async () => { + const defaultMappings = { + properties: { + myField: { + type: 'byte', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + + const { + component, + find, + exists, + form, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + + // Change the type to "scaled_float" + await act(async () => { + find('mappingsEditorFieldEdit.fieldSubType').simulate('change', [ + { + label: 'Scaled float', + value: 'scaled_float', + }, + ]); + }); + component.update(); + + // It should **not** allow to save as the "scaling factor" parameter has not been set + await updateFieldAndCloseFlyout(); + expect(exists('mappingsEditorFieldEdit')).toBe(true); + expect(form.getErrorsMessages()).toEqual(['A scaling factor is required.']); + + await act(async () => { + form.setInputValue('scalingFactor.input', '123'); + }); + await updateFieldAndCloseFlyout(); + expect(exists('mappingsEditorFieldEdit')).toBe(false); + + // It should have the default parameters values added, plus the scaling factor + updatedMappings.properties.myField = { + ...defaultScaledFloatParameters, + scaling_factor: 123, + } as any; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 2eb56a97dc3a0..a625cc8c0ab4b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -358,9 +358,11 @@ export type TestSubjects = | 'toggleExpandButton' | 'createFieldForm' | 'createFieldForm.fieldType' + | 'createFieldForm.fieldSubType' | 'createFieldForm.addButton' | 'mappingsEditorFieldEdit' | 'mappingsEditorFieldEdit.fieldType' + | 'mappingsEditorFieldEdit.fieldSubType' | 'mappingsEditorFieldEdit.editFieldUpdateButton' | 'mappingsEditorFieldEdit.flyoutTitle' | 'mappingsEditorFieldEdit.documentationLink' @@ -383,4 +385,7 @@ export type TestSubjects = | 'searchQuoteAnalyzer-custom.input' | 'useSameAnalyzerForSearchCheckBox.input' | 'metaParameterEditor' + | 'scalingFactor.input' + | 'formatParameter' + | 'formatParameter.formatInput' | string; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx index c2fe37559ae51..69e7a0e88bb16 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx @@ -55,6 +55,7 @@ export const FormatParameter = ({ defaultValue, defaultToggleValue }: Props) => href: documentationService.getFormatLink(), }} defaultToggleValue={defaultToggleValue} + data-test-subj="formatParameter" > {(formatField) => { @@ -81,6 +82,7 @@ export const FormatParameter = ({ defaultValue, defaultToggleValue }: Props) => setComboBoxOptions([...comboBoxOptions, newOption]); }} fullWidth + data-test-subj="formatInput" /> ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx index cfa8b60653d4c..9ad0f25d2a4ec 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/subtype_parameter.tsx @@ -85,6 +85,7 @@ export const SubTypeParameter = ({ selectedOptions={subTypeField.value as ComboBoxOption[]} onChange={subTypeField.setValue} isClearable={false} + data-test-subj="fieldSubType" /> ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 3a02a4db5f4c9..2bdb6f10d65f3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -60,6 +60,7 @@ export const NumericType = ({ field }: Props) => { path="scaling_factor" config={getFieldConfig('scaling_factor')} component={Field} + data-test-subj="scalingFactor" /> ) : null; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index b2664d5a6d9fe..ee71167b5a1c7 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -58,7 +58,14 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); - const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); + const info = await getNodeInfo( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index d0f0bd18b5d56..f1341c7ec8101 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -20,7 +20,8 @@ export const getNodeInfo = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: InventoryItemType + nodeType: InventoryItemType, + timeRange: { from: number; to: number } ): Promise => { // If the nodeType is a Kubernetes pod then we need to get the node info // from a host record instead of a pod. This is due to the fact that any host @@ -33,7 +34,8 @@ export const getNodeInfo = async ( requestContext, sourceConfiguration, nodeId, - nodeType + nodeType, + timeRange ); if (kubernetesNodeName) { return getNodeInfo( @@ -41,12 +43,14 @@ export const getNodeInfo = async ( requestContext, sourceConfiguration, kubernetesNodeName, - 'host' + 'host', + timeRange ); } return {}; } const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const timestampField = sourceConfiguration.fields.timestamp; const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -55,9 +59,21 @@ export const getNodeInfo = async ( body: { size: 1, _source: ['host.*', 'cloud.*'], + sort: [{ [timestampField]: 'desc' }], query: { bool: { - filter: [{ match: { [fields.id]: nodeId } }], + filter: [ + { match: { [fields.id]: nodeId } }, + { + range: { + [timestampField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, + ], }, }, }, diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index be6e29a794d09..b4656178d395e 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -15,9 +15,11 @@ export const getPodNodeName = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: 'host' | 'pod' | 'container', + timeRange: { from: number; to: number } ): Promise => { const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const timestampField = sourceConfiguration.fields.timestamp; const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -26,11 +28,21 @@ export const getPodNodeName = async ( body: { size: 1, _source: ['kubernetes.node.name'], + sort: [{ [timestampField]: 'desc' }], query: { bool: { filter: [ { match: { [fields.id]: nodeId } }, { exists: { field: `kubernetes.node.name` } }, + { + range: { + [timestampField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts index aa9fbc20fc0b0..5fe72fd57f3ed 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts @@ -63,6 +63,7 @@ export interface DeleteAgentPolicyRequest { export interface DeleteAgentPolicyResponse { id: string; + name: string; } export interface GetFullAgentPolicyRequest { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts index 61669ab876d80..171902471e178 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts @@ -52,5 +52,6 @@ export interface DeletePackagePoliciesRequest { export type DeletePackagePoliciesResponse = Array<{ id: string; + name?: string; success: boolean; }>; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx index 0266b4a90abe4..ab8a9c99227c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -63,7 +63,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle', { defaultMessage: "Deleted agent policy '{id}'", - values: { id: agentPolicy }, + values: { id: data.name || data.id }, }) ); if (onSuccessCallback.current) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx index 9242c6eb86225..ff5961b94f726 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -106,7 +106,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ 'xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle', { defaultMessage: "Deleted integration '{id}'", - values: { id: successfulResults[0].id }, + values: { id: successfulResults[0].name || successfulResults[0].id }, } ); notifications.toasts.addSuccess(successMessage); 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 83cbb9ccb728c..bc37338f04394 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 @@ -173,6 +173,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent data states const [showInactive, setShowInactive] = useState(false); const [showUpgradeable, setShowUpgradeable] = useState(false); + // Table and search states const [search, setSearch] = useState(defaultKuery); const [selectionMode, setSelectionMode] = useState('manual'); @@ -188,11 +189,20 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); const [selectedStatus, setSelectedStatus] = useState([]); + const isUsingFilter = + search.trim() || + selectedAgentPolicies.length || + selectedStatus.length || + showInactive || + showUpgradeable; + const clearFilters = useCallback(() => { setSearch(''); setSelectedAgentPolicies([]); setSelectedStatus([]); - }, [setSearch, setSelectedAgentPolicies, setSelectedStatus]); + setShowInactive(false); + setShowUpgradeable(false); + }, [setSearch, setSelectedAgentPolicies, setSelectedStatus, setShowInactive, setShowUpgradeable]); // Add a agent policy id to current search const addAgentPolicyFilter = (policyId: string) => { @@ -638,7 +648,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { id="xpack.ingestManager.agentList.loadingAgentsMessage" defaultMessage="Loading agents…" /> - ) : search.trim() || selectedAgentPolicies.length || selectedStatus.length ? ( + ) : isUsingFilter ? ( ({ appContextService: { + getInternalUserSOClient: () => { + return {}; + }, getEncryptedSavedObjects: () => ({ getDecryptedAsInternalUser: () => ({ attributes: { @@ -19,7 +27,83 @@ jest.mock('../../app_context', () => ({ }, })); +jest.mock('../actions'); + +jest.useFakeTimers(); + +function waitForPromiseResolved() { + return new Promise((resolve) => setImmediate(resolve)); +} + +function getMockedNewActionSince() { + return getNewActionsSince as jest.MockedFunction; +} + describe('test agent checkin new action services', () => { + describe('newAgetActionObservable', () => { + beforeEach(() => { + (getNewActionsSince as jest.MockedFunction).mockReset(); + }); + it('should work, call get actions until there is new action', async () => { + const observable = createNewActionsSharedObservable(); + + getMockedNewActionSince() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + ({ id: 'action1', created_at: new Date().toISOString() } as unknown) as AgentAction, + ]) + .mockResolvedValueOnce([ + ({ id: 'action2', created_at: new Date().toISOString() } as unknown) as AgentAction, + ]); + // First call + const promise = observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + const res = await promise; + expect(getNewActionsSince).toBeCalledTimes(2); + expect(res).toHaveLength(1); + expect(res[0].id).toBe('action1'); + // Second call + const secondSubscription = observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + const secondRes = await secondSubscription; + expect(secondRes).toHaveLength(1); + expect(secondRes[0].id).toBe('action2'); + expect(getNewActionsSince).toBeCalledTimes(3); + // It should call getNewActionsSince with the last action returned + expect(getMockedNewActionSince().mock.calls[2][1]).toBe(res[0].created_at); + }); + + it('should not fetch actions concurrently', async () => { + const observable = createNewActionsSharedObservable(); + + const resolves: Array<() => void> = []; + getMockedNewActionSince().mockImplementation(() => { + return new Promise((resolve) => { + resolves.push(resolve); + }); + }); + + observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + expect(getNewActionsSince).toBeCalledTimes(1); + }); + }); + describe('createAgentActionFromPolicyAction()', () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); const mockAgent: Agent = { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 1871cd2cb04f6..aa48d8fe18e9f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -12,6 +12,7 @@ import { share, distinctUntilKeyChanged, switchMap, + exhaustMap, concatMap, merge, filter, @@ -62,18 +63,28 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } -function createNewActionsSharedObservable(): Observable { +export function createNewActionsSharedObservable(): Observable { let lastTimestamp = new Date().toISOString(); return timer(0, AGENT_UPDATE_ACTIONS_INTERVAL_MS).pipe( - switchMap(() => { + exhaustMap(() => { const internalSOClient = getInternalUserSOClient(); - const timestamp = lastTimestamp; - lastTimestamp = new Date().toISOString(); - return from(getNewActionsSince(internalSOClient, timestamp)); + return from( + getNewActionsSince(internalSOClient, lastTimestamp).then((data) => { + if (data.length > 0) { + lastTimestamp = data.reduce((acc, action) => { + return acc >= action.created_at ? acc : action.created_at; + }, lastTimestamp); + } + + return data; + }) + ); + }), + filter((data) => { + return data.length > 0; }), - filter((data) => data.length > 0), share() ); } diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts index d91f6e8580fc3..dc3a4495191c9 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts @@ -286,15 +286,15 @@ class PackagePolicyService { for (const id of ids) { try { - const oldPackagePolicy = await this.get(soClient, id); - if (!oldPackagePolicy) { + const packagePolicy = await this.get(soClient, id); + if (!packagePolicy) { throw new Error('Package policy not found'); } if (!options?.skipUnassignFromAgentPolicies) { await agentPolicyService.unassignPackagePolicies( soClient, - oldPackagePolicy.policy_id, - [oldPackagePolicy.id], + packagePolicy.policy_id, + [packagePolicy.id], { user: options?.user, } @@ -303,6 +303,7 @@ class PackagePolicyService { await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, + name: packagePolicy.name, success: true, }); } catch (e) { diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index b7a07e98437a1..57f83b9533bda 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -379,11 +379,13 @@ export function removeSelectedLayer() { ) => { const state = getState(); const layerId = getSelectedLayerId(state); - dispatch(removeLayer(layerId)); + if (layerId) { + dispatch(removeLayer(layerId)); + } }; } -export function removeLayer(layerId: string | null) { +export function removeLayer(layerId: string) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -398,7 +400,7 @@ export function removeLayer(layerId: string | null) { }; } -function removeLayerFromLayerList(layerId: string | null) { +function removeLayerFromLayerList(layerId: string) { return ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -411,7 +413,7 @@ function removeLayerFromLayerList(layerId: string | null) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(cleanTooltipStateForLayer(layerId!)); + dispatch(cleanTooltipStateForLayer(layerId)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index 58621b0a70e04..cbb6f0a4054cc 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -27,6 +27,10 @@ describe('map_actions', () => { require('../selectors/map_selectors').getDataFilters = () => { return {}; }; + + require('../selectors/map_selectors').getLayerList = () => { + return []; + }; }); it('should add newMapConstants to dispatch action mapState', async () => { diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 4efdd3dda344e..4e76bb24c9e34 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -18,6 +18,7 @@ import { getWaitingForMapReadyLayerListRaw, getQuery, getTimeFilters, + getLayerList, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -56,6 +57,7 @@ import { } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { scaleBounds } from '../../common/elasticsearch_util'; +import { cleanTooltipStateForLayer } from './tooltip_actions'; export function setMapInitError(errorMessage: string) { return { @@ -128,8 +130,7 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt dispatch: ThunkDispatch, getState: () => MapStoreState ) => { - const state = getState(); - const dataFilters = getDataFilters(state); + const dataFilters = getDataFilters(getState()); const { extent, zoom: newZoom } = newMapConstants; const { buffer, zoom: currentZoom } = dataFilters; @@ -164,6 +165,15 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt ...newMapConstants, }, }); + + if (currentZoom !== newZoom) { + getLayerList(getState()).map((layer) => { + if (!layer.showAtZoomLevel(newZoom)) { + dispatch(cleanTooltipStateForLayer(layer.getId())); + } + }); + } + await dispatch(syncDataForAllLayers()); }; } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index 12451e04efbc3..98a121e6be7a3 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -72,7 +72,7 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function cleanTooltipStateForLayer(layerId: string | null, layerFeatures: Feature[] = []) { +export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { return (dispatch: Dispatch, getState: () => MapStoreState) => { let featuresRemoved = false; const openTooltips = getOpenTooltips(getState()) 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 7747fe5746c29..85bd5618300fe 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 @@ -18,6 +18,7 @@ import { EuiLink, EuiSelect, EuiSpacer, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -138,7 +139,7 @@ export const AdvancedStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setEstimatedModelMemoryLimit, setFormState } = actions; - const { form, isJobCreated } = state; + const { form, isJobCreated, estimatedModelMemoryLimit } = state; const { computeFeatureInfluence, eta, @@ -159,6 +160,7 @@ export const AdvancedStepForm: FC = ({ outlierFraction, predictionFieldName, randomizeSeed, + useEstimatedMml, } = form; const [numTopClassesOptions, setNumTopClassesOptions] = useState([ @@ -204,7 +206,9 @@ export const AdvancedStepForm: FC = ({ if (success) { if (modelMemoryLimit !== expectedMemory) { setEstimatedModelMemoryLimit(expectedMemory); - setFormState({ modelMemoryLimit: expectedMemory }); + if (useEstimatedMml === true) { + setFormState({ modelMemoryLimit: expectedMemory }); + } } } else { // Check which field is invalid @@ -481,18 +485,35 @@ export const AdvancedStepForm: FC = ({ } )} > - setFormState({ modelMemoryLimit: e.target.value })} - isInvalid={modelMemoryLimitValidationResult !== null} - data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" - /> + <> + setFormState({ modelMemoryLimit: e.target.value })} + isInvalid={modelMemoryLimitValidationResult !== null} + data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" + /> + + + setFormState({ + useEstimatedMml: !useEstimatedMml, + }) + } + data-test-subj="mlAnalyticsCreateJobWizardUseEstimatedMml" + /> + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index dd9ecc963840a..9c166f32f1d34 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -91,6 +91,7 @@ export const ConfigurationStepForm: FC = ({ requiredFieldsError, sourceIndex, trainingPercent, + useEstimatedMml, } = form; const toastNotifications = getToastNotifications(); @@ -164,7 +165,8 @@ export const ConfigurationStepForm: FC = ({ const debouncedGetExplainData = debounce(async () => { const jobTypeChanged = previousJobType !== jobType; - const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateModelMemoryLimit = + (!firstUpdate.current || !modelMemoryLimit) && useEstimatedMml === true; const shouldUpdateEstimatedMml = !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; 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 898bd7aa183cd..533a5eb6f5c34 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 @@ -93,6 +93,7 @@ export interface State { sourceIndexFieldsCheckFailed: boolean; standardizationEnabled: undefined | string; trainingPercent: number; + useEstimatedMml: boolean; }; disabled: boolean; indexPatternsMap: SourceIndexMap; @@ -161,6 +162,7 @@ export const getInitialState = (): State => ({ sourceIndexFieldsCheckFailed: false, standardizationEnabled: 'true', trainingPercent: 80, + useEstimatedMml: true, }, jobConfig: {}, disabled: diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index d0ca1bc6bbde6..ef97d78b4f745 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -25,7 +25,7 @@ let once = false; let inTransit = false; export function monitoringClustersProvider($injector) { - return (clusterUuid, ccs, codePaths) => { + return async (clusterUuid, ccs, codePaths) => { const { min, max } = Legacy.shims.timefilter.getBounds(); // append clusterUuid if the parameter is given @@ -36,74 +36,73 @@ export function monitoringClustersProvider($injector) { const $http = $injector.get('$http'); - function getClusters() { - return $http - .post(url, { + async function getClusters() { + try { + const response = await $http.post(url, { ccs, timeRange: { min: min.toISOString(), max: max.toISOString(), }, codePaths, - }) - .then((response) => response.data) - .then((data) => { - return formatClusters(data); // return set of clusters - }) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); }); + return formatClusters(response.data); + } catch (err) { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + } } - function ensureAlertsEnabled() { - return $http.post('../api/monitoring/v1/alerts/enable', {}).catch((err) => { + async function ensureAlertsEnabled() { + try { + return $http.post('../api/monitoring/v1/alerts/enable', {}); + } catch (err) { const Private = $injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); - }); + } } - function ensureMetricbeatEnabled() { + async function ensureMetricbeatEnabled() { if (Legacy.shims.isCloud) { - return Promise.resolve(); + return; } const globalState = $injector.get('globalState'); - return $http - .post('../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', { - ccs: globalState.ccs, - }) - .then(({ data }) => { - showInternalMonitoringToast({ - legacyIndices: data.legacy_indices, - metricbeatIndices: data.mb_indices, - }); - }) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); + try { + const response = await $http.post( + '../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', + { + ccs: globalState.ccs, + } + ); + const { data } = response; + showInternalMonitoringToast({ + legacyIndices: data.legacy_indices, + metricbeatIndices: data.mb_indices, }); + } catch (err) { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + } } if (!once && !inTransit) { inTransit = true; - return getClusters().then((clusters) => { - if (clusters.length) { - Promise.all([ensureAlertsEnabled(), ensureMetricbeatEnabled()]) - .then(([{ data }]) => { - showSecurityToast(data); - once = true; - }) - .catch(() => { - // Intentionally swallow the error as this will retry the next page load - }) - .finally(() => (inTransit = false)); + const clusters = await getClusters(); + if (clusters.length) { + try { + const [{ data }] = await Promise.all([ensureAlertsEnabled(), ensureMetricbeatEnabled()]); + showSecurityToast(data); + once = true; + } catch (_err) { + // Intentionally swallow the error as this will retry the next page load } - return clusters; - }); + inTransit = false; + } + return clusters; } - return getClusters(); + return await getClusters(); }; } diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index 4341ada00da8b..e69d572f9560b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -63,7 +63,7 @@ uiRoutes.when('/elasticsearch/nodes', { const promise = globalState.cluster_uuid ? getNodes() - : new Promise((resolve) => resolve({})); + : new Promise((resolve) => resolve({ data: {} })); return promise .then((response) => response.data) .catch((err) => { diff --git a/x-pack/plugins/monitoring/public/views/loading/index.js b/x-pack/plugins/monitoring/public/views/loading/index.js index 5ca523899067d..b5d79e1d2b652 100644 --- a/x-pack/plugins/monitoring/public/views/loading/index.js +++ b/x-pack/plugins/monitoring/public/views/loading/index.js @@ -53,15 +53,18 @@ uiRoutes.when('/loading', { (clusters) => { if (!clusters || !clusters.length) { window.location.hash = '#/no-data'; + $scope.$apply(); return; } if (clusters.length === 1) { // Bypass the cluster listing if there is just 1 cluster window.history.replaceState(null, null, '#/overview'); + $scope.$apply(); return; } window.history.replaceState(null, null, '#/home'); + $scope.$apply(); } ); } diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts index e3d69820ebb05..5605641992e1a 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts @@ -36,7 +36,7 @@ describe('DiskUsageAlert', () => { expect(alert.type).toBe(ALERT_DISK_USAGE); expect(alert.label).toBe('Disk Usage'); expect(alert.defaultThrottle).toBe('1d'); - expect(alert.defaultParams).toStrictEqual({ threshold: 90, duration: '5m' }); + expect(alert.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); expect(alert.actionVariables).toStrictEqual([ { name: 'nodes', description: 'The list of nodes reporting high disk usage.' }, { name: 'count', description: 'The number of nodes reporting high disk usage.' }, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index c577550de8617..34c640de79625 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -54,7 +54,7 @@ export class DiskUsageAlert extends BaseAlert { public label = DiskUsageAlert.LABEL; protected defaultParams = { - threshold: 90, + threshold: 80, duration: '5m', }; diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 6ed237a055b5c..57d01dc6a1100 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -22,9 +22,9 @@ describe('MissingMonitoringDataAlert', () => { const alert = new MissingMonitoringDataAlert(); expect(alert.type).toBe(ALERT_MISSING_MONITORING_DATA); expect(alert.label).toBe('Missing monitoring data'); - expect(alert.defaultThrottle).toBe('1d'); + expect(alert.defaultThrottle).toBe('6h'); // @ts-ignore - expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' }); + expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { name: 'stackProducts', description: 'The stack products missing monitoring data.' }, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 252d005f1e4a6..5b4542a4439ca 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -50,7 +50,7 @@ const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', { defaultMessage: 'firing', }); -const DEFAULT_DURATION = '5m'; +const DEFAULT_DURATION = '15m'; const DEFAULT_LIMIT = '1d'; // Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back @@ -77,6 +77,8 @@ export class MissingMonitoringDataAlert extends BaseAlert { } as CommonAlertParamDetail, }; + public defaultThrottle: string = '6h'; + public type = ALERT_MISSING_MONITORING_DATA; public label = i18n.translate('xpack.monitoring.alerts.missingData.label', { defaultMessage: 'Missing monitoring data', diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index dfb335902b7b8..2d3142d4e5804 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -64,7 +64,7 @@ export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime } /> )} - {hasData?.ux && ( + {(hasData.ux as UXHasDataResponse).hasData && ( ; @@ -88,6 +88,8 @@ export function OverviewPage({ routeParams }: Props) { const appEmptySections = getEmptySections({ core }).filter(({ id }) => { if (id === 'alert') { return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; + } else if (id === 'ux') { + return !(hasData[id] as UXHasDataResponse).hasData; } return !hasData[id]; }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 2cf5b69835d1f..39f597acec6ec 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -12,8 +12,7 @@ import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; import { ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; -// @ts-ignore untyped module -import { pdf } from './pdf'; +import { PdfMaker } from './pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { @@ -58,7 +57,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.endScreenshots(); tracker.startSetup(); - const pdfOutput = pdf.create(layout, logo); + const pdfOutput = new PdfMaker(layout, logo); if (title) { const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange}` : ''; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index 9f7e9310333ba..fe687c3f47327 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -31,7 +31,7 @@ test(`gets logo from uiSettings`, async () => { }; const mockGet = jest.fn(); - mockGet.mockImplementationOnce((...args: any[]) => { + mockGet.mockImplementationOnce((...args: string[]) => { if (args[0] === 'xpackReporting:customPdfLogo') { return 'purple pony'; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts new file mode 100644 index 0000000000000..5a3671835ce51 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BufferOptions } from 'pdfmake/interfaces'; + +export function getDocOptions(tableBorderWidth: number): BufferOptions { + return { + tableLayouts: { + noBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + simpleBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => tableBorderWidth, + vLineWidth: () => tableBorderWidth, + hLineColor: () => 'silver', + vLineColor: () => 'silver', + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts new file mode 100644 index 0000000000000..5cd6136153f04 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore: no module definition +import xRegExp from 'xregexp'; + +export function getFont(text: string) { + // Once unicode regex scripts are fully supported we should be able to get rid of the dependency + // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes + // for more information. We are matching Han characters which is one of the supported unicode scripts + // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). + // This will match Chinese, Japanese, Korean and some other Asian languages. + const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); + if (isCKJ) { + return 'noto-cjk'; + } else { + return 'Roboto'; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts new file mode 100644 index 0000000000000..131d289576384 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.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 { i18n } from '@kbn/i18n'; +import path from 'path'; +import { TDocumentDefinitions } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getFont } from './get_font'; + +export function getTemplate( + layout: LayoutInstance, + logo: string | undefined, + title: string, + tableBorderWidth: number, + assetPath: string +): Partial { + const pageMarginTop = 40; + const pageMarginBottom = 80; + const pageMarginWidth = 40; + const headingFontSize = 14; + const headingMarginTop = 10; + const headingMarginBottom = 5; + const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; + const subheadingFontSize = 12; + const subheadingMarginTop = 0; + const subheadingMarginBottom = 5; + const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; + + return { + // define page size + pageOrientation: layout.getPdfPageOrientation(), + pageSize: layout.getPdfPageSize({ + pageMarginTop, + pageMarginBottom, + pageMarginWidth, + tableBorderWidth, + headingHeight, + subheadingHeight, + }), + pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], + + header() { + return { + margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], + text: title, + font: getFont(title), + style: { + color: '#aaa', + }, + fontSize: 10, + alignment: 'center', + }; + }, + + footer(currentPage: number, pageCount: number) { + const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo + return { + margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], + layout: 'noBorder', + table: { + widths: [100, '*', 100], + body: [ + [ + { + fit: [100, 35], + image: logo || logoPath, + }, + { + alignment: 'center', + text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { + defaultMessage: 'Page {currentPage} of {pageCount}', + values: { currentPage: currentPage.toString(), pageCount }, + }), + style: { + color: '#aaa', + }, + }, + '', + ], + [ + logo + ? { + text: i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.logoDescription', + { + defaultMessage: 'Powered by Elastic', + } + ), + fontSize: 10, + style: { + color: '#aaa', + }, + margin: [0, 2, 0, 0], + } + : '', + '', + '', + ], + ], + }, + }; + }, + + styles: { + heading: { + alignment: 'left', + fontSize: headingFontSize, + bold: true, + margin: [headingMarginTop, 0, headingMarginBottom, 0], + }, + subheading: { + alignment: 'left', + fontSize: subheadingFontSize, + italics: true, + margin: [0, 0, subheadingMarginBottom, 20], + }, + warning: { + color: '#f39c12', // same as @brand-warning in Kibana colors.less + }, + }, + + defaultStyle: { + fontSize: 12, + font: 'Roboto', + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js deleted file mode 100644 index 8840fd524f3e4..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ /dev/null @@ -1,319 +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 path from 'path'; -import _ from 'lodash'; -import concat from 'concat-stream'; -import Printer from 'pdfmake'; -import xRegExp from 'xregexp'; -import { i18n } from '@kbn/i18n'; - -const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); - -const tableBorderWidth = 1; - -function getFont(text) { - // Once unicode regex scripts are fully supported we should be able to get rid of the dependency - // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes - // for more information. We are matching Han characters which is one of the supported unicode scripts - // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). - // This will match Chinese, Japanese, Korean and some other Asian languages. - const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); - if (isCKJ) { - return 'noto-cjk'; - } else { - return 'Roboto'; - } -} - -class PdfMaker { - constructor(layout, logo) { - const fontPath = (filename) => path.resolve(assetPath, 'fonts', filename); - const fonts = { - Roboto: { - normal: fontPath('roboto/Roboto-Regular.ttf'), - bold: fontPath('roboto/Roboto-Medium.ttf'), - italics: fontPath('roboto/Roboto-Italic.ttf'), - bolditalics: fontPath('roboto/Roboto-Italic.ttf'), - }, - 'noto-cjk': { - // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. - normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - }, - }; - - this._layout = layout; - this._logo = logo; - this._title = ''; - this._content = []; - this._printer = new Printer(fonts); - } - - _addContents(contents) { - const groupCount = this._content.length; - - // inject a page break for every 2 groups on the page - if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { - contents = [ - { - text: '', - pageBreak: 'after', - }, - ].concat(contents); - } - this._content.push(contents); - } - - addImage(base64EncodedData, { title = '', description = '' }) { - const contents = []; - - if (title && title.length > 0) { - contents.push({ - text: title, - style: 'heading', - font: getFont(title), - noWrap: true, - }); - } - - if (description && description.length > 0) { - contents.push({ - text: description, - style: 'subheading', - font: getFont(description), - noWrap: true, - }); - } - - const img = { - image: `data:image/png;base64,${base64EncodedData}`, - alignment: 'center', - }; - - const size = this._layout.getPdfImageSize(); - img.height = size.height; - img.width = size.width; - - const wrappedImg = { - table: { - body: [[img]], - }, - layout: 'noBorder', - }; - - contents.push(wrappedImg); - - this._addContents(contents); - } - - addHeading(headingText, opts = {}) { - const contents = []; - contents.push({ - text: headingText, - style: ['heading'].concat(opts.styles || []), - font: getFont(headingText), - }); - this._addContents(contents); - } - - setTitle(title) { - this._title = title; - } - - generate() { - const docTemplate = _.assign(getTemplate(this._layout, this._logo, this._title), { - content: this._content, - }); - this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions()); - return this; - } - - getBuffer() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - return new Promise((resolve, reject) => { - const concatStream = concat(function (pdfBuffer) { - resolve(pdfBuffer); - }); - - this._pdfDoc.on('error', reject); - this._pdfDoc.pipe(concatStream); - this._pdfDoc.end(); - }); - } - - getStream() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - this._pdfDoc.end(); - return this._pdfDoc; - } -} - -function getTemplate(layout, logo, title) { - const pageMarginTop = 40; - const pageMarginBottom = 80; - const pageMarginWidth = 40; - const headingFontSize = 14; - const headingMarginTop = 10; - const headingMarginBottom = 5; - const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; - const subheadingFontSize = 12; - const subheadingMarginTop = 0; - const subheadingMarginBottom = 5; - const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; - - return { - // define page size - pageOrientation: layout.getPdfPageOrientation(), - pageSize: layout.getPdfPageSize({ - pageMarginTop, - pageMarginBottom, - pageMarginWidth, - tableBorderWidth, - headingHeight, - subheadingHeight, - }), - pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], - - header: function () { - return { - margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], - text: title, - font: getFont(title), - style: { - color: '#aaa', - }, - fontSize: 10, - alignment: 'center', - }; - }, - - footer: function (currentPage, pageCount) { - const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); - return { - margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], - layout: 'noBorder', - table: { - widths: [100, '*', 100], - body: [ - [ - { - fit: [100, 35], - image: logo || logoPath, - }, - { - alignment: 'center', - text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { - defaultMessage: 'Page {currentPage} of {pageCount}', - values: { currentPage: currentPage.toString(), pageCount }, - }), - style: { - color: '#aaa', - }, - }, - '', - ], - [ - logo - ? { - text: i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.logoDescription', - { - defaultMessage: 'Powered by Elastic', - } - ), - fontSize: 10, - style: { - color: '#aaa', - }, - margin: [0, 2, 0, 0], - } - : '', - '', - '', - ], - ], - }, - }; - }, - - styles: { - heading: { - alignment: 'left', - fontSize: headingFontSize, - bold: true, - marginTop: headingMarginTop, - marginBottom: headingMarginBottom, - }, - subheading: { - alignment: 'left', - fontSize: subheadingFontSize, - italics: true, - marginLeft: 20, - marginBottom: subheadingMarginBottom, - }, - warning: { - color: '#f39c12', // same as @brand-warning in Kibana colors.less - }, - }, - - defaultStyle: { - fontSize: 12, - font: 'Roboto', - }, - }; -} - -function getDocOptions() { - return { - tableLayouts: { - noBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => 0, - vLineWidth: () => 0, - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - simpleBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => tableBorderWidth, - vLineWidth: () => tableBorderWidth, - hLineColor: () => 'silver', - vLineColor: () => 'silver', - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - }, - }; -} - -export const pdf = { - create: (layout, logo) => new PdfMaker(layout, logo), -}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts new file mode 100644 index 0000000000000..5c6a7c7c63c69 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PreserveLayout, PrintLayout } from '../../../../lib/layouts'; +import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers'; +import { PdfMaker } from './'; + +const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`; + +describe('PdfMaker', () => { + it('makes PDF using PrintLayout mode', async () => { + const config = createMockConfig(createMockConfigSchema()); + const layout = new PrintLayout(config.get('capture')); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the best PDF in the world')).toBe(undefined); + expect([ + pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }), + pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }), + ]).toEqual([undefined, undefined]); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data? + groupCount: 2, + id: 'print', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-item]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the best PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); + + it('makes PDF using PreserveLayout mode', async () => { + const layout = new PreserveLayout({ width: 400, height: 300 }); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined); + expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + groupCount: 1, + id: 'preserve_layout', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-items-container]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the finest PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts new file mode 100644 index 0000000000000..c58ceae85657b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +// @ts-ignore: no module definition +import concat from 'concat-stream'; +import _ from 'lodash'; +import path from 'path'; +import Printer from 'pdfmake'; +import { Content, ContentText } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getDocOptions } from './get_doc_options'; +import { getFont } from './get_font'; +import { getTemplate } from './get_template'; + +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); +const tableBorderWidth = 1; + +export class PdfMaker { + private _layout: LayoutInstance; + private _logo: string | undefined; + private _title: string; + private _content: Content[]; + private _printer: Printer; + private _pdfDoc: PDFKit.PDFDocument | undefined; + + constructor(layout: LayoutInstance, logo: string | undefined) { + const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); + const fonts = { + Roboto: { + normal: fontPath('roboto/Roboto-Regular.ttf'), + bold: fontPath('roboto/Roboto-Medium.ttf'), + italics: fontPath('roboto/Roboto-Italic.ttf'), + bolditalics: fontPath('roboto/Roboto-Italic.ttf'), + }, + 'noto-cjk': { + // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. + normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + }, + }; + + this._layout = layout; + this._logo = logo; + this._title = ''; + this._content = []; + this._printer = new Printer(fonts); + } + + _addContents(contents: Content[]) { + const groupCount = this._content.length; + + // inject a page break for every 2 groups on the page + if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { + contents = [ + ({ + text: '', + pageBreak: 'after', + } as ContentText) as Content, + ].concat(contents); + } + this._content.push(contents); + } + + addImage(base64EncodedData: string, { title = '', description = '' }) { + const contents: Content[] = []; + + if (title && title.length > 0) { + contents.push({ + text: title, + style: 'heading', + font: getFont(title), + noWrap: true, + }); + } + + if (description && description.length > 0) { + contents.push({ + text: description, + style: 'subheading', + font: getFont(description), + noWrap: true, + }); + } + + const size = this._layout.getPdfImageSize(); + const img = { + image: `data:image/png;base64,${base64EncodedData}`, + alignment: 'center', + height: size.height, + width: size.width, + }; + + const wrappedImg = { + table: { + body: [[img]], + }, + layout: 'noBorder', + }; + + contents.push(wrappedImg); + + this._addContents(contents); + } + + setTitle(title: string) { + this._title = title; + } + + generate() { + const docTemplate = _.assign( + getTemplate(this._layout, this._logo, this._title, tableBorderWidth, assetPath), + { + content: this._content, + } + ); + this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions(tableBorderWidth)); + return this; + } + + getBuffer(): Promise { + return new Promise((resolve, reject) => { + if (!this._pdfDoc) { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', + { + defaultMessage: 'Document stream has not been generated', + } + ) + ); + } + + const concatStream = concat(function (pdfBuffer: Buffer) { + resolve(pdfBuffer); + }); + + this._pdfDoc.on('error', reject); + this._pdfDoc.pipe(concatStream); + this._pdfDoc.end(); + }); + } +} diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 585175aac82c5..e69b8d61dec0d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -5,11 +5,14 @@ */ import { CaptureConfig } from '../../types'; -import { LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams, LayoutTypes } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; -export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams) { +export function createLayout( + captureConfig: CaptureConfig, + layoutParams?: LayoutParams +): LayoutInstance { if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts index 433edb35df4cd..4dd4003c269c0 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/layout.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; import { PageSizeParams, PdfImageSize, Size } from './'; export interface ViewZoomWidthHeight { @@ -14,6 +15,7 @@ export interface ViewZoomWidthHeight { export abstract class Layout { public id: string = ''; + public groupCount: number = 0; constructor(id: string) { this.id = id; @@ -21,9 +23,11 @@ export abstract class Layout { public abstract getPdfImageSize(): PdfImageSize; - public abstract getPdfPageOrientation(): string | undefined; + public abstract getPdfPageOrientation(): 'portrait' | 'landscape' | undefined; - public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; + public abstract getPdfPageSize( + pageSizeParams: PageSizeParams + ): CustomPageSize | PredefinedPageSize; public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index cecd761fbcf32..faddaae64ce5d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -5,14 +5,15 @@ */ import path from 'path'; +import { CustomPageSize } from 'pdfmake/interfaces'; import { getDefaultLayoutSelectors, Layout, + LayoutInstance, LayoutSelectorDictionary, LayoutTypes, PageSizeParams, Size, - LayoutInstance, } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. @@ -72,7 +73,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { return undefined; } - public getPdfPageSize(pageSizeParams: PageSizeParams) { + public getPdfPageSize(pageSizeParams: PageSizeParams): CustomPageSize { return { height: this.height + diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 33f16bc7865d5..e979cdeeb71fe 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -90,11 +91,11 @@ export class PrintLayout extends Layout implements LayoutInstance { }; } - public getPdfPageOrientation() { + public getPdfPageOrientation(): PageOrientation { return 'portrait'; } - public getPdfPageSize() { + public getPdfPageSize(): PredefinedPageSize { return 'A4'; } } 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 d7a963ba4efa7..491f4f8952fd9 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 @@ -73,6 +73,7 @@ import { import { changeToThreeHundredRowsPerPage, deleteFirstRule, + deleteRule, deleteSelectedRules, editFirstRule, filterByCustomRules, @@ -119,6 +120,7 @@ describe('Custom detection rules creation', () => { }); after(() => { + deleteRule(); esArchiverUnload('timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 1b76e74d47bb1..bee4713ca7cda 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -55,6 +55,7 @@ import { } from '../tasks/alerts'; import { changeToThreeHundredRowsPerPage, + deleteRule, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -93,6 +94,7 @@ describe('Detection rules, EQL', () => { }); afterEach(() => { + deleteRule(); esArchiverUnload('timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 0f34e7d71e5fa..153c55fae59fe 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -44,6 +44,7 @@ import { } from '../tasks/alerts'; import { changeToThreeHundredRowsPerPage, + deleteRule, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -78,6 +79,7 @@ describe('Detection rules, machine learning', () => { }); after(() => { + deleteRule(); esArchiverUnload('prebuilt_rules_loaded'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index f997b9eb3bc51..a6f974256f3e4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -56,6 +56,7 @@ import { } from '../tasks/alerts'; import { changeToThreeHundredRowsPerPage, + deleteRule, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -91,6 +92,7 @@ describe('Detection rules, threshold', () => { }); after(() => { + deleteRule(); esArchiverUnload('timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 5a376e95e38dd..e40b81ed0e856 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const ALL_ACTIONS = '[data-test-subj="rules-details-popover-button-icon"]'; + export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; @@ -24,6 +26,8 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; +export const DELETE_RULE = '[data-test-subj=rules-details-delete-rule]'; + export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 8b494edaade3a..1c430e12b6b73 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -27,6 +27,7 @@ import { EDIT_RULE_ACTION_BTN, NEXT_BTN, } from '../screens/alerts_detection_rules'; +import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; export const activateRule = (rulePosition: number) => { cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true }); @@ -47,6 +48,11 @@ export const deleteFirstRule = () => { cy.get(DELETE_RULE_ACTION_BTN).click(); }; +export const deleteRule = () => { + cy.get(ALL_ACTIONS).click(); + cy.get(DELETE_RULE).click(); +}; + export const deleteSelectedRules = () => { cy.get(BULK_ACTIONS_BTN).click({ force: true }); cy.get(DELETE_RULE_BULK_BTN).click(); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 4c9e3bc06037e..c3fc6bd1ae1dd 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/detections/mitre/mitre_tactics_techniques.ts --fix", "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index 8014431192170..d95e0300fe140 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -1,21 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` - +
primary - +
`; exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 37003961d67d0..829d8db5a5a0f 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -80,18 +80,12 @@ export const ItemDetailsPropertySummary = memo( ItemDetailsPropertySummary.displayName = 'ItemPropertySummary'; export const ItemDetailsAction: FC> = memo( - ({ children, ...rest }) => ( - <> - + ({ children, className = '', ...rest }) => ( +
+ {children} - +
) ); diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index fb8deeec8309c..027aa7fd699e4 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -78,9 +78,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0009', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.collectionDescription', - { - defaultMessage: 'Collection (TA0009)', - } + { defaultMessage: 'Collection (TA0009)' } ), value: 'collection', }, @@ -120,9 +118,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0007', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.discoveryDescription', - { - defaultMessage: 'Discovery (TA0007)', - } + { defaultMessage: 'Discovery (TA0007)' } ), value: 'discovery', }, @@ -132,9 +128,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0002', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.executionDescription', - { - defaultMessage: 'Execution (TA0002)', - } + { defaultMessage: 'Execution (TA0002)' } ), value: 'execution', }, @@ -144,9 +138,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0010', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.exfiltrationDescription', - { - defaultMessage: 'Exfiltration (TA0010)', - } + { defaultMessage: 'Exfiltration (TA0010)' } ), value: 'exfiltration', }, @@ -156,9 +148,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0040', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.impactDescription', - { - defaultMessage: 'Impact (TA0040)', - } + { defaultMessage: 'Impact (TA0040)' } ), value: 'impact', }, @@ -168,9 +158,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0001', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.initialAccessDescription', - { - defaultMessage: 'Initial Access (TA0001)', - } + { defaultMessage: 'Initial Access (TA0001)' } ), value: 'initialAccess', }, @@ -190,9 +178,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0003', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.persistenceDescription', - { - defaultMessage: 'Persistence (TA0003)', - } + { defaultMessage: 'Persistence (TA0003)' } ), value: 'persistence', }, @@ -1998,9 +1984,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bitsJobsDescription', - { - defaultMessage: 'BITS Jobs (T1197)', - } + { defaultMessage: 'BITS Jobs (T1197)' } ), id: 'T1197', name: 'BITS Jobs', @@ -2033,9 +2017,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription', - { - defaultMessage: 'Bootkit (T1067)', - } + { defaultMessage: 'Bootkit (T1067)' } ), id: 'T1067', name: 'Bootkit', @@ -2090,9 +2072,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription', - { - defaultMessage: 'CMSTP (T1191)', - } + { defaultMessage: 'CMSTP (T1191)' } ), id: 'T1191', name: 'CMSTP', @@ -2367,9 +2347,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription', - { - defaultMessage: 'DCShadow (T1207)', - } + { defaultMessage: 'DCShadow (T1207)' } ), id: 'T1207', name: 'DCShadow', @@ -2688,9 +2666,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription', - { - defaultMessage: 'Emond (T1519)', - } + { defaultMessage: 'Emond (T1519)' } ), id: 'T1519', name: 'Emond', @@ -3053,9 +3029,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription', - { - defaultMessage: 'Hooking (T1179)', - } + { defaultMessage: 'Hooking (T1179)' } ), id: 'T1179', name: 'Hooking', @@ -3231,9 +3205,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription', - { - defaultMessage: 'Keychain (T1142)', - } + { defaultMessage: 'Keychain (T1142)' } ), id: 'T1142', name: 'Keychain', @@ -3310,9 +3282,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription', - { - defaultMessage: 'Launchctl (T1152)', - } + { defaultMessage: 'Launchctl (T1152)' } ), id: 'T1152', name: 'Launchctl', @@ -3334,9 +3304,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription', - { - defaultMessage: 'Login Item (T1162)', - } + { defaultMessage: 'Login Item (T1162)' } ), id: 'T1162', name: 'Login Item', @@ -3402,9 +3370,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription', - { - defaultMessage: 'Mshta (T1170)', - } + { defaultMessage: 'Mshta (T1170)' } ), id: 'T1170', name: 'Mshta', @@ -3778,9 +3744,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription', - { - defaultMessage: 'Rc.common (T1163)', - } + { defaultMessage: 'Rc.common (T1163)' } ), id: 'T1163', name: 'Rc.common', @@ -3835,9 +3799,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description', - { - defaultMessage: 'Regsvr32 (T1117)', - } + { defaultMessage: 'Regsvr32 (T1117)' } ), id: 'T1117', name: 'Regsvr32', @@ -3936,9 +3898,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rootkitDescription', - { - defaultMessage: 'Rootkit (T1014)', - } + { defaultMessage: 'Rootkit (T1014)' } ), id: 'T1014', name: 'Rootkit', @@ -3949,9 +3909,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description', - { - defaultMessage: 'Rundll32 (T1085)', - } + { defaultMessage: 'Rundll32 (T1085)' } ), id: 'T1085', name: 'Rundll32', @@ -4050,9 +4008,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription', - { - defaultMessage: 'Scripting (T1064)', - } + { defaultMessage: 'Scripting (T1064)' } ), id: 'T1064', name: 'Scripting', @@ -4217,9 +4173,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription', - { - defaultMessage: 'Source (T1153)', - } + { defaultMessage: 'Source (T1153)' } ), id: 'T1153', name: 'Source', @@ -4351,9 +4305,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription', - { - defaultMessage: 'Sudo (T1169)', - } + { defaultMessage: 'Sudo (T1169)' } ), id: 'T1169', name: 'Sudo', @@ -4529,9 +4481,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription', - { - defaultMessage: 'Timestomp (T1099)', - } + { defaultMessage: 'Timestomp (T1099)' } ), id: 'T1099', name: 'Timestomp', @@ -4564,9 +4514,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription', - { - defaultMessage: 'Trap (T1154)', - } + { defaultMessage: 'Trap (T1154)' } ), id: 'T1154', name: 'Trap', @@ -4698,9 +4646,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription', - { - defaultMessage: 'Web Shell (T1100)', - } + { defaultMessage: 'Web Shell (T1100)' } ), id: 'T1100', name: 'Web Shell', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 0cd75506fa9f5..a327f8498f7c0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -300,28 +300,40 @@ describe('rule helpers', () => { }); describe('getHumanizedDuration', () => { - test('returns from as seconds if from duration is less than a minute', () => { + test('returns from as seconds if from duration is specified in seconds', () => { const result = getHumanizedDuration('now-62s', '1m'); expect(result).toEqual('2s'); }); - test('returns from as minutes if from duration is less than an hour', () => { + test('returns from as seconds if from duration is specified in seconds greater than 60', () => { + const result = getHumanizedDuration('now-122s', '1m'); + + expect(result).toEqual('62s'); + }); + + test('returns from as minutes if from duration is specified in minutes', () => { const result = getHumanizedDuration('now-660s', '5m'); expect(result).toEqual('6m'); }); - test('returns from as hours if from duration is more than 60 minutes', () => { - const result = getHumanizedDuration('now-7400s', '5m'); + test('returns from as minutes if from duration is specified in minutes greater than 60', () => { + const result = getHumanizedDuration('now-6600s', '5m'); + + expect(result).toEqual('105m'); + }); + + test('returns from as hours if from duration is specified in hours', () => { + const result = getHumanizedDuration('now-7500s', '5m'); - expect(result).toEqual('1h'); + expect(result).toEqual('2h'); }); test('returns from as if from is not parsable as dateMath', () => { const result = getHumanizedDuration('randomstring', '5m'); - expect(result).toEqual('NaNh'); + expect(result).toEqual('NaNs'); }); test('returns from as 5m if interval is not parsable as dateMath', () => { 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 4dbcffbc807ec..ffcf25d253798 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 @@ -118,15 +118,21 @@ export const getHumanizedDuration = (from: string, interval: string): string => const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); const fromDuration = moment.duration(intervalValue.diff(fromValue)); - const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; - if (fromDuration.asSeconds() < 60) { - return `${Math.floor(fromDuration.asSeconds())}s`; - } else if (fromDuration.asMinutes() < 60) { - return `${Math.floor(fromDuration.asMinutes())}m`; + // Basing calculations off floored seconds count as moment durations weren't precise + const intervalDuration = Math.floor(fromDuration.asSeconds()); + // For consistency of display value + if (intervalDuration === 0) { + return `0s`; } - return fromHumanize; + if (intervalDuration % 3600 === 0) { + return `${intervalDuration / 3600}h`; + } else if (intervalDuration % 60 === 0) { + return `${intervalDuration / 60}m`; + } else { + return `${intervalDuration}s`; + } }; export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 399509466e573..86cb203671ac2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -344,22 +344,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -595,22 +596,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -846,22 +848,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -1097,22 +1100,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -1348,22 +1352,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -1599,22 +1604,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -1850,22 +1856,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -2101,22 +2108,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -2352,22 +2360,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -2603,22 +2612,23 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- + +
@@ -3144,22 +3154,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -3395,22 +3406,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -3646,22 +3658,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -3897,22 +3910,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -4148,22 +4162,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -4399,22 +4414,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -4650,22 +4666,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -4901,22 +4918,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -5152,22 +5170,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -5403,22 +5422,23 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
- + +
@@ -5902,22 +5922,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -6153,22 +6174,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -6404,22 +6426,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -6655,22 +6678,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -6906,22 +6930,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -7157,22 +7182,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -7408,22 +7434,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -7659,22 +7686,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -7910,22 +7938,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
@@ -8161,22 +8190,23 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
- + +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index a56e71e8073cb..841f9dc81bd8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -996,22 +996,23 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- + +
diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index 5c31b3fad685a..aa4112d8a6f97 100644 --- a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js @@ -13,9 +13,10 @@ const fetch = require('node-fetch'); const { camelCase } = require('lodash'); const { resolve } = require('path'); -const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); -const MITRE_ENTREPRISE_ATTACK_URL = - 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; +const OUTPUT_DIRECTORY = resolve('public', 'detections', 'mitre'); +// Revert to https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json once we support sub-techniques +const MITRE_ENTERPRISE_ATTACK_URL = + 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v6.3/enterprise-attack/enterprise-attack.json'; const getTacticsOptions = (tactics) => tactics.map((t) => @@ -63,7 +64,7 @@ const getIdReference = (references) => ); async function main() { - fetch(MITRE_ENTREPRISE_ATTACK_URL) + fetch(MITRE_ENTERPRISE_ATTACK_URL) .then((res) => res.json()) .then((json) => { const mitreData = json.objects; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx index 2685e62eb9a6c..2ef887d0627cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -74,4 +74,51 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeFalsy(); expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); }); + + test('should display a message to remember username and password when creating a connector with authentication', () => { + const actionConnector = { + actionTypeId: '.email', + config: { + hasAuth: true, + }, + secrets: {}, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message when editing an authenticated email connector explaining why username and password must be re-entered', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 1e92e9fc2519c..111f6c9a47da9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -12,6 +12,7 @@ import { EuiFieldPassword, EuiSwitch, EuiFormRow, + EuiText, EuiTitle, EuiSpacer, EuiCallOut, @@ -56,7 +57,7 @@ export const EmailActionConnectorFields: React.FunctionComponent } @@ -202,17 +203,7 @@ export const EmailActionConnectorFields: React.FunctionComponent {hasAuth ? ( <> - {action.id ? ( - <> - - - - - ) : null} + {getEncryptedFieldNotifyLabel(!action.id)} + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { EmailActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 61923d8f78b51..f476522c2bf5a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -68,7 +68,7 @@ describe('jira connector validation', () => { errors: { apiUrl: ['URL is required.'], email: [], - apiToken: ['API token or Password is required'], + apiToken: ['API token or password is required'], projectKey: ['Project key is required'], }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index 2cac1819d552d..6d055f4fdb9f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -96,4 +96,60 @@ describe('JiraActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.jira', + isPreconfigured: false, + secrets: {}, + config: {}, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index 2ab9843c143b9..35c6bd7af00ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -6,12 +6,15 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldPassword, EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -133,6 +136,20 @@ const JiraConnectorFields: React.FC + + + +

{i18n.JIRA_AUTHENTICATION_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.JIRA_REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { JiraConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 019133b03d55f..f6db56e188322 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const JIRA_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', { - defaultMessage: 'Push or update data to a new issue in Jira', + defaultMessage: 'Create an incident in Jira.', } ); @@ -55,31 +55,54 @@ export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( } ); +export const JIRA_AUTHENTICATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel', + { + defaultMessage: 'Authentication', + } +); + +export const JIRA_REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const JIRA_REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel', + { + defaultMessage: + 'Authentication credentials are encrypted. Please reenter values for these fields.', + } +); + export const JIRA_EMAIL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', { - defaultMessage: 'Email or Username', + defaultMessage: 'Username or email address', } ); export const JIRA_EMAIL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', { - defaultMessage: 'Email or Username is required', + defaultMessage: 'Username or email address is required', } ); export const JIRA_API_TOKEN_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', { - defaultMessage: 'API token or Password', + defaultMessage: 'API token or password', } ); export const JIRA_API_TOKEN_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', { - defaultMessage: 'API token or Password is required', + defaultMessage: 'API token or password is required', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx index 53e68e6453690..18978be7b4680 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -49,4 +49,56 @@ describe('PagerDutyActionConnectorFields renders', () => { ); expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.pagerduty', + secrets: {}, + config: {}, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 6399e1f80984c..ad2d5b3be5268 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -53,7 +53,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent } @@ -66,26 +66,61 @@ const PagerDutyActionConnectorFields: React.FunctionComponent - 0 && routingKey !== undefined} - name="routingKey" - readOnly={readOnly} - value={routingKey || ''} - data-test-subj="pagerdutyRoutingKeyInput" - onChange={(e: React.ChangeEvent) => { - editActionSecrets('routingKey', e.target.value); - }} - onBlur={() => { - if (!routingKey) { - editActionSecrets('routingKey', ''); - } - }} - /> + + {getEncryptedFieldNotifyLabel(!action.id)} + 0 && routingKey !== undefined} + name="routingKey" + readOnly={readOnly} + value={routingKey || ''} + data-test-subj="pagerdutyRoutingKeyInput" + onChange={(e: React.ChangeEvent) => { + editActionSecrets('routingKey', e.target.value); + }} + onBlur={() => { + if (!routingKey) { + editActionSecrets('routingKey', ''); + } + }} + /> + ); }; +function getEncryptedFieldNotifyLabel(isCreate: boolean) { + if (isCreate) { + return ( + + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { PagerDutyActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index b73eb72f137c1..937fe61e887ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -68,7 +68,7 @@ describe('resilient connector validation', () => { errors: { apiUrl: ['URL is required.'], apiKeyId: [], - apiKeySecret: ['API key secret is required'], + apiKeySecret: ['Secret is required'], orgId: ['Organization ID is required'], }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index 7e242f1f501d8..6dede85736fcd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -97,4 +97,60 @@ describe('ResilientActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.resilient', + isPreconfigured: false, + config: {}, + secrets: {}, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx index 7965e216f1d6c..fe2aa341a7111 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -6,12 +6,15 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldPassword, EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -133,6 +136,20 @@ const ResilientConnectorFields: React.FC + + + +

{i18n.API_KEY_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { ResilientConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts index 71ad05abfdecf..b45f898d7d809 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', { - defaultMessage: 'Push or update data to a new incident in Resilient.', + defaultMessage: 'Create an incident in IBM Resilient.', } ); @@ -55,31 +55,53 @@ export const ORG_ID_REQUIRED = i18n.translate( } ); +export const API_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey', + { + defaultMessage: 'API key', + } +); + +export const REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel', + { + defaultMessage: 'ID and secret are encrypted. Please reenter values for these fields.', + } +); + export const API_KEY_ID_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId', { - defaultMessage: 'API key ID', + defaultMessage: 'ID', } ); export const API_KEY_ID_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField', { - defaultMessage: 'API key ID is required', + defaultMessage: 'ID is required', } ); export const API_KEY_SECRET_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret', { - defaultMessage: 'API key secret', + defaultMessage: 'Secret', } ); export const API_KEY_SECRET_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField', { - defaultMessage: 'API key secret is required', + defaultMessage: 'Secret is required', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 216e6967833b2..b666db4024b12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -83,4 +83,59 @@ describe('ServiceNowActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.servicenow', + isPreconfigured: false, + config: {}, + secrets: {}, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'servicenow', + config: { + apiUrl: 'https://test/', + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a8f1ed8d55447..d351b32c3bb06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -6,6 +6,7 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -13,6 +14,8 @@ import { EuiFieldPassword, EuiSpacer, EuiLink, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -89,7 +92,7 @@ const ServiceNowConnectorFields: React.FC } @@ -113,6 +116,20 @@ const ServiceNowConnectorFields: React.FC + + + +

{i18n.AUTHENTICATION_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 48544945836d9..67e94bc136cf9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -41,6 +41,28 @@ export const API_URL_INVALID = i18n.translate( } ); +export const AUTHENTICATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel', + { + defaultMessage: 'Authentication', + } +); + +export const REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel', + { + defaultMessage: 'Username and password are encrypted. Please reenter values for these fields.', + } +); + export const USERNAME_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx index 5bc778830b6e6..f87c2ad99fb4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -44,4 +44,54 @@ describe('SlackActionFields renders', () => { 'http:\\test' ); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.email', + config: {}, + secrets: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index aa3a1932eacdb..3e744eff07797 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -27,7 +27,7 @@ const SlackActionFields: React.FunctionComponent } @@ -40,27 +40,62 @@ const SlackActionFields: React.FunctionComponent - 0 && webhookUrl !== undefined} - name="webhookUrl" - readOnly={readOnly} - placeholder="Example: https://hooks.slack.com/services" - value={webhookUrl || ''} - data-test-subj="slackWebhookUrlInput" - onChange={(e) => { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> + + {getEncryptedFieldNotifyLabel(!action.id)} + 0 && webhookUrl !== undefined} + name="webhookUrl" + readOnly={readOnly} + placeholder="Example: https://hooks.slack.com/services" + value={webhookUrl || ''} + data-test-subj="slackWebhookUrlInput" + onChange={(e) => { + editActionSecrets('webhookUrl', e.target.value); + }} + onBlur={() => { + if (!webhookUrl) { + editActionSecrets('webhookUrl', ''); + } + }} + /> + ); }; +function getEncryptedFieldNotifyLabel(isCreate: boolean) { + if (isCreate) { + return ( + + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { SlackActionFields as default }; 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 7f2bed6c41f3b..45e4c566f7a27 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 @@ -44,4 +44,55 @@ describe('WebhookActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + secrets: {}, + actionTypeId: '.webhook', + isPreconfigured: false, + config: {}, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); 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 52160441adb5b..e4f5ef023a529 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 @@ -7,6 +7,7 @@ import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiCallOut, EuiFieldPassword, EuiFieldText, EuiFormRow, @@ -18,6 +19,7 @@ import { EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, + EuiText, EuiTitle, EuiSwitch, EuiButtonEmpty, @@ -266,6 +268,26 @@ const WebhookActionConnectorFields: React.FunctionComponent + + + + +

+ +

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { WebhookActionConnectorFields as default }; diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js index 55c34615373a9..0cf20b6599acb 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js @@ -86,6 +86,7 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject } async clickDiskCol() { await find.clickByCssSelector(`[data-test-subj="${SUBJ_TABLE_SORT_DISK_COL}"] > button`); + await this.waitForTableToFinishLoading(); } async clickShardsCol() { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 6f7d693baac7f..297eb2e9b4540 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -24,6 +24,8 @@ { "path": "../plugins/global_search/tsconfig.json" }, { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../../src/plugins/telemetry/tsconfig.json" } + { "path": "../../src/plugins/telemetry/tsconfig.json" }, + { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../../src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 1d392890b27fd..79309369386cf 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -13,10 +13,7 @@ "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", "plugins/licensing/**/*", - "plugins/global_search/**/*", - "../src/plugins/usage_collection/**/*", - "../src/plugins/telemetry_collection_manager/**/*", - "../src/plugins/telemetry/**/*" + "plugins/global_search/**/*" ], "compilerOptions": { "paths": { @@ -36,6 +33,8 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" } + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../src/plugins/newsfeed/tsconfig.json" }, ] } diff --git a/yarn.lock b/yarn.lock index 44c59d0264ddf..e2f5ed412a14a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,13 +1244,14 @@ utility-types "^3.10.0" uuid "^3.3.2" -"@elastic/elasticsearch@7.9.1": - version "7.9.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.9.1.tgz#40f1c38e8f70d783851c13be78a7cb346891c15e" - integrity sha512-NfPADbm9tRK/4ohpm9+aBtJ8WPKQqQaReyBKT225pi2oKQO1IzRlfM+OPplAvbhoH1efrSj1NKk27L+4BCrzXQ== +"@elastic/elasticsearch@7.10.0-rc.1": + version "7.10.0-rc.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0-rc.1.tgz#c23fc5cbfdb40cf2ce6f9cd796b75940e8c9dc8a" + integrity sha512-STaBlEwYbT8yT3HJ+mbO1kx+Kb7Ft7Q0xG5GxZbqbAJ7PZvgGgJWwN7jUg4oKJHbTfxV3lPvFa+PaUK2TqGuYg== dependencies: debug "^4.1.1" decompress-response "^4.2.0" + hpagent "^0.1.1" ms "^2.1.1" pump "^3.0.0" secure-json-parse "^2.1.0" @@ -4584,6 +4585,21 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== +"@types/pdfkit@*": + version "0.10.6" + resolved "https://registry.yarnpkg.com/@types/pdfkit/-/pdfkit-0.10.6.tgz#9ddde7e642e6c3f1245134456a03fbc4b67dc4b5" + integrity sha512-o6R2fO/fhg392YNYahiaNGZ8CNdSKMmf5W0LvyjJ/63mLQEPTgl6M9vb4zKoHaGpsP43VimuBz+xgVevsPy8jA== + dependencies: + "@types/node" "*" + +"@types/pdfmake@^0.1.15": + version "0.1.15" + resolved "https://registry.yarnpkg.com/@types/pdfmake/-/pdfmake-0.1.15.tgz#383a8fca407612a580b82d1ca496d39001aee102" + integrity sha512-uyKefZzC1OUTKoUdY0fU9n7BjciSSYPHq2KLQmGNejeZn6Xo6hI04xhAGk368Rv9wHjKo36IGLIVIysvhrGVJQ== + dependencies: + "@types/node" "*" + "@types/pdfkit" "*" + "@types/pegjs@^0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" @@ -15100,6 +15116,11 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +hpagent@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2" + integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ== + html-element-map@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"