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/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/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/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/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/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/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/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/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/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/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/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"