diff --git a/.eslintignore b/.eslintignore index 4058d971b764..ce21d5bb3126 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,4 @@ **/*.js.snap -**/graphql/types.ts /.es /.chromium /build diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 9564087dabef..610d78bacccd 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -65,6 +65,7 @@ yarn kbn watch-bazel - @kbn/apm-utils - @kbn/babel-preset - @kbn/config-schema +- @kbn/std - @kbn/tinymath - @kbn/utility-types diff --git a/package.json b/package.json index 047dd38d92cd..992433e17e6c 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", "globby/fast-glob": "3.2.5", - "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", @@ -137,7 +136,7 @@ "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", - "@kbn/std": "link:packages/kbn-std", + "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath/npm_module", "@kbn/ui-framework": "link:packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:packages/kbn-ui-shared-deps", @@ -193,10 +192,10 @@ "compare-versions": "3.5.1", "concat-stream": "1.6.2", "constate": "^1.3.2", - "cronstrue": "^1.51.0", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", + "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", @@ -231,8 +230,6 @@ "glob": "^7.1.2", "glob-all": "^3.2.1", "globby": "^11.0.3", - "graphql": "^0.13.2", - "graphql-tag": "^2.10.3", "handlebars": "4.7.7", "he": "^1.2.0", "history": "^4.9.0", @@ -274,9 +271,9 @@ "lodash": "^4.17.21", "lru-cache": "^4.1.5", "lz-string": "^1.4.4", - "markdown-it": "^10.0.0", "mapbox-gl": "1.13.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", + "markdown-it": "^10.0.0", "md5": "^2.1.0", "memoize-one": "^5.0.0", "mime": "^2.4.4", @@ -298,12 +295,12 @@ "object-path-immutable": "^3.1.1", "opn": "^5.5.0", "oppsy": "^2.0.0", + "p-limit": "^3.0.1", "p-map": "^4.0.0", "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", "pegjs": "0.10.0", - "p-limit": "^3.0.1", "pluralize": "3.1.0", "pngjs": "^3.4.0", "polished": "^1.9.2", @@ -335,19 +332,19 @@ "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", "react-query": "^3.13.10", + "react-redux": "^7.2.0", + "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "react-router-redux": "^4.0.8", "react-shortcuts": "^2.0.0", "react-sizeme": "^2.3.6", "react-syntax-highlighter": "^15.3.1", - "react-redux": "^7.2.0", - "react-resizable": "^1.7.5", - "react-router": "^5.2.0", - "react-router-dom": "^5.2.0", "react-tiny-virtual-list": "^2.2.0", - "react-virtualized": "^9.21.2", "react-use": "^15.3.8", + "react-virtualized": "^9.21.2", "react-vis": "^1.8.1", "react-visibility-sensor": "^5.1.1", "reactcss": "1.2.3", @@ -376,8 +373,8 @@ "strip-ansi": "^6.0.0", "style-it": "^2.1.3", "styled-components": "^5.1.0", - "symbol-observable": "^1.2.0", "suricata-sid-db": "^1.0.2", + "symbol-observable": "^1.2.0", "tabbable": "1.1.3", "tar": "4.4.13", "tinycolor2": "1.4.1", @@ -438,7 +435,7 @@ "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", "@istanbuljs/schema": "^0.1.2", - "@jest/reporters": "^26.5.2", + "@jest/reporters": "^26.6.2", "@kbn/babel-code-parser": "link:packages/kbn-babel-code-parser", "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset/npm_module", "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", @@ -475,11 +472,11 @@ "@storybook/node-logger": "^6.1.20", "@storybook/react": "^6.1.20", "@storybook/theming": "^6.1.20", - "@testing-library/dom": "^7.24.2", - "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.0.4", - "@testing-library/react-hooks": "^3.4.1", - "@testing-library/user-event": "^12.1.6", + "@testing-library/dom": "^7.30.3", + "@testing-library/jest-dom": "^5.11.10", + "@testing-library/react": "^11.2.6", + "@testing-library/react-hooks": "^5.1.1", + "@testing-library/user-event": "^13.1.1", "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", @@ -488,7 +485,7 @@ "@types/base64-js": "^1.2.5", "@types/bluebird": "^3.1.1", "@types/chance": "^1.0.0", - "@types/cheerio": "^0.22.10", + "@types/cheerio": "^0.22.28", "@types/chroma-js": "^1.4.2", "@types/chromedriver": "^81.0.0", "@types/classnames": "^2.2.9", @@ -508,7 +505,7 @@ "@types/delete-empty": "^2.0.0", "@types/ejs": "^3.0.6", "@types/elasticsearch": "^5.0.33", - "@types/enzyme": "^3.10.5", + "@types/enzyme": "^3.10.8", "@types/eslint": "^6.1.3", "@types/extract-zip": "^1.6.2", "@types/faker": "^5.1.5", @@ -521,7 +518,6 @@ "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", "@types/glob": "^7.1.2", - "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", "@types/hapi__cookie": "^10.1.1", @@ -537,9 +533,9 @@ "@types/http-proxy-agent": "^2.0.2", "@types/inquirer": "^7.3.1", "@types/intl-relativeformat": "^2.1.0", - "@types/jest": "^26.0.14", - "@types/jest-specific-snapshot": "^0.5.4", - "@types/jest-when": "^2.7.1", + "@types/jest": "^26.0.22", + "@types/jest-specific-snapshot": "^0.5.5", + "@types/jest-when": "^2.7.2", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", "@types/js-search": "^1.4.0", @@ -618,8 +614,8 @@ "@types/tar": "^4.0.3", "@types/tar-fs": "^1.16.1", "@types/tempy": "^0.2.0", - "@types/testing-library__jest-dom": "^5.9.3", - "@types/testing-library__react-hooks": "^3.4.0", + "@types/testing-library__jest-dom": "^5.9.5", + "@types/testing-library__react-hooks": "^4.0.0", "@types/tinycolor2": "^1.4.1", "@types/type-detect": "^4.0.1", "@types/use-resize-observer": "^6.0.0", @@ -682,9 +678,9 @@ "dpdm": "3.5.0", "ejs": "^3.1.6", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", - "enzyme-adapter-utils": "^1.13.0", - "enzyme-to-json": "^3.4.4", + "enzyme-adapter-react-16": "^1.15.6", + "enzyme-adapter-utils": "^1.14.0", + "enzyme-to-json": "^3.6.1", "eslint": "^6.8.0", "eslint-config-prettier": "^6.15.0", "eslint-import-resolver-node": "0.3.2", @@ -695,7 +691,7 @@ "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.0.2", + "eslint-plugin-jest": "^24.3.4", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-mocha": "^6.2.2", "eslint-plugin-no-unsanitized": "^3.0.2", @@ -731,18 +727,17 @@ "is-path-inside": "^3.0.2", "istanbul-instrumenter-loader": "^3.0.1", "jest": "^26.6.3", - "jest-canvas-mock": "^2.2.0", + "jest-canvas-mock": "^2.3.1", "jest-circus": "^26.6.3", "jest-cli": "^26.6.3", "jest-diff": "^26.6.2", - "jest-environment-jsdom-thirteen": "^1.0.1", "jest-environment-jsdom": "^26.6.2", "jest-raw-loader": "^1.0.1", - "jest-silent-reporter": "^0.2.1", + "jest-silent-reporter": "^0.5.0", "jest-snapshot": "^26.6.2", "jest-specific-snapshot": "2.0.0", - "jest-styled-components": "^7.0.2", - "jest-when": "^2.7.2", + "jest-styled-components": "^7.0.3", + "jest-when": "^3.2.1", "jimp": "^0.14.0", "jsdom": "13.1.0", "json5": "^1.0.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index e1a85e926f04..552eed64d418 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -7,6 +7,7 @@ filegroup( "//packages/kbn-apm-utils:build", "//packages/kbn-babel-preset:build", "//packages/kbn-config-schema:build", + "//packages/kbn-std:build", "//packages/kbn-tinymath:build", "//packages/kbn-utility-types:build", ], diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 1ea319ef3601..2ffa09d7e160 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -18,7 +18,6 @@ "@kbn/logging": "link:../kbn-logging", "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer", - "@kbn/std": "link:../kbn-std", "@kbn/dev-utils": "link:../kbn-dev-utils", "@kbn/utils": "link:../kbn-utils" } diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 8093b6ac0d21..9bf491e30087 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -11,8 +11,7 @@ }, "dependencies": { "@elastic/safer-lodash-set": "link:../elastic-safer-lodash-set", - "@kbn/logging": "link:../kbn-logging", - "@kbn/std": "link:../kbn-std" + "@kbn/logging": "link:../kbn-logging" }, "devDependencies": { "@kbn/dev-utils": "link:../kbn-dev-utils", diff --git a/packages/kbn-docs-utils/src/index.ts b/packages/kbn-docs-utils/src/index.ts index 24aef1bf891f..5accd1fa2984 100644 --- a/packages/kbn-docs-utils/src/index.ts +++ b/packages/kbn-docs-utils/src/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export * from './release_notes'; export * from './api_docs'; diff --git a/packages/kbn-docs-utils/src/release_notes/cli.ts b/packages/kbn-docs-utils/src/release_notes/cli.ts deleted file mode 100644 index e6d1c717459b..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/cli.ts +++ /dev/null @@ -1,152 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Fs from 'fs'; -import Path from 'path'; -import { inspect } from 'util'; - -import { REPO_ROOT } from '@kbn/utils'; -import { run, createFlagError, createFailError } from '@kbn/dev-utils'; - -import { FORMATS, SomeFormat } from './formats'; -import { - PrApi, - Version, - ClassifiedPr, - streamFromIterable, - asyncPipeline, - IrrelevantPrSummary, - isPrRelevant, - classifyPr, -} from './lib'; - -const rootPackageJson = JSON.parse( - Fs.readFileSync(Path.resolve(REPO_ROOT, 'package.json'), 'utf8') -); -const extensions = FORMATS.map((f) => f.extension); - -export function runReleaseNotesCli() { - run( - async ({ flags, log }) => { - const token = flags.token; - if (!token || typeof token !== 'string') { - throw createFlagError('--token must be defined'); - } - const prApi = new PrApi(log, token); - - const version = Version.fromFlag(flags.version); - if (!version) { - throw createFlagError('unable to parse --version, use format "v{major}.{minor}.{patch}"'); - } - - const includeVersions = Version.fromFlags(flags.include || []); - if (!includeVersions) { - throw createFlagError('unable to parse --include, use format "v{major}.{minor}.{patch}"'); - } - - const Formats: SomeFormat[] = []; - for (const flag of Array.isArray(flags.format) ? flags.format : [flags.format]) { - const Format = FORMATS.find((F) => F.extension === flag); - if (!Format) { - throw createFlagError(`--format must be one of "${extensions.join('", "')}"`); - } - Formats.push(Format); - } - - const filename = flags.filename; - if (!filename || typeof filename !== 'string') { - throw createFlagError('--filename must be a string'); - } - - if (flags['debug-pr']) { - const number = parseInt(String(flags['debug-pr']), 10); - if (Number.isNaN(number)) { - throw createFlagError('--debug-pr must be a pr number when specified'); - } - - const summary = new IrrelevantPrSummary(log); - const pr = await prApi.getPr(number); - log.success( - inspect( - { - version: version.label, - includeVersions: includeVersions.map((v) => v.label), - isPrRelevant: isPrRelevant(pr, version, includeVersions, summary), - ...classifyPr(pr, log), - pr, - }, - { depth: 100 } - ) - ); - summary.logStats(); - return; - } - - log.info(`Loading all PRs with label [${version.label}] to build release notes...`); - - const summary = new IrrelevantPrSummary(log); - const prsToReport: ClassifiedPr[] = []; - const prIterable = prApi.iterRelevantPullRequests(version); - for await (const pr of prIterable) { - if (!isPrRelevant(pr, version, includeVersions, summary)) { - continue; - } - prsToReport.push(classifyPr(pr, log)); - } - summary.logStats(); - - if (!prsToReport.length) { - throw createFailError( - `All PRs with label [${version.label}] were filtered out by the config. Run again with --debug for more info.` - ); - } - - log.info(`Found ${prsToReport.length} prs to report on`); - - for (const Format of Formats) { - const format = new Format(version, prsToReport, log); - const outputPath = Path.resolve(`${filename}.${Format.extension}`); - await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath)); - log.success(`[${Format.extension}] report written to ${outputPath}`); - } - }, - { - usage: `node scripts/release_notes --token {token} --version {version}`, - flags: { - alias: { - version: 'v', - include: 'i', - }, - string: ['token', 'version', 'format', 'filename', 'include', 'debug-pr'], - default: { - filename: 'report', - version: rootPackageJson.version, - format: extensions, - }, - help: ` - --token (required) The Github access token to use for requests - --version, -v The version to fetch PRs by, PRs with version labels prior to - this one will be ignored (see --include-version) (default ${ - rootPackageJson.version - }) - --include, -i A version that is before --version but shouldn't be considered - "released" and cause PRs with a matching label to be excluded from - release notes. Use this when PRs are labeled with a version that - is less that --version and is expected to be released after - --version, can be specified multiple times. - --format Only produce a certain format, options: "${extensions.join('", "')}" - --filename Output filename, defaults to "report" - --debug-pr Fetch and print the details for a single PR, disabling reporting - `, - }, - description: ` - Fetch details from Github PRs for generating release notes - `, - } - ); -} diff --git a/packages/kbn-docs-utils/src/release_notes/formats/asciidoc.ts b/packages/kbn-docs-utils/src/release_notes/formats/asciidoc.ts deleted file mode 100644 index df86f6c7a40e..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/formats/asciidoc.ts +++ /dev/null @@ -1,73 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import dedent from 'dedent'; - -import { Format } from './format'; -import { - ASCIIDOC_SECTIONS, - UNKNOWN_ASCIIDOC_SECTION, - AREAS, - UNKNOWN_AREA, -} from '../release_notes_config'; - -function* lines(body: string) { - for (const line of dedent(body).split('\n')) { - yield `${line}\n`; - } -} - -export class AsciidocFormat extends Format { - static extension = 'asciidoc'; - - *print() { - const sortedAreas = [ - ...AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)), - UNKNOWN_AREA, - ]; - - yield* lines(` - [[release-notes-${this.version.label}]] - == ${this.version.label} Release Notes - - Also see <>. - `); - - for (const section of [...ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION]) { - const prsInSection = this.prs.filter((pr) => pr.asciidocSection === section); - if (!prsInSection.length) { - continue; - } - - yield '\n'; - yield* lines(` - [float] - [[${section.id}-${this.version.label}]] - === ${section.title} - `); - - for (const area of sortedAreas) { - const prsInArea = prsInSection.filter((pr) => pr.area === area); - - if (!prsInArea.length) { - continue; - } - - yield `${area.title}::\n`; - for (const pr of prsInArea) { - const fixes = pr.fixes.length ? `[Fixes ${pr.fixes.join(', ')}] ` : ''; - const strippedTitle = pr.title.replace(/^\s*\[[^\]]+\]\s*/, ''); - yield `* ${fixes}${strippedTitle} {kibana-pull}${pr.number}[#${pr.number}]\n`; - if (pr.note) { - yield ` - ${pr.note}\n`; - } - } - } - } - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/formats/csv.ts b/packages/kbn-docs-utils/src/release_notes/formats/csv.ts deleted file mode 100644 index ad03ebaff804..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/formats/csv.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Format } from './format'; - -/** - * Escape a value to conform to field and header encoding defined at https://tools.ietf.org/html/rfc4180 - */ -function esc(value: string | number) { - if (typeof value === 'number') { - return String(value); - } - - if (!value.includes(',') && !value.includes('\n') && !value.includes('"')) { - return value; - } - - return `"${value.split('"').join('""')}"`; -} - -function row(...fields: Array) { - return fields.map(esc).join(',') + '\r\n'; -} - -export class CsvFormat extends Format { - static extension = 'csv'; - - *print() { - // columns - yield row( - 'areas', - 'versions', - 'user', - 'title', - 'number', - 'url', - 'date', - 'fixes', - 'labels', - 'state' - ); - - for (const pr of this.prs) { - yield row( - pr.area.title, - pr.versions.map((v) => v.label).join(', '), - pr.user.name || pr.user.login, - pr.title, - pr.number, - pr.url, - pr.mergedAt, - pr.fixes.join(', '), - pr.labels.join(', '), - pr.state - ); - } - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/formats/format.ts b/packages/kbn-docs-utils/src/release_notes/formats/format.ts deleted file mode 100644 index 937beb2f3fd6..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/formats/format.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ToolingLog } from '@kbn/dev-utils'; - -import { Version, ClassifiedPr } from '../lib'; - -export abstract class Format { - static extension: string; - - constructor( - protected readonly version: Version, - protected readonly prs: ClassifiedPr[], - protected readonly log: ToolingLog - ) {} - - abstract print(): Iterator; -} diff --git a/packages/kbn-docs-utils/src/release_notes/formats/index.ts b/packages/kbn-docs-utils/src/release_notes/formats/index.ts deleted file mode 100644 index 2019dce53f53..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/formats/index.ts +++ /dev/null @@ -1,14 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ArrayItem } from '../lib'; -import { AsciidocFormat } from './asciidoc'; -import { CsvFormat } from './csv'; - -export const FORMATS = [CsvFormat, AsciidocFormat] as const; -export type SomeFormat = ArrayItem; diff --git a/packages/kbn-docs-utils/src/release_notes/index.ts b/packages/kbn-docs-utils/src/release_notes/index.ts deleted file mode 100644 index 7ee97ec9aa05..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './cli'; diff --git a/packages/kbn-docs-utils/src/release_notes/lib/classify_pr.ts b/packages/kbn-docs-utils/src/release_notes/lib/classify_pr.ts deleted file mode 100644 index ca24367fa728..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/classify_pr.ts +++ /dev/null @@ -1,55 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ToolingLog } from '@kbn/dev-utils'; - -import { - Area, - AREAS, - UNKNOWN_AREA, - AsciidocSection, - ASCIIDOC_SECTIONS, - UNKNOWN_ASCIIDOC_SECTION, -} from '../release_notes_config'; -import { PullRequest } from './pr_api'; - -export interface ClassifiedPr extends PullRequest { - area: Area; - asciidocSection: AsciidocSection; -} - -export function classifyPr(pr: PullRequest, log: ToolingLog): ClassifiedPr { - const filter = (a: Area | AsciidocSection) => - a.labels.some((test) => - typeof test === 'string' ? pr.labels.includes(test) : pr.labels.some((l) => l.match(test)) - ); - - const areas = AREAS.filter(filter); - const asciidocSections = ASCIIDOC_SECTIONS.filter(filter); - - const pickOne = (name: string, options: T[]) => { - if (options.length > 1) { - const matches = options.map((o) => o.title).join(', '); - log.warning(`[${pr.terminalLink}] ambiguous ${name}, mulitple match [${matches}]`); - return options[0]; - } - - if (options.length === 0) { - log.error(`[${pr.terminalLink}] unable to determine ${name} because none match`); - return; - } - - return options[0]; - }; - - return { - ...pr, - area: pickOne('area', areas) || UNKNOWN_AREA, - asciidocSection: pickOne('asciidoc section', asciidocSections) || UNKNOWN_ASCIIDOC_SECTION, - }; -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.test.ts deleted file mode 100644 index 8cc8aec19f94..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.test.ts +++ /dev/null @@ -1,57 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getFixReferences } from './get_fix_references'; - -it('returns all fixed issue mentions in the PR text', () => { - expect( - getFixReferences(` - clOses #1 - closes: #2 - clOse #3 - close: #4 - clOsed #5 - closed: #6 - fiX #7 - fix: #8 - fiXes #9 - fixes: #10 - fiXed #11 - fixed: #12 - reSolve #13 - resolve: #14 - reSolves #15 - resolves: #16 - reSolved #17 - resolved: #18 - fixed - #19 - `) - ).toMatchInlineSnapshot(` - Array [ - "#1", - "#2", - "#3", - "#4", - "#5", - "#6", - "#7", - "#8", - "#9", - "#10", - "#11", - "#12", - "#13", - "#14", - "#15", - "#16", - "#17", - "#18", - ] - `); -}); diff --git a/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.ts deleted file mode 100644 index c4c8ed0f9a9e..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.ts +++ /dev/null @@ -1,18 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const FIXES_RE = /(?:closes|close|closed|fix|fixes|fixed|resolve|resolves|resolved)[ :]*(#\d*)/gi; - -export function getFixReferences(prText: string) { - const fixes: string[] = []; - let match; - while ((match = FIXES_RE.exec(prText))) { - fixes.push(match[1]); - } - return fixes; -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.test.ts deleted file mode 100644 index 59945a835a3c..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import MarkdownIt from 'markdown-it'; -import dedent from 'dedent'; - -import { getNoteFromDescription } from './get_note_from_description'; - -it('extracts expected components from html', () => { - const mk = new MarkdownIt(); - - expect( - getNoteFromDescription( - mk.render(dedent` - My PR description - - Fixes: #1234 - - ## Release Note: - - Checkout this feature - `), - 'release note' - ) - ).toMatchInlineSnapshot(`"Checkout this feature"`); - - expect( - getNoteFromDescription( - mk.render(dedent` - My PR description - - Fixes: #1234 - - #### Dev docs: - - We fixed an issue - `), - 'dev docs' - ) - ).toMatchInlineSnapshot(`"We fixed an issue"`); - - expect( - getNoteFromDescription( - mk.render(dedent` - My PR description - - Fixes: #1234 - - OTHER TITLE: Checkout feature foo - `), - 'other title' - ) - ).toMatchInlineSnapshot(`"Checkout feature foo"`); - - expect( - getNoteFromDescription( - mk.render(dedent` - # Summary - - My PR description - - release note : bar - `), - 'release note' - ) - ).toMatchInlineSnapshot(`"bar"`); -}); diff --git a/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.ts deleted file mode 100644 index db80c29454cf..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.ts +++ /dev/null @@ -1,25 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import cheerio from 'cheerio'; - -export function getNoteFromDescription(descriptionHtml: string, header: string) { - const re = new RegExp(`^(\\s*${header.toLowerCase()}(?:s)?\\s*:?\\s*)`, 'i'); - const $ = cheerio.load(descriptionHtml); - for (const el of $('p,h1,h2,h3,h4,h5').toArray()) { - const text = $(el).text(); - const match = text.match(re); - - if (!match) { - continue; - } - - const note = text.replace(match[1], '').trim(); - return note || $(el).next().text().trim(); - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/index.ts b/packages/kbn-docs-utils/src/release_notes/lib/index.ts deleted file mode 100644 index 8578060007d7..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './pr_api'; -export * from './version'; -export * from './is_pr_relevant'; -export * from './streams'; -export * from './type_helpers'; -export * from './irrelevant_pr_summary'; -export * from './classify_pr'; diff --git a/packages/kbn-docs-utils/src/release_notes/lib/irrelevant_pr_summary.ts b/packages/kbn-docs-utils/src/release_notes/lib/irrelevant_pr_summary.ts deleted file mode 100644 index 3bc9ebfced60..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/irrelevant_pr_summary.ts +++ /dev/null @@ -1,50 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ToolingLog } from '@kbn/dev-utils'; - -import { PullRequest } from './pr_api'; -import { Version } from './version'; - -export class IrrelevantPrSummary { - private readonly stats = { - 'skipped by label': new Map(), - 'skipped by label regexp': new Map(), - 'skipped by version': new Map(), - }; - - constructor(private readonly log: ToolingLog) {} - - skippedByLabel(pr: PullRequest, label: string) { - this.log.debug(`${pr.terminalLink} skipped, label [${label}] is ignored`); - this.increment('skipped by label', label); - } - - skippedByLabelRegExp(pr: PullRequest, regexp: RegExp, label: string) { - this.log.debug(`${pr.terminalLink} skipped, label [${label}] matches regexp [${regexp}]`); - this.increment('skipped by label regexp', `${regexp}`); - } - - skippedByVersion(pr: PullRequest, earliestVersion: Version) { - this.log.debug(`${pr.terminalLink} skipped, earliest version is [${earliestVersion.label}]`); - this.increment('skipped by version', earliestVersion.label); - } - - private increment(stat: keyof IrrelevantPrSummary['stats'], key: string) { - const n = this.stats[stat].get(key) || 0; - this.stats[stat].set(key, n + 1); - } - - logStats() { - for (const [description, stats] of Object.entries(this.stats)) { - for (const [key, count] of stats) { - this.log.warning(`${count} ${count === 1 ? 'pr was' : 'prs were'} ${description} [${key}]`); - } - } - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/is_pr_relevant.ts b/packages/kbn-docs-utils/src/release_notes/lib/is_pr_relevant.ts deleted file mode 100644 index 1de75373c095..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/is_pr_relevant.ts +++ /dev/null @@ -1,50 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Version } from './version'; -import { PullRequest } from './pr_api'; -import { IGNORE_LABELS } from '../release_notes_config'; -import { IrrelevantPrSummary } from './irrelevant_pr_summary'; - -export function isPrRelevant( - pr: PullRequest, - version: Version, - includeVersions: Version[], - summary: IrrelevantPrSummary -) { - for (const label of IGNORE_LABELS) { - if (typeof label === 'string') { - if (pr.labels.includes(label)) { - summary.skippedByLabel(pr, label); - return false; - } - } - - if (label instanceof RegExp) { - const matching = pr.labels.find((l) => label.test(l)); - if (matching) { - summary.skippedByLabelRegExp(pr, label, matching); - return false; - } - } - } - - const [earliestVersion] = Version.sort( - // filter out `includeVersions` so that they won't be considered the "earliest version", only - // versions which are actually before the current `version` or the `version` itself are eligible - pr.versions.filter((v) => !includeVersions.includes(v)), - 'asc' - ); - - if (version !== earliestVersion) { - summary.skippedByVersion(pr, earliestVersion); - return false; - } - - return true; -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/pr_api.ts b/packages/kbn-docs-utils/src/release_notes/lib/pr_api.ts deleted file mode 100644 index 0f4f8abc7fd9..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/pr_api.ts +++ /dev/null @@ -1,222 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { inspect } from 'util'; - -import Axios from 'axios'; -import gql from 'graphql-tag'; -import * as GraphqlPrinter from 'graphql/language/printer'; -import { DocumentNode } from 'graphql/language/ast'; -import makeTerminalLink from 'terminal-link'; -import { ToolingLog, isAxiosResponseError } from '@kbn/dev-utils'; - -import { Version } from './version'; -import { getFixReferences } from './get_fix_references'; -import { getNoteFromDescription } from './get_note_from_description'; - -const PrNodeFragment = gql` - fragment PrNode on PullRequest { - number - url - title - bodyText - bodyHTML - mergedAt - baseRefName - state - author { - login - ... on User { - name - } - } - labels(first: 100) { - nodes { - name - } - } - } -`; - -export interface PullRequest { - number: number; - url: string; - title: string; - targetBranch: string; - mergedAt: string; - state: string; - labels: string[]; - fixes: string[]; - user: { - name: string; - login: string; - }; - versions: Version[]; - terminalLink: string; - note?: string; -} - -export class PrApi { - constructor(private readonly log: ToolingLog, private readonly token: string) {} - - async getPr(number: number) { - const resp = await this.gqlRequest( - gql` - query($number: Int!) { - repository(owner: "elastic", name: "kibana") { - pullRequest(number: $number) { - ...PrNode - } - } - } - ${PrNodeFragment} - `, - { - number, - } - ); - - const node = resp.data?.repository?.pullRequest; - if (!node) { - throw new Error(`unexpected github response, unable to fetch PR: ${inspect(resp)}`); - } - - return this.parsePullRequestNode(node); - } - - /** - * Iterate all of the PRs which have the `version` label - */ - async *iterRelevantPullRequests(version: Version) { - let nextCursor: string | undefined; - let hasNextPage = true; - - while (hasNextPage) { - const resp = await this.gqlRequest( - gql` - query($cursor: String, $labels: [String!]) { - repository(owner: "elastic", name: "kibana") { - pullRequests(first: 100, after: $cursor, labels: $labels, states: MERGED) { - pageInfo { - hasNextPage - endCursor - } - nodes { - ...PrNode - } - } - } - } - ${PrNodeFragment} - `, - { - cursor: nextCursor, - labels: [version.label], - } - ); - - const pullRequests = resp.data?.repository?.pullRequests; - if (!pullRequests) { - throw new Error(`unexpected github response, unable to fetch PRs: ${inspect(resp)}`); - } - - hasNextPage = pullRequests.pageInfo?.hasNextPage; - nextCursor = pullRequests.pageInfo?.endCursor; - - if (hasNextPage === undefined || (hasNextPage && !nextCursor)) { - throw new Error( - `github response does not include valid pagination information: ${inspect(resp)}` - ); - } - - for (const node of pullRequests.nodes) { - yield this.parsePullRequestNode(node); - } - } - } - - /** - * Convert the Github API response into the structure used by this tool - * - * @param node A GraphQL response from Github using the PrNode fragment - */ - private parsePullRequestNode(node: any): PullRequest { - const terminalLink = makeTerminalLink(`#${node.number}`, node.url); - - const labels: string[] = node.labels.nodes.map((l: { name: string }) => l.name); - - return { - number: node.number, - url: node.url, - terminalLink, - title: node.title, - targetBranch: node.baseRefName, - state: node.state, - mergedAt: node.mergedAt, - labels, - fixes: getFixReferences(node.bodyText), - user: { - login: node.author?.login || 'deleted user', - name: node.author?.name, - }, - versions: labels - .map((l) => Version.fromLabel(l)) - .filter((v): v is Version => v instanceof Version), - note: - getNoteFromDescription(node.bodyHTML, 'release note') || - getNoteFromDescription(node.bodyHTML, 'dev docs'), - }; - } - - /** - * Send a single request to the Github v4 GraphQL API - */ - private async gqlRequest(query: DocumentNode, variables: Record = {}) { - let attempt = 0; - - while (true) { - attempt += 1; - - try { - const resp = await Axios.request({ - url: 'https://api.github.com/graphql', - method: 'POST', - headers: { - 'user-agent': '@kbn/release-notes', - authorization: `bearer ${this.token}`, - }, - data: { - query: GraphqlPrinter.print(query), - variables, - }, - }); - - return resp.data; - } catch (error) { - if (!isAxiosResponseError(error) || error.response.status < 500) { - // rethrow error unless it is a 500+ response from github - throw error; - } - - const { status, data } = error.response; - const resp = inspect(data); - - if (attempt === 5) { - throw new Error( - `${status} response from Github, attempted request ${attempt} times: [${resp}]` - ); - } - - const delay = attempt * 2000; - this.log.debug(`Github responded with ${status}, retrying in ${delay} ms: [${resp}]`); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - } - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/lib/streams.ts b/packages/kbn-docs-utils/src/release_notes/lib/streams.ts deleted file mode 100644 index 6893bfd7f4f4..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/streams.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { promisify } from 'util'; -import { Readable, pipeline } from 'stream'; - -/** - * @types/node still doesn't have this method that was added - * in 10.17.0 https://nodejs.org/api/stream.html#stream_stream_readable_from_iterable_options - */ -export function streamFromIterable( - iter: Iterable | AsyncIterable -): Readable { - // @ts-ignore - return Readable.from(iter); -} - -export const asyncPipeline = promisify(pipeline); diff --git a/packages/kbn-docs-utils/src/release_notes/lib/type_helpers.ts b/packages/kbn-docs-utils/src/release_notes/lib/type_helpers.ts deleted file mode 100644 index 81860160094d..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/type_helpers.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export type ArrayItem = T extends ReadonlyArray ? X : never; diff --git a/packages/kbn-docs-utils/src/release_notes/lib/version.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/version.test.ts deleted file mode 100644 index b23feb0929a2..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/version.test.ts +++ /dev/null @@ -1,135 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Version } from './version'; - -it('parses version labels, returns null on failure', () => { - expect(Version.fromLabel('v1.0.2')).toMatchInlineSnapshot(` - Version { - "label": "v1.0.2", - "major": 1, - "minor": 0, - "patch": 2, - "tag": undefined, - "tagNum": undefined, - "tagOrder": Infinity, - } - `); - expect(Version.fromLabel('v1.0.0')).toMatchInlineSnapshot(` - Version { - "label": "v1.0.0", - "major": 1, - "minor": 0, - "patch": 0, - "tag": undefined, - "tagNum": undefined, - "tagOrder": Infinity, - } - `); - expect(Version.fromLabel('v9.0.2')).toMatchInlineSnapshot(` - Version { - "label": "v9.0.2", - "major": 9, - "minor": 0, - "patch": 2, - "tag": undefined, - "tagNum": undefined, - "tagOrder": Infinity, - } - `); - expect(Version.fromLabel('v9.0.2-alpha0')).toMatchInlineSnapshot(` - Version { - "label": "v9.0.2-alpha0", - "major": 9, - "minor": 0, - "patch": 2, - "tag": "alpha", - "tagNum": 0, - "tagOrder": 1, - } - `); - expect(Version.fromLabel('v9.0.2-beta1')).toMatchInlineSnapshot(` - Version { - "label": "v9.0.2-beta1", - "major": 9, - "minor": 0, - "patch": 2, - "tag": "beta", - "tagNum": 1, - "tagOrder": 2, - } - `); - expect(Version.fromLabel('v9.0')).toMatchInlineSnapshot(`undefined`); - expect(Version.fromLabel('some:area')).toMatchInlineSnapshot(`undefined`); -}); - -it('sorts versions in ascending order', () => { - const versions = [ - 'v1.7.3', - 'v1.7.0', - 'v1.5.0', - 'v2.7.0', - 'v7.0.0-beta2', - 'v7.0.0-alpha1', - 'v2.0.0', - 'v0.0.0', - 'v7.0.0-beta1', - 'v7.0.0', - ].map((l) => Version.fromLabel(l)!); - - const sorted = Version.sort(versions); - - expect(sorted.map((v) => v.label)).toMatchInlineSnapshot(` - Array [ - "v0.0.0", - "v1.5.0", - "v1.7.0", - "v1.7.3", - "v2.0.0", - "v2.7.0", - "v7.0.0-alpha1", - "v7.0.0-beta1", - "v7.0.0-beta2", - "v7.0.0", - ] - `); - - // ensure versions was not mutated - expect(sorted).not.toEqual(versions); -}); - -it('sorts versions in decending order', () => { - const versions = [ - 'v1.7.3', - 'v1.7.0', - 'v1.5.0', - 'v7.0.0-beta1', - 'v2.7.0', - 'v2.0.0', - 'v0.0.0', - 'v7.0.0', - ].map((l) => Version.fromLabel(l)!); - - const sorted = Version.sort(versions, 'desc'); - - expect(sorted.map((v) => v.label)).toMatchInlineSnapshot(` - Array [ - "v7.0.0", - "v7.0.0-beta1", - "v2.7.0", - "v2.0.0", - "v1.7.3", - "v1.7.0", - "v1.5.0", - "v0.0.0", - ] - `); - - // ensure versions was not mutated - expect(sorted).not.toEqual(versions); -}); diff --git a/packages/kbn-docs-utils/src/release_notes/lib/version.ts b/packages/kbn-docs-utils/src/release_notes/lib/version.ts deleted file mode 100644 index c59060c99022..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/lib/version.ts +++ /dev/null @@ -1,112 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const LABEL_RE = /^v(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta)(\d+))?$/; - -const versionCache = new Map(); - -const multiCompare = (...diffs: number[]) => { - for (const diff of diffs) { - if (diff !== 0) { - return diff; - } - } - return 0; -}; - -export class Version { - static fromFlag(flag: string | string[] | boolean | undefined) { - if (typeof flag !== 'string') { - return; - } - - return Version.fromLabel(flag) || Version.fromLabel(`v${flag}`); - } - - static fromFlags(flag: string | string[] | boolean | undefined) { - const flags = Array.isArray(flag) ? flag : [flag]; - const versions: Version[] = []; - - for (const f of flags) { - const version = Version.fromFlag(f); - if (!version) { - return; - } - versions.push(version); - } - - return versions; - } - - static fromLabel(label: string) { - const match = label.match(LABEL_RE); - if (!match) { - return; - } - - const cached = versionCache.get(label); - if (cached) { - return cached; - } - - const [, major, minor, patch, tag, tagNum] = match; - const version = new Version( - parseInt(major, 10), - parseInt(minor, 10), - parseInt(patch, 10), - tag as 'alpha' | 'beta' | undefined, - tagNum ? parseInt(tagNum, 10) : undefined - ); - - versionCache.set(label, version); - return version; - } - - static sort(versions: Version[], dir: 'asc' | 'desc' = 'asc') { - const order = dir === 'asc' ? 1 : -1; - - return versions.slice().sort((a, b) => a.compare(b) * order); - } - - public readonly label = `v${this.major}.${this.minor}.${this.patch}${ - this.tag ? `-${this.tag}${this.tagNum}` : '' - }`; - private readonly tagOrder: number; - - constructor( - public readonly major: number, - public readonly minor: number, - public readonly patch: number, - public readonly tag: 'alpha' | 'beta' | undefined, - public readonly tagNum: number | undefined - ) { - switch (tag) { - case undefined: - this.tagOrder = Infinity; - break; - case 'alpha': - this.tagOrder = 1; - break; - case 'beta': - this.tagOrder = 2; - break; - default: - throw new Error('unexpected tag'); - } - } - - compare(other: Version) { - return multiCompare( - this.major - other.major, - this.minor - other.minor, - this.patch - other.patch, - this.tagOrder - other.tagOrder, - (this.tagNum ?? 0) - (other.tagNum ?? 0) - ); - } -} diff --git a/packages/kbn-docs-utils/src/release_notes/release_notes_config.ts b/packages/kbn-docs-utils/src/release_notes/release_notes_config.ts deleted file mode 100644 index a94dcd8766cb..000000000000 --- a/packages/kbn-docs-utils/src/release_notes/release_notes_config.ts +++ /dev/null @@ -1,283 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * Exclude any PR from release notes that has a matching label. String - * labels must match exactly, for more complicated use a RegExp - */ -export const IGNORE_LABELS: Array = [ - 'Team:Docs', - ':KibanaApp/fix-it-week', - 'reverted', - /^test/, - 'non-issue', - 'jenkins', - 'build', - 'chore', - 'backport', - 'release_note:skip', - 'release_note:dev_docs', -]; - -/** - * Define areas that are used to categorize changes in the release notes - * based on the labels a PR has. the `labels` array can contain strings, which - * are matched exactly, or regular expressions. The first area, in definition - * order, which has a `label` which matches and label on a PR is the area - * assigned to that PR. - */ - -export interface Area { - title: string; - labels: Array; -} - -export const AREAS: Area[] = [ - { - title: 'Design', - labels: ['Team:Design', 'Project:Accessibility'], - }, - { - title: 'Logstash', - labels: ['App:Logstash', 'Feature:Logstash Pipelines'], - }, - { - title: 'Management', - labels: [ - 'Feature:license', - 'Feature:Console', - 'Feature:Search Profiler', - 'Feature:watcher', - 'Feature:Index Patterns', - 'Feature:Kibana Management', - 'Feature:Dev Tools', - 'Feature:Inspector', - 'Feature:Index Management', - 'Feature:Snapshot and Restore', - 'Team:Elasticsearch UI', - 'Feature:FieldFormatters', - 'Feature:CCR', - 'Feature:ILM', - 'Feature:Transforms', - ], - }, - { - title: 'Monitoring', - labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'], - }, - { - title: 'Operations', - labels: ['Team:Operations', 'Feature:License'], - }, - { - title: 'Kibana UI', - labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'], - }, - { - title: 'Platform', - labels: [ - 'Team:Platform', - 'Feature:Plugins', - 'Feature:New Platform', - 'Project:i18n', - 'Feature:ExpressionLanguage', - 'Feature:Saved Objects', - 'Team:Stack Services', - 'Feature:NP Migration', - 'Feature:Task Manager', - 'Team:Pulse', - ], - }, - { - title: 'Machine Learning', - labels: [ - ':ml', - 'Feature:Anomaly Detection', - 'Feature:Data Frames', - 'Feature:File Data Viz', - 'Feature:ml-results', - 'Feature:Data Frame Analytics', - ], - }, - { - title: 'Maps', - labels: ['Team:Geo'], - }, - { - title: 'QA', - labels: ['Team:QA'], - }, - { - title: 'Security', - labels: [ - 'Team:Security', - 'Feature:Security/Spaces', - 'Feature:users and roles', - 'Feature:Security/Authentication', - 'Feature:Security/Authorization', - 'Feature:Security/Feature Controls', - ], - }, - { - title: 'Canvas', - labels: ['Feature:Canvas'], - }, - { - title: 'Dashboard', - labels: ['Feature:Dashboard', 'Feature:Drilldowns'], - }, - { - title: 'Discover', - labels: ['Feature:Discover'], - }, - { - title: 'Kibana Home & Add Data', - labels: ['Feature:Add Data', 'Feature:Home'], - }, - { - title: 'Querying & Filtering', - labels: [ - 'Feature:Query Bar', - 'Feature:Courier', - 'Feature:Filters', - 'Feature:Timepicker', - 'Feature:Highlight', - 'Feature:KQL', - 'Feature:Rollups', - ], - }, - { - title: 'Reporting', - labels: ['Feature:Reporting', 'Team:Reporting Services'], - }, - { - title: 'Sharing', - labels: ['Feature:Embedding', 'Feature:SharingURLs'], - }, - { - title: 'Visualizations', - labels: [ - 'Feature:Timelion', - 'Feature:TSVB', - 'Feature:Coordinate Map', - 'Feature:Region Map', - 'Feature:Vega', - 'Feature:Gauge Vis', - 'Feature:Tagcloud', - 'Feature:Vis Loader', - 'Feature:Vislib', - 'Feature:Vis Editor', - 'Feature:Aggregations', - 'Feature:Input Control', - 'Feature:Visualizations', - 'Feature:Markdown', - 'Feature:Data Table', - 'Feature:Heatmap', - 'Feature:Pie Chart', - 'Feature:XYAxis', - 'Feature:Graph', - 'Feature:New Feature', - 'Feature:MetricVis', - ], - }, - { - title: 'SIEM', - labels: ['Team:SIEM'], - }, - { - title: 'Code', - labels: ['Team:Code'], - }, - { - title: 'Infrastructure', - labels: ['App:Infrastructure', 'Feature:Infra UI', 'Feature:Service Maps'], - }, - { - title: 'Logs', - labels: ['App:Logs', 'Feature:Logs UI'], - }, - { - title: 'Uptime', - labels: ['App:Uptime', 'Feature:Uptime', 'Team:uptime'], - }, - { - title: 'Beats Management', - labels: ['App:Beats', 'Feature:beats-cm', 'Team:Beats'], - }, - { - title: 'APM', - labels: ['Team:apm', /^apm[:\-]/], - }, - { - title: 'Lens', - labels: ['App:Lens', 'Feature:Lens'], - }, - { - title: 'Alerting', - labels: ['App:Alerting', 'Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions'], - }, - { - title: 'Metrics', - labels: ['App:Metrics', 'Feature:Metrics UI', 'Team:logs-metrics-ui'], - }, - { - title: 'Data ingest', - labels: ['Ingest', 'Feature:Ingest Node Pipelines'], - }, -]; - -export const UNKNOWN_AREA: Area = { - title: 'Unknown', - labels: [], -}; - -/** - * Define the sections that will be assigned to PRs when generating the - * asciidoc formatted report. The order of the sections determines the - * order they will be rendered in the report - */ - -export interface AsciidocSection { - title: string; - labels: Array; - id: string; -} - -export const ASCIIDOC_SECTIONS: AsciidocSection[] = [ - { - id: 'enhancement', - title: 'Enhancements', - labels: ['release_note:enhancement'], - }, - { - id: 'bug', - title: 'Bug fixes', - labels: ['release_note:fix'], - }, - { - id: 'roadmap', - title: 'Roadmap', - labels: ['release_note:roadmap'], - }, - { - id: 'deprecation', - title: 'Deprecations', - labels: ['release_note:deprecation'], - }, - { - id: 'breaking', - title: 'Breaking Changes', - labels: ['release_note:breaking'], - }, -]; - -export const UNKNOWN_ASCIIDOC_SECTION: AsciidocSection = { - id: 'unknown', - title: 'Unknown', - labels: [], -}; diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 2ecfa5c907a2..236d5cf25213 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -272,7 +272,7 @@ exports.Cluster = class Cluster { // especially because we currently run many instances of ES on the same machine during CI options.esEnvVars.ES_JAVA_OPTS = (options.esEnvVars.ES_JAVA_OPTS ? `${options.esEnvVars.ES_JAVA_OPTS} ` : '') + - '-Xms1g -Xmx1g'; + '-Xms2g -Xmx2g'; this._process = execa(ES_BIN, args, { cwd: installPath, diff --git a/packages/kbn-logging/package.json b/packages/kbn-logging/package.json index c7db148c75a2..596eda1fe625 100644 --- a/packages/kbn-logging/package.json +++ b/packages/kbn-logging/package.json @@ -9,8 +9,5 @@ "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" - }, - "dependencies": { - "@kbn/std": "link:../kbn-std" } } \ No newline at end of file diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 3c14d98755a3..423bba0fd8c7 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -13,7 +13,6 @@ "dependencies": { "@kbn/config": "link:../kbn-config", "@kbn/dev-utils": "link:../kbn-dev-utils", - "@kbn/std": "link:../kbn-std", "@kbn/ui-shared-deps": "link:../kbn-ui-shared-deps" } } \ No newline at end of file diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index 24f8f8d67dfd..5a1bb0d5b536 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -11,8 +11,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/crypto": "link:../kbn-crypto", - "@kbn/std": "link:../kbn-std" + "@kbn/crypto": "link:../kbn-crypto" }, "devDependencies": { "@kbn/utility-types": "link:../kbn-utility-types" diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel new file mode 100644 index 000000000000..82520be97df1 --- /dev/null +++ b/packages/kbn-std/BUILD.bazel @@ -0,0 +1,85 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-std" +PKG_REQUIRE_NAME = "@kbn/std" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-utility-types", + "@npm//lodash", + "@npm//query-string", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index b914356d9924..d88422ec1aa8 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -4,12 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build" - }, - "devDependencies": { - "@kbn/utility-types": "link:../kbn-utility-types" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index d2ed46dcad6f..dec2d2df6408 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -1,12 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "declarationDir": "./target", + "incremental": true, "outDir": "./target", "stripInternal": true, "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-std/src", "types": [ @@ -16,8 +16,5 @@ }, "include": [ "./src/**/*.ts" - ], - "exclude": [ - "**/__fixture__/**/*" ] } diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index e22ba38b24a4..1a02f94a88f4 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -57,15 +57,14 @@ ts_project( js_library( name = PKG_BASE_NAME, - srcs = [], + srcs = NPM_MODULE_EXTRA_FILES, deps = [":tsc"] + DEPS, - package_name = PKG_REQUIRE_NAME, + package_name = "@kbn/utility-types", visibility = ["//visibility:public"], ) pkg_npm( name = "npm_module", - srcs = NPM_MODULE_EXTRA_FILES, deps = [ ":%s" % PKG_BASE_NAME, ] diff --git a/renovate.json5 b/renovate.json5 index 4dec49ceddf6..ea41175e1aaa 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -38,7 +38,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00', 'nickofthyme'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.13.0'], + labels: ['release_note:skip', 'v8.0.0', 'v7.14.0'], enabled: true, }, { @@ -54,7 +54,7 @@ packageNames: ['@elastic/elasticsearch'], reviewers: ['team:kibana-operations'], matchBaseBranches: ['7.x'], - labels: ['release_note:skip', 'v7.13.0', 'Team:Operations', 'backport:skip'], + labels: ['release_note:skip', 'v7.14.0', 'Team:Operations', 'backport:skip'], enabled: true, }, { diff --git a/scripts/release_notes.js b/scripts/release_notes.js deleted file mode 100644 index 7408ce322677..000000000000 --- a/scripts/release_notes.js +++ /dev/null @@ -1,10 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -require('../src/setup_node_env/no_transpilation'); -require('@kbn/docs-utils').runReleaseNotesCli(); diff --git a/src/cli_keystore/dev.js b/src/cli_keystore/dev.js index f7497add69ba..8cc08fa8e323 100644 --- a/src/cli_keystore/dev.js +++ b/src/cli_keystore/dev.js @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -require('../setup_node_env/no_transpilation'); +require('../setup_node_env'); require('./cli_keystore'); diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 65df5cd6aa31..bba0dc6fd8a6 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -80,7 +80,7 @@ describe('RenderingService', () => { it('renders "core" page', async () => { const content = await render(createKibanaRequest(), uiSettings); const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); @@ -90,7 +90,7 @@ describe('RenderingService', () => { const content = await render(createKibanaRequest(), uiSettings); const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); @@ -99,7 +99,7 @@ describe('RenderingService', () => { uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: true } }); const content = await render(createKibanaRequest(), uiSettings); const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); @@ -109,7 +109,7 @@ describe('RenderingService', () => { includeUserSettings: false, }); const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); @@ -117,7 +117,7 @@ describe('RenderingService', () => { it('renders "core" from legacy request', async () => { const content = await render(createRawRequest(), uiSettings); const dom = load(content); - const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 1508cab69a04..599c32137c55 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -49,7 +49,7 @@ export const bucketAggsSchemas: Record = { histogram: s.object({ field: s.maybe(s.string()), interval: s.maybe(s.number()), - min_doc_count: s.maybe(s.number()), + min_doc_count: s.maybe(s.number({ min: 1 })), extended_bounds: s.maybe( s.object({ min: s.number(), @@ -78,7 +78,7 @@ export const bucketAggsSchemas: Record = { include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), execution_hint: s.maybe(s.string()), missing: s.maybe(s.number()), - min_doc_count: s.maybe(s.number()), + min_doc_count: s.maybe(s.number({ min: 1 })), size: s.maybe(s.number()), show_term_doc_count_error: s.maybe(s.boolean()), order: s.maybe(s.oneOf([s.literal('asc'), s.literal('desc')])), diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts new file mode 100644 index 000000000000..33f7ca12abc5 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { bucketAggsSchemas } from './bucket_aggs'; + +describe('bucket aggregation schemas', () => { + describe('terms aggregation schema', () => { + const schema = bucketAggsSchemas.terms; + + it('passes validation when using `1` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 1 })).not.toThrow(); + }); + + // see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_minimum_document_count_4 + // Setting min_doc_count=0 will also return buckets for terms that didn’t match any hit, + // bypassing any filtering perform via `filter` or `query` + // causing a potential security issue as we can return values from other spaces. + it('throws an error when using `0` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 0 })).toThrowErrorMatchingInlineSnapshot( + `"[min_doc_count]: Value must be equal to or greater than [1]."` + ); + }); + }); + + describe('histogram aggregation schema', () => { + const schema = bucketAggsSchemas.histogram; + + it('passes validation when using `1` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 1 })).not.toThrow(); + }); + + it('throws an error when using `0` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 0 })).toThrowErrorMatchingInlineSnapshot( + `"[min_doc_count]: Value must be equal to or greater than [1]."` + ); + }); + }); +}); diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_remove.sh b/src/dev/build/tasks/os_packages/package_scripts/post_remove.sh index 4e36ae11a29b..7e587e26ffa9 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_remove.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_remove.sh @@ -34,12 +34,25 @@ case $1 in esac if [ "$REMOVE_DIRS" = "true" ]; then + + if [ -d "<%= logDir %>" ]; then + echo -n "Deleting log directory..." + rm -rf "<%= logDir %>" + echo " OK" + fi + if [ -d "<%= pluginsDir %>" ]; then echo -n "Deleting plugins directory..." rm -rf "<%= pluginsDir %>" echo " OK" fi + if [ -d "<%= pidDir %>" ]; then + echo -n "Deleting PID directory..." + rm -rf "<%= pidDir %>" + echo " OK" + fi + if [ -d "<%= configDir %>" ]; then rmdir --ignore-fail-on-non-empty "<%= configDir %>" fi diff --git a/src/plugins/data/common/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts index df31719b0aec..6a6ac1dfa93e 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -11,7 +11,6 @@ import { SearchSourceDependencies } from './search_source'; import { IIndexPattern } from '../../index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { Filter } from '../../es_query/filters'; -import { BehaviorSubject } from 'rxjs'; describe('createSearchSource', () => { const indexPatternMock: IIndexPattern = {} as IIndexPattern; @@ -24,10 +23,6 @@ describe('createSearchSource', () => { getConfig: jest.fn(), search: jest.fn(), onResponse: (req, res) => res, - legacy: { - callMsearch: jest.fn(), - loadingCount$: new BehaviorSubject(0), - }, }; indexPatternContractMock = ({ diff --git a/src/plugins/data/common/search/search_source/fetch/types.ts b/src/plugins/data/common/search/search_source/fetch/types.ts index 8e8a9f1025b8..79aa45163b91 100644 --- a/src/plugins/data/common/search/search_source/fetch/types.ts +++ b/src/plugins/data/common/search/search_source/fetch/types.ts @@ -7,7 +7,6 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { LegacyFetchHandlers } from '../legacy/types'; import { GetConfigFn } from '../../../types'; /** @@ -29,11 +28,6 @@ export interface FetchHandlers { request: SearchRequest, response: estypes.SearchResponse ) => estypes.SearchResponse; - /** - * These handlers are only used by the legacy defaultSearchStrategy and can be removed - * once that strategy has been deprecated. - */ - legacy: LegacyFetchHandlers; } export interface SearchError { diff --git a/src/plugins/data/common/search/search_source/legacy/call_client.test.ts b/src/plugins/data/common/search/search_source/legacy/call_client.test.ts deleted file mode 100644 index 93849b63939e..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/call_client.test.ts +++ /dev/null @@ -1,102 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { callClient } from './call_client'; -import { SearchStrategySearchParams } from './types'; -import { defaultSearchStrategy } from './default_search_strategy'; -import { FetchHandlers } from '../fetch'; -import { BehaviorSubject } from 'rxjs'; - -const mockAbortFn = jest.fn(); - -jest.mock('./default_search_strategy', () => { - return { - defaultSearchStrategy: { - search: jest.fn(({ searchRequests }: SearchStrategySearchParams) => { - return { - searching: Promise.resolve( - searchRequests.map((req) => { - return { - id: req._searchStrategyId, - }; - }) - ), - abort: mockAbortFn, - }; - }), - }, - }; -}); - -describe('callClient', () => { - const handleResponse = jest.fn().mockImplementation((req, res) => res); - const handlers = { - getConfig: jest.fn(), - onResponse: handleResponse, - legacy: { - callMsearch: jest.fn(), - loadingCount$: new BehaviorSubject(0), - }, - } as FetchHandlers; - - beforeEach(() => { - handleResponse.mockClear(); - }); - - test('Passes the additional arguments it is given to the search strategy', () => { - const searchRequests = [{ _searchStrategyId: 0 }]; - - callClient(searchRequests, [], handlers); - - expect(defaultSearchStrategy.search).toBeCalled(); - expect((defaultSearchStrategy.search as any).mock.calls[0][0]).toEqual({ - searchRequests, - ...handlers, - }); - }); - - test('Returns the responses in the original order', async () => { - const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; - - const responses = await Promise.all(callClient(searchRequests, [], handlers)); - - expect(responses[0]).toEqual({ id: searchRequests[0]._searchStrategyId }); - expect(responses[1]).toEqual({ id: searchRequests[1]._searchStrategyId }); - }); - - test('Calls handleResponse with each request and response', async () => { - const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - - const responses = callClient(searchRequests, [], handlers); - await Promise.all(responses); - - expect(handleResponse).toBeCalledTimes(2); - expect(handleResponse).toBeCalledWith(searchRequests[0], { - id: searchRequests[0]._searchStrategyId, - }); - expect(handleResponse).toBeCalledWith(searchRequests[1], { - id: searchRequests[1]._searchStrategyId, - }); - }); - - test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { - const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - const abortController = new AbortController(); - const requestOptions = [ - { - abortSignal: abortController.signal, - }, - ]; - - callClient(searchRequests, requestOptions, handlers); - abortController.abort(); - - expect(mockAbortFn).toBeCalled(); - // expect(mockAbortFns[1]).not.toBeCalled(); - }); -}); diff --git a/src/plugins/data/common/search/search_source/legacy/call_client.ts b/src/plugins/data/common/search/search_source/legacy/call_client.ts deleted file mode 100644 index 4c1156aac701..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/call_client.ts +++ /dev/null @@ -1,38 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { estypes } from '@elastic/elasticsearch'; -import { FetchHandlers, SearchRequest } from '../fetch'; -import { defaultSearchStrategy } from './default_search_strategy'; -import { ISearchOptions } from '../../index'; - -export function callClient( - searchRequests: SearchRequest[], - requestsOptions: ISearchOptions[] = [], - fetchHandlers: FetchHandlers -) { - // Correlate the options with the request that they're associated with - const requestOptionEntries: Array< - [SearchRequest, ISearchOptions] - > = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); - const requestResponseMap = new Map>>(); - - const { searching, abort } = defaultSearchStrategy.search({ - searchRequests, - ...fetchHandlers, - }); - - searchRequests.forEach((request, i) => { - const response = searching.then((results) => fetchHandlers.onResponse(request, results[i])); - const { abortSignal = null } = requestOptionsMap.get(request) || {}; - if (abortSignal) abortSignal.addEventListener('abort', abort); - requestResponseMap.set(request, response); - }); - return searchRequests.map((request) => requestResponseMap.get(request)!); -} diff --git a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts deleted file mode 100644 index 9b03ebdbd116..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts +++ /dev/null @@ -1,61 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { MockedKeys } from '@kbn/utility-types/jest'; -import { defaultSearchStrategy } from './default_search_strategy'; -import { LegacyFetchHandlers, SearchStrategySearchParams } from './types'; -import { BehaviorSubject } from 'rxjs'; - -const { search } = defaultSearchStrategy; - -describe('defaultSearchStrategy', () => { - describe('search', () => { - let searchArgs: MockedKeys; - - beforeEach(() => { - searchArgs = { - searchRequests: [ - { - index: { title: 'foo' }, - body: {}, - }, - ], - getConfig: jest.fn(), - onResponse: (req, res) => res, - legacy: { - callMsearch: jest.fn().mockResolvedValue(undefined), - loadingCount$: new BehaviorSubject(0) as any, - } as jest.Mocked, - }; - }); - - test('calls callMsearch with the correct arguments', async () => { - await search({ ...searchArgs }); - expect(searchArgs.legacy.callMsearch.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "body": Object { - "searches": Array [ - Object { - "body": Object {}, - "header": Object { - "index": "foo", - "preference": undefined, - }, - }, - ], - }, - "signal": AbortSignal {}, - }, - ], - ] - `); - }); - }); -}); diff --git a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.ts b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.ts deleted file mode 100644 index 16e109d65a5b..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getPreference } from '../fetch'; -import { SearchStrategyProvider, SearchStrategySearchParams } from './types'; - -// @deprecated -export const defaultSearchStrategy: SearchStrategyProvider = { - id: 'default', - - search: (params) => { - return msearch(params); - }, -}; - -function msearch({ searchRequests, getConfig, legacy }: SearchStrategySearchParams) { - const { callMsearch, loadingCount$ } = legacy; - - const requests = searchRequests.map(({ index, body }) => { - return { - header: { - index: index.title || index, - preference: getPreference(getConfig), - }, - body, - }; - }); - - const abortController = new AbortController(); - let resolved = false; - - // Start LoadingIndicator - loadingCount$.next(loadingCount$.getValue() + 1); - - const cleanup = () => { - if (!resolved) { - resolved = true; - // Decrement loading counter & cleanup BehaviorSubject - loadingCount$.next(loadingCount$.getValue() - 1); - loadingCount$.complete(); - } - }; - - const searching = callMsearch({ - body: { searches: requests }, - signal: abortController.signal, - }) - .then((res: any) => res?.body?.responses) - .finally(() => cleanup()); - - return { - abort: () => { - abortController.abort(); - cleanup(); - }, - searching, - }; -} diff --git a/src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts b/src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts deleted file mode 100644 index eca6e75fc69d..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts +++ /dev/null @@ -1,132 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SearchResponse } from 'elasticsearch'; -import { UI_SETTINGS } from '../../../constants'; -import { GetConfigFn } from '../../../types'; -import { FetchHandlers, SearchRequest } from '../fetch'; -import { ISearchOptions } from '../../index'; -import { callClient } from './call_client'; -import { fetchSoon } from './fetch_soon'; - -function getConfigStub(config: any = {}): GetConfigFn { - return (key) => config[key]; -} - -const mockResponses: Record> = { - foo: { - took: 1, - timed_out: false, - } as SearchResponse, - bar: { - took: 2, - timed_out: false, - } as SearchResponse, - baz: { - took: 3, - timed_out: false, - } as SearchResponse, -}; - -jest.useFakeTimers(); - -jest.mock('./call_client', () => ({ - callClient: jest.fn((requests: SearchRequest[]) => { - // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) - // in addition to how long to simulate waiting before returning a response (_waitMs) - const responses = requests.map((request) => { - const waitMs = requests.reduce((total, { _waitMs }) => total + _waitMs || 0, 0); - return new Promise((resolve) => { - setTimeout(() => { - resolve(mockResponses[request._mockResponseId]); - }, waitMs); - }); - }); - return Promise.resolve(responses); - }), -})); - -describe('fetchSoon', () => { - beforeEach(() => { - (callClient as jest.Mock).mockClear(); - }); - - test('should execute asap if config is set to not batch searches', () => { - const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false }); - const request = {}; - const options = {}; - - fetchSoon(request, options, { getConfig } as FetchHandlers); - - expect(callClient).toBeCalled(); - }); - - test('should delay by 50ms if config is set to batch searches', () => { - const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - const request = {}; - const options = {}; - - fetchSoon(request, options, { getConfig } as FetchHandlers); - - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(0); - expect(callClient).not.toBeCalled(); - jest.advanceTimersByTime(50); - expect(callClient).toBeCalled(); - }); - - test('should send a batch of requests to callClient', () => { - const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - const requests = [{ foo: 1 }, { foo: 2 }]; - const options = [{ bar: 1 }, { bar: 2 }]; - - requests.forEach((request, i) => { - fetchSoon(request, options[i] as ISearchOptions, { getConfig } as FetchHandlers); - }); - - jest.advanceTimersByTime(50); - expect(callClient).toBeCalledTimes(1); - expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(requests); - expect((callClient as jest.Mock).mock.calls[0][1]).toEqual(options); - }); - - test('should return the response to the corresponding call for multiple batched requests', async () => { - const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; - - const promises = requests.map((request) => { - return fetchSoon(request, {}, { getConfig } as FetchHandlers); - }); - jest.advanceTimersByTime(50); - const results = await Promise.all(promises); - - expect(results).toEqual([mockResponses.foo, mockResponses.bar]); - }); - - test('should wait for the previous batch to start before starting a new batch', () => { - const getConfig = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - const firstBatch = [{ foo: 1 }, { foo: 2 }]; - const secondBatch = [{ bar: 1 }, { bar: 2 }]; - - firstBatch.forEach((request) => { - fetchSoon(request, {}, { getConfig } as FetchHandlers); - }); - jest.advanceTimersByTime(50); - secondBatch.forEach((request) => { - fetchSoon(request, {}, { getConfig } as FetchHandlers); - }); - - expect(callClient).toBeCalledTimes(1); - expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(firstBatch); - - jest.advanceTimersByTime(50); - - expect(callClient).toBeCalledTimes(2); - expect((callClient as jest.Mock).mock.calls[1][0]).toEqual(secondBatch); - }); -}); diff --git a/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts b/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts deleted file mode 100644 index ff8ae2d19bd5..000000000000 --- a/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts +++ /dev/null @@ -1,83 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { estypes } from '@elastic/elasticsearch'; -import { UI_SETTINGS } from '../../../constants'; -import { FetchHandlers, SearchRequest } from '../fetch'; -import { ISearchOptions } from '../../index'; -import { callClient } from './call_client'; - -/** - * This function introduces a slight delay in the request process to allow multiple requests to queue - * up (e.g. when a dashboard is loading). - */ -export async function fetchSoon( - request: SearchRequest, - options: ISearchOptions, - fetchHandlers: FetchHandlers -) { - const msToDelay = fetchHandlers.getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; - return delayedFetch(request, options, fetchHandlers, msToDelay); -} - -/** - * Delays executing a function for a given amount of time, and returns a promise that resolves - * with the result. - * @param fn The function to invoke - * @param ms The number of milliseconds to wait - * @return Promise A promise that resolves with the result of executing the function - */ -function delay(fn: (...args: any) => T, ms: number): Promise { - return new Promise((resolve) => { - setTimeout(() => resolve(fn()), ms); - }); -} - -// The current batch/queue of requests to fetch -let requestsToFetch: SearchRequest[] = []; -let requestOptions: ISearchOptions[] = []; - -// The in-progress fetch (if there is one) -let fetchInProgress: any = null; - -/** - * Delay fetching for a given amount of time, while batching up the requests to be fetched. - * Returns a promise that resolves with the response for the given request. - * @param request The request to fetch - * @param ms The number of milliseconds to wait (and batch requests) - * @return Promise The response for the given request - */ -async function delayedFetch( - request: SearchRequest, - options: ISearchOptions, - fetchHandlers: FetchHandlers, - ms: number -): Promise> { - if (ms === 0) { - return callClient([request], [options], fetchHandlers)[0] as Promise< - estypes.SearchResponse - >; - } - - const i = requestsToFetch.length; - requestsToFetch = [...requestsToFetch, request]; - requestOptions = [...requestOptions, options]; - - // Note: the typescript here only worked because `SearchResponse` was `any` - // Since this code is legacy, I'm leaving the any here. - const responses: any[] = await (fetchInProgress = - fetchInProgress || - delay(() => { - const response = callClient(requestsToFetch, requestOptions, fetchHandlers); - requestsToFetch = []; - requestOptions = []; - fetchInProgress = null; - return response; - }, ms)); - return responses[i]; -} diff --git a/src/plugins/data/common/search/search_source/legacy/index.ts b/src/plugins/data/common/search/search_source/legacy/index.ts index 2c90dc679542..12594660136d 100644 --- a/src/plugins/data/common/search/search_source/legacy/index.ts +++ b/src/plugins/data/common/search/search_source/legacy/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { fetchSoon } from './fetch_soon'; export * from './types'; diff --git a/src/plugins/data/common/search/search_source/legacy/types.ts b/src/plugins/data/common/search/search_source/legacy/types.ts index a4328528fd66..6778be77c21c 100644 --- a/src/plugins/data/common/search/search_source/legacy/types.ts +++ b/src/plugins/data/common/search/search_source/legacy/types.ts @@ -6,9 +6,7 @@ * Side Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; import type { estypes, ApiResponse } from '@elastic/elasticsearch'; -import { FetchHandlers, SearchRequest } from '../fetch'; interface MsearchHeaders { index: string; @@ -29,27 +27,3 @@ export interface MsearchRequestBody { export interface MsearchResponse { body: ApiResponse<{ responses: Array> }>; } - -// @internal -export interface LegacyFetchHandlers { - callMsearch: (params: { - body: MsearchRequestBody; - signal: AbortSignal; - }) => Promise; - loadingCount$: BehaviorSubject; -} - -export interface SearchStrategySearchParams extends FetchHandlers { - searchRequests: SearchRequest[]; -} - -// @deprecated -export interface SearchStrategyProvider { - id: string; - search: (params: SearchStrategySearchParams) => SearchStrategyResponse; -} - -export interface SearchStrategyResponse { - searching: Promise>>; - abort: () => void; -} diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index ade22c20596d..64ed82f36e81 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { BehaviorSubject, of } from 'rxjs'; +import { of } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; @@ -47,8 +47,4 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) => getConfig: uiSettingsServiceMock.createStartContract().get, search: jest.fn(), onResponse: jest.fn().mockImplementation((req, res) => res), - legacy: { - callMsearch: jest.fn(), - loadingCount$: new BehaviorSubject(0), - }, }); diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 012fc5257397..68e386acfd48 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -6,20 +6,15 @@ * Side Public License, v 1. */ -import { BehaviorSubject, of } from 'rxjs'; +import { of } from 'rxjs'; import { IndexPattern } from '../../index_patterns'; import { GetConfigFn } from '../../types'; -import { fetchSoon } from './legacy'; import { SearchSource, SearchSourceDependencies, SortDirection } from './'; -import { AggConfigs, AggTypesRegistryStart } from '../../'; +import { AggConfigs, AggTypesRegistryStart, ES_SEARCH_STRATEGY } from '../../'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { RequestResponder } from 'src/plugins/inspector/common'; import { switchMap } from 'rxjs/operators'; -jest.mock('./legacy', () => ({ - fetchSoon: jest.fn().mockResolvedValue({}), -})); - const getComputedFields = () => ({ storedFields: [], scriptFields: {}, @@ -89,10 +84,6 @@ describe('SearchSource', () => { getConfig: getConfigMock, search: mockSearchMethod, onResponse: (req, res) => res, - legacy: { - callMsearch: jest.fn(), - loadingCount$: new BehaviorSubject(0), - }, }; searchSource = new SearchSource({}, searchSourceDependencies); @@ -869,7 +860,7 @@ describe('SearchSource', () => { }); describe('fetch$', () => { - describe('#legacy fetch()', () => { + describe('#legacy COURIER_BATCH_SEARCHES', () => { beforeEach(() => { searchSourceDependencies = { ...searchSourceDependencies, @@ -879,11 +870,22 @@ describe('SearchSource', () => { }; }); - test('should call msearch', async () => { + test('should override to use sync search if not set', async () => { searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; await searchSource.fetch$(options).toPromise(); - expect(fetchSoon).toBeCalledTimes(1); + + const [, callOptions] = mockSearchMethod.mock.calls[0]; + expect(callOptions.strategy).toBe(ES_SEARCH_STRATEGY); + }); + + test('should not override strategy if set ', async () => { + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = { strategy: 'banana' }; + await searchSource.fetch$(options).toPromise(); + + const [, callOptions] = mockSearchMethod.mock.calls[0]; + expect(callOptions.strategy).toBe('banana'); }); }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 6f34d5ce1f29..585126e1184d 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -75,7 +75,7 @@ import { estypes } from '@elastic/elasticsearch'; import { normalizeSortRequest } from './normalize_sort_request'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns'; -import { AggConfigs, ISearchGeneric, ISearchOptions } from '../..'; +import { AggConfigs, ES_SEARCH_STRATEGY, ISearchGeneric, ISearchOptions } from '../..'; import type { ISearchSource, SearchFieldValue, @@ -95,7 +95,6 @@ import { IKibanaSearchResponse, } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; -import { fetchSoon } from './legacy'; import { extractReferences } from './extract_references'; /** @internal */ @@ -274,6 +273,13 @@ export class SearchSource { */ fetch$(options: ISearchOptions = {}) { const { getConfig } = this.dependencies; + const syncSearchByDefault = getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES); + + // Use the sync search strategy if legacy search is enabled. + // This still uses bfetch for batching. + if (!options?.strategy && syncSearchByDefault) { + options.strategy = ES_SEARCH_STRATEGY; + } const s$ = defer(() => this.requestIsStarting(options)).pipe( switchMap(() => { @@ -283,9 +289,7 @@ export class SearchSource { options.indexPattern = searchRequest.index; } - return getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES) - ? from(this.legacyFetch(searchRequest, options)) - : this.fetchSearch$(searchRequest, options); + return this.fetchSearch$(searchRequest, options); }), tap((response) => { // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved @@ -477,27 +481,6 @@ export class SearchSource { ); } - /** - * Run a search using the search service - * @return {Promise>} - */ - private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) { - const { getConfig, legacy, onResponse } = this.dependencies; - - return await fetchSoon( - searchRequest, - { - ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), - ...options, - }, - { - getConfig, - onResponse, - legacy, - } - ); - } - /** * Called by requests of this search source when they are started * @param options diff --git a/src/plugins/data/common/search/search_source/search_source_service.test.ts b/src/plugins/data/common/search/search_source/search_source_service.test.ts index 9e36d3c6002d..23bb809092bd 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.test.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { SearchSourceService, SearchSourceDependencies } from './'; @@ -19,10 +18,6 @@ describe('SearchSource service', () => { getConfig: jest.fn(), search: jest.fn(), onResponse: jest.fn(), - legacy: { - callMsearch: jest.fn(), - loadingCount$: new BehaviorSubject(0), - }, }; }); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 820619aa05ed..cc7228268cb7 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -7,8 +7,7 @@ import { $Values } from '@kbn/utility-types'; import { Action } from 'history'; import { Adapters as Adapters_2 } from 'src/plugins/inspector/common'; -import { ApiResponse } from '@elastic/elasticsearch'; -import { ApiResponse as ApiResponse_2 } from '@elastic/elasticsearch/lib/Transport'; +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/plugins/data/public/search/legacy/call_msearch.test.ts b/src/plugins/data/public/search/legacy/call_msearch.test.ts deleted file mode 100644 index 0627a09e12e6..000000000000 --- a/src/plugins/data/public/search/legacy/call_msearch.test.ts +++ /dev/null @@ -1,43 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { HttpStart } from 'src/core/public'; -import { coreMock } from '../../../../../core/public/mocks'; -import { getCallMsearch } from './call_msearch'; - -describe('callMsearch', () => { - const msearchMock = jest.fn().mockResolvedValue({ body: { responses: [] } }); - let http: jest.Mocked; - - beforeEach(() => { - msearchMock.mockClear(); - http = coreMock.createStart().http; - http.post.mockResolvedValue(msearchMock); - }); - - test('calls http.post with the correct arguments', async () => { - const searches = [{ header: { index: 'foo' }, body: {} }]; - const callMsearch = getCallMsearch({ http }); - await callMsearch({ - body: { searches }, - signal: new AbortController().signal, - }); - - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/internal/_msearch", - Object { - "body": "{\\"searches\\":[{\\"header\\":{\\"index\\":\\"foo\\"},\\"body\\":{}}]}", - "signal": AbortSignal {}, - }, - ], - ] - `); - }); -}); diff --git a/src/plugins/data/public/search/legacy/call_msearch.ts b/src/plugins/data/public/search/legacy/call_msearch.ts deleted file mode 100644 index f20ae322fee5..000000000000 --- a/src/plugins/data/public/search/legacy/call_msearch.ts +++ /dev/null @@ -1,26 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { HttpStart } from 'src/core/public'; -import { LegacyFetchHandlers } from '../../../common/search/search_source'; - -/** - * Wrapper for calling the internal msearch endpoint from the client. - * This is needed to abstract away differences in the http service - * between client & server. - * - * @internal - */ -export function getCallMsearch({ http }: { http: HttpStart }): LegacyFetchHandlers['callMsearch'] { - return async ({ body, signal }) => { - return http.post('/internal/_msearch', { - body: JSON.stringify(body), - signal, - }); - }; -} diff --git a/src/plugins/data/public/search/legacy/index.ts b/src/plugins/data/public/search/legacy/index.ts deleted file mode 100644 index 52f576d1b2e3..000000000000 --- a/src/plugins/data/public/search/legacy/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './call_msearch'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 83a44b6f68af..ec7a486445b7 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -35,7 +35,6 @@ import { phraseFilterFunction, esRawResponse, } from '../../common/search'; -import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; @@ -157,10 +156,10 @@ export class SearchService implements Plugin { } public start( - { application, http, notifications, uiSettings }: CoreStart, + { http, uiSettings }: CoreStart, { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { - const search = ((request, options) => { + const search = ((request, options = {}) => { return this.searchInterceptor.search(request, options); }) as ISearchGeneric; @@ -171,10 +170,6 @@ export class SearchService implements Plugin { getConfig: uiSettings.get.bind(uiSettings), search, onResponse: handleResponse, - legacy: { - callMsearch: getCallMsearch({ http }), - loadingCount$, - }, }; return { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 0201f3226fd3..383e09b4a6eb 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { BehaviorSubject, from, Observable, throwError } from 'rxjs'; +import { from, Observable, throwError } from 'rxjs'; import { pick } from 'lodash'; import moment from 'moment'; import { @@ -36,7 +36,7 @@ import { AggsService } from './aggs'; import { FieldFormatsStart } from '../field_formats'; import { IndexPatternsServiceStart } from '../index_patterns'; -import { getCallMsearch, registerMsearchRoute, registerSearchRoute } from './routes'; +import { registerMsearchRoute, registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './strategies/es_search'; import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; @@ -237,14 +237,6 @@ export class SearchService implements Plugin { getConfig: (key: string): T => uiSettingsCache[key], search: this.asScoped(request).search, onResponse: (req, res) => res, - legacy: { - callMsearch: getCallMsearch({ - esClient, - globalConfig$: this.initializerContext.config.legacy.globalConfig$, - uiSettings: uiSettingsClient, - }), - loadingCount$: new BehaviorSubject(0), - }, }; return this.searchSourceService.start(scopedIndexPatterns, searchSourceDependencies); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index be502950a84e..15d3f5c403b1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -6,9 +6,7 @@ import { $Values } from '@kbn/utility-types'; import { Adapters } from 'src/plugins/inspector/common'; -import { ApiResponse } from '@elastic/elasticsearch'; import { Assign } from '@kbn/utility-types'; -import { BehaviorSubject } from 'rxjs'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 971ae3bb7507..78d7c15cac5d 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -276,12 +276,12 @@ export function getUiSettings(): Record> { }, [UI_SETTINGS.COURIER_BATCH_SEARCHES]: { name: i18n.translate('data.advancedSettings.courier.batchSearchesTitle', { - defaultMessage: 'Use legacy search', + defaultMessage: 'Use sync search', }), value: false, type: 'boolean', description: i18n.translate('data.advancedSettings.courier.batchSearchesText', { - defaultMessage: `Kibana uses a new search and batching infrastructure. + defaultMessage: `Kibana uses a new asynchronous search and infrastructure. Enable this option if you prefer to fallback to the legacy synchronous behavior`, }), deprecation: { diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 0078aacd6dfd..27cb3cec8be4 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -7,14 +7,14 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; -import { uiSettings } from './ui_settings'; +import { getUiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; import { searchSavedObjectType } from './saved_objects'; export class DiscoverServerPlugin implements Plugin { public setup(core: CoreSetup) { core.capabilities.registerProvider(capabilitiesProvider); - core.uiSettings.register(uiSettings); + core.uiSettings.register(getUiSettings()); core.savedObjects.registerType(searchSavedObjectType); return {}; diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 103a06965835..5f361ba2711c 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -27,7 +27,7 @@ import { MAX_DOC_FIELDS_DISPLAYED, } from '../common'; -export const uiSettings: Record = { +export const getUiSettings: () => Record = () => ({ [DEFAULT_COLUMNS_SETTING]: { name: i18n.translate('discover.advancedSettings.defaultColumnsTitle', { defaultMessage: 'Default columns', @@ -186,10 +186,17 @@ export const uiSettings: Record = { }, }, [SEARCH_FIELDS_FROM_SOURCE]: { - name: 'Read fields from _source', - description: `When enabled will load documents directly from \`_source\`. This is soon going to be deprecated. When disabled, will retrieve fields via the new Fields API in the high-level search service.`, + name: i18n.translate('discover.advancedSettings.discover.readFieldsFromSource', { + defaultMessage: 'Read fields from _source', + }), + description: i18n.translate( + 'discover.advancedSettings.discover.readFieldsFromSourceDescription', + { + defaultMessage: `When enabled will load documents directly from \`_source\`. This is soon going to be deprecated. When disabled, will retrieve fields via the new Fields API in the high-level search service.`, + } + ), value: false, category: ['discover'], schema: schema.boolean(), }, -}; +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx index 69092b2bc092..a9d6d596baf2 100644 --- a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx @@ -87,7 +87,7 @@ export function DeleteFieldModal({ fieldsToDelete, closeModal, confirmDelete }: const i18nTexts = geti18nTexts(fieldsToDelete); const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts; const isMultiple = Boolean(fieldsToDelete.length > 1); - const [confirmContent, setConfirmContent] = useState(); + const [confirmContent, setConfirmContent] = useState(''); return ( { plugins: VisTypeTimeseriesPluginSetupDependencies ) { const logger = this.initializerContext.logger.get('visTypeTimeseries'); - core.uiSettings.register(uiSettings); + core.uiSettings.register(getUiSettings()); const config$ = this.initializerContext.config.create(); // Global config contains things like the ES shard timeout const globalConfig$ = this.initializerContext.config.legacy.globalConfig$; diff --git a/src/plugins/vis_type_timeseries/server/ui_settings.ts b/src/plugins/vis_type_timeseries/server/ui_settings.ts index 07d2355b2225..e61635058cee 100644 --- a/src/plugins/vis_type_timeseries/server/ui_settings.ts +++ b/src/plugins/vis_type_timeseries/server/ui_settings.ts @@ -13,7 +13,7 @@ import { UiSettingsParams } from 'kibana/server'; import { MAX_BUCKETS_SETTING } from '../common/constants'; -export const uiSettings: Record = { +export const getUiSettings: () => Record = () => ({ [MAX_BUCKETS_SETTING]: { name: i18n.translate('visTypeTimeseries.advancedSettings.maxBucketsTitle', { defaultMessage: 'TSVB buckets limit', @@ -25,4 +25,4 @@ export const uiSettings: Record = { }), schema: schema.number(), }, -}; +}); diff --git a/src/plugins/vis_type_vislib/server/plugin.ts b/src/plugins/vis_type_vislib/server/plugin.ts index f670c81a7338..dfbb30a13dbb 100644 --- a/src/plugins/vis_type_vislib/server/plugin.ts +++ b/src/plugins/vis_type_vislib/server/plugin.ts @@ -7,11 +7,11 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; -import { uiSettings } from './ui_settings'; +import { getUiSettings } from './ui_settings'; export class VisTypeVislibServerPlugin implements Plugin { public setup(core: CoreSetup) { - core.uiSettings.register(uiSettings); + core.uiSettings.register(getUiSettings()); return {}; } diff --git a/src/plugins/vis_type_vislib/server/ui_settings.ts b/src/plugins/vis_type_vislib/server/ui_settings.ts index c0ef3f3b40e1..bd4615e47fb6 100644 --- a/src/plugins/vis_type_vislib/server/ui_settings.ts +++ b/src/plugins/vis_type_vislib/server/ui_settings.ts @@ -12,7 +12,7 @@ import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from 'kibana/server'; import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../common'; -export const uiSettings: Record = { +export const getUiSettings: () => Record = () => ({ // TODO: move this to vis_type_xy when vislib is removed // https://github.com/elastic/kibana/issues/56143 [DIMMING_OPACITY_SETTING]: { @@ -47,4 +47,4 @@ export const uiSettings: Record = { category: ['visualization'], schema: schema.number(), }, -}; +}); diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index a9e6020cf3ee..08aefdeb836b 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -13,7 +13,7 @@ import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; import { LEGACY_CHARTS_LIBRARY } from '../common'; -export const uiSettingsConfig: Record> = { +export const getUiSettingsConfig: () => Record> = () => ({ // TODO: Remove this when vis_type_vislib is removed // https://github.com/elastic/kibana/issues/56143 [LEGACY_CHARTS_LIBRARY]: { @@ -31,11 +31,11 @@ export const uiSettingsConfig: Record> = { category: ['visualization'], schema: schema.boolean(), }, -}; +}); export class VisTypeXyServerPlugin implements Plugin { public setup(core: CoreSetup) { - core.uiSettings.register(uiSettingsConfig); + core.uiSettings.register(getUiSettingsConfig()); return {}; } diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 81569c5bfc49..89889088bd73 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,8 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - // Flaky: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); diff --git a/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts b/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts index 301eb656ed6f..c01e07fd0762 100644 --- a/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts +++ b/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts @@ -8,13 +8,13 @@ interface CheerioSelector { (selector: string): CustomCheerio; (selector: string, context: string): CustomCheerio; - (selector: string, context: CheerioElement): CustomCheerio; - (selector: string, context: CheerioElement[]): CustomCheerio; - (selector: string, context: Cheerio): CustomCheerio; + (selector: string, context: cheerio.Element): CustomCheerio; + (selector: string, context: cheerio.Element[]): CustomCheerio; + (selector: string, context: cheerio.Cheerio): CustomCheerio; (selector: string, context: string, root: string): CustomCheerio; - (selector: string, context: CheerioElement, root: string): CustomCheerio; - (selector: string, context: CheerioElement[], root: string): CustomCheerio; - (selector: string, context: Cheerio, root: string): CustomCheerio; + (selector: string, context: cheerio.Element, root: string): CustomCheerio; + (selector: string, context: cheerio.Element[], root: string): CustomCheerio; + (selector: string, context: cheerio.Cheerio, root: string): CustomCheerio; (selector: any): CustomCheerio; } @@ -24,13 +24,13 @@ export interface CustomCheerioStatic extends CheerioSelector { // JQuery http://api.jquery.com xml(): string; root(): CustomCheerio; - contains(container: CheerioElement, contained: CheerioElement): boolean; + contains(container: cheerio.Element, contained: cheerio.Element): boolean; parseHTML(data: string, context?: Document, keepScripts?: boolean): Document[]; - html(options?: CheerioOptionsInterface): string; - html(selector: string, options?: CheerioOptionsInterface): string; - html(element: CustomCheerio, options?: CheerioOptionsInterface): string; - html(element: CheerioElement, options?: CheerioOptionsInterface): string; + html(options?: cheerio.CheerioParserOptions): string; + html(selector: string, options?: cheerio.CheerioParserOptions): string; + html(element: CustomCheerio, options?: cheerio.CheerioParserOptions): string; + html(element: cheerio.Element, options?: cheerio.CheerioParserOptions): string; // // CUSTOM METHODS @@ -44,7 +44,7 @@ export interface CustomCheerio { // Cheerio https://github.com/cheeriojs/cheerio // JQuery http://api.jquery.com - [index: number]: CheerioElement; + [index: number]: cheerio.Element; length: number; // Attributes @@ -63,7 +63,7 @@ export interface CustomCheerio { removeAttr(name: string): CustomCheerio; has(selector: string): CustomCheerio; - has(element: CheerioElement): CustomCheerio; + has(element: cheerio.Element): CustomCheerio; hasClass(className: string): boolean; addClass(classNames: string): CustomCheerio; @@ -81,10 +81,10 @@ export interface CustomCheerio { ): CustomCheerio; is(selector: string): boolean; - is(element: CheerioElement): boolean; - is(element: CheerioElement[]): boolean; + is(element: cheerio.Element): boolean; + is(element: cheerio.Element[]): boolean; is(selection: CustomCheerio): boolean; - is(func: (index: number, element: CheerioElement) => boolean): boolean; + is(func: (index: number, element: cheerio.Element) => boolean): boolean; // Form serialize(): string; @@ -98,7 +98,7 @@ export interface CustomCheerio { parent(selector?: string): CustomCheerio; parents(selector?: string): CustomCheerio; parentsUntil(selector?: string, filter?: string): CustomCheerio; - parentsUntil(element: CheerioElement, filter?: string): CustomCheerio; + parentsUntil(element: cheerio.Element, filter?: string): CustomCheerio; parentsUntil(element: CustomCheerio, filter?: string): CustomCheerio; prop(name: string): any; @@ -112,7 +112,7 @@ export interface CustomCheerio { nextAll(selector: string): CustomCheerio; nextUntil(selector?: string, filter?: string): CustomCheerio; - nextUntil(element: CheerioElement, filter?: string): CustomCheerio; + nextUntil(element: cheerio.Element, filter?: string): CustomCheerio; nextUntil(element: CustomCheerio, filter?: string): CustomCheerio; prev(selector?: string): CustomCheerio; @@ -120,7 +120,7 @@ export interface CustomCheerio { prevAll(selector: string): CustomCheerio; prevUntil(selector?: string, filter?: string): CustomCheerio; - prevUntil(element: CheerioElement, filter?: string): CustomCheerio; + prevUntil(element: cheerio.Element, filter?: string): CustomCheerio; prevUntil(element: CustomCheerio, filter?: string): CustomCheerio; slice(start: number, end?: number): CustomCheerio; @@ -131,19 +131,19 @@ export interface CustomCheerio { contents(): CustomCheerio; - each(func: (index: number, element: CheerioElement) => any): CustomCheerio; - map(func: (index: number, element: CheerioElement) => any): CustomCheerio; + each(func: (index: number, element: cheerio.Element) => any): CustomCheerio; + map(func: (index: number, element: cheerio.Element) => any): CustomCheerio; filter(selector: string): CustomCheerio; filter(selection: CustomCheerio): CustomCheerio; - filter(element: CheerioElement): CustomCheerio; - filter(elements: CheerioElement[]): CustomCheerio; - filter(func: (index: number, element: CheerioElement) => boolean): CustomCheerio; + filter(element: cheerio.Element): CustomCheerio; + filter(elements: cheerio.Element[]): CustomCheerio; + filter(func: (index: number, element: cheerio.Element) => boolean): CustomCheerio; not(selector: string): CustomCheerio; not(selection: CustomCheerio): CustomCheerio; - not(element: CheerioElement): CustomCheerio; - not(func: (index: number, element: CheerioElement) => boolean): CustomCheerio; + not(element: cheerio.Element): CustomCheerio; + not(func: (index: number, element: cheerio.Element) => boolean): CustomCheerio; first(): CustomCheerio; last(): CustomCheerio; @@ -161,8 +161,8 @@ export interface CustomCheerio { add(selectorOrHtml: string): CustomCheerio; add(selector: string, context: Document): CustomCheerio; - add(element: CheerioElement): CustomCheerio; - add(elements: CheerioElement[]): CustomCheerio; + add(element: cheerio.Element): CustomCheerio; + add(elements: cheerio.Element[]): CustomCheerio; add(selection: CustomCheerio): CustomCheerio; addBack(): CustomCheerio; @@ -203,8 +203,8 @@ export interface CustomCheerio { remove(selector?: string): CustomCheerio; replaceWith(content: string): CustomCheerio; - replaceWith(content: CheerioElement): CustomCheerio; - replaceWith(content: CheerioElement[]): CustomCheerio; + replaceWith(content: cheerio.Element): CustomCheerio; + replaceWith(content: cheerio.Element[]): CustomCheerio; replaceWith(content: CustomCheerio): CustomCheerio; replaceWith(content: () => CustomCheerio): CustomCheerio; @@ -236,7 +236,7 @@ export interface CustomCheerio { // Not Documented - toArray(): CheerioElement[]; + toArray(): CustomCheerio[]; // // CUSTOM METHODS diff --git a/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.test.ts b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.test.ts similarity index 100% rename from x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.test.ts rename to x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.test.ts diff --git a/x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts similarity index 100% rename from x-pack/plugins/apm/server/utils/offset_previous_period_coordinate.ts rename to x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap index a3074bf66a05..58d5287e084c 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap @@ -1,352 +1,278 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ErrorGroupOverview -> List should render empty state 1`] = ` - - Group ID - - - , - "render": [Function], - "sortable": false, - "width": "96px", - }, - Object { - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - }, - Object { - "field": "message", - "name": "Error message and culprit", - "render": [Function], - "sortable": false, - "width": "50%", - }, - Object { - "align": "right", - "field": "handled", - "name": "", - "render": [Function], - "sortable": false, - }, - Object { - "dataType": "number", - "field": "occurrenceCount", - "name": "Occurrences", - "render": [Function], - "sortable": true, - }, - Object { - "align": "right", - "field": "latestOccurrenceAt", - "name": "Latest occurrence", - "render": [Function], - "sortable": true, - }, - ] - } - initialPageSize={25} - initialSortDirection="desc" - initialSortField="occurrenceCount" - items={Array []} - noItemsMessage="No errors were found" - sortItems={false} +
-
-
+
+
- +
-
-
-
- -
-
+ Sorting + + +
- +
- - - - + +
-
+ + + + - + - + - - - + - - - - - + + + + - - -
+
+
-
+ Group ID + - Group ID - - - + aria-label="Info" + className="eui-alignTop" + color="subdued" + data-euiicon-type="questionInCircle" + onBlur={[Function]} + onFocus={[Function]} + size="s" + tabIndex={0} + /> -
-
+ + +
-
- - Type - -
-
+ + +
-
- - Error message and culprit - -
-
+ + + +
-
- -
-
+ + + + - + + + Click to sort in ascending order + + + + -
+ + Click to sort in ascending order + + + +
+
-
- - No errors were found - -
-
- + No errors were found + + + +
- +
`; exports[`ErrorGroupOverview -> List should render with data 1`] = ` @@ -381,590 +307,524 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } - - Group ID - - - , - "render": [Function], - "sortable": false, - "width": "96px", - }, - Object { - "field": "type", - "name": "Type", - "render": [Function], - "sortable": false, - }, - Object { - "field": "message", - "name": "Error message and culprit", - "render": [Function], - "sortable": false, - "width": "50%", - }, - Object { - "align": "right", - "field": "handled", - "name": "", - "render": [Function], - "sortable": false, - }, - Object { - "dataType": "number", - "field": "occurrenceCount", - "name": "Occurrences", - "render": [Function], - "sortable": true, - }, - Object { - "align": "right", - "field": "latestOccurrenceAt", - "name": "Latest occurrence", - "render": [Function], - "sortable": true, - }, - ] - } - initialPageSize={25} - initialSortDirection="desc" - initialSortField="occurrenceCount" - items={ - Array [ - Object { - "culprit": "elasticapm.contrib.django.client.capture", - "groupId": "a0ce2c8978ef92cdf2ff163ae28576ee", - "handled": true, - "latestOccurrenceAt": "2018-01-10T10:06:37.561Z", - "message": "About to blow up!", - "occurrenceCount": 75, - "type": "AssertionError", - }, - Object { - "culprit": "opbeans.views.oopsie", - "groupId": "f3ac95493913cc7a3cfec30a19d2120a", - "handled": true, - "latestOccurrenceAt": "2018-01-10T10:06:37.630Z", - "message": "AssertionError: ", - "occurrenceCount": 75, - "type": "AssertionError", - }, - Object { - "culprit": "opbeans.tasks.update_stats", - "groupId": "e90863d04b7a692435305f09bbe8c840", - "handled": true, - "latestOccurrenceAt": "2018-01-10T10:06:36.859Z", - "message": "AssertionError: Bad luck!", - "occurrenceCount": 24, - "type": "AssertionError", - }, - Object { - "culprit": "opbeans.views.customer", - "groupId": "8673d8bf7a032e387c101bafbab0d2bc", - "handled": true, - "latestOccurrenceAt": "2018-01-10T10:06:13.211Z", - "message": "Customer with ID 8517 not found", - "occurrenceCount": 15, - "type": "AssertionError", - }, - ] - } - noItemsMessage="No errors were found" - sortItems={false} +
-
-
+
+
- +
-
-
-
- -
-
+ Sorting + + +
- +
- - - - + +
-
+ + + + - + - + - + + - + + + + + - - - + + + + + + - + + + - + + + - + - - + - - - - + + + - + + + - + - - + - - - - + + + - + + + - + - - + - - - - + - + - - + - - - -
+
+
-
+ Group ID + - Group ID - - - + aria-label="Info" + className="eui-alignTop" + color="subdued" + data-euiicon-type="questionInCircle" + onBlur={[Function]} + onFocus={[Function]} + size="s" + tabIndex={0} + /> -
-
+ + +
-
- - Type - -
-
+ + +
-
- - Error message and culprit - -
-
+ + + +
+ +
+
+ + + Click to sort in ascending order + + + + - + + Click to sort in ascending order + + +
+
- - -
- + Type + + + +
+ Error message and culprit +
+
- Group ID - - + rel="noreferrer" + > + About to blow up! + -
-
- + - - - a0ce2 - - - + elasticapm.contrib.django.client.capture +
+
-
+ +
+
+
-
- Type -
-
+
+ 75 +
+
+
+ Latest occurrence +
+
+ - - - - AssertionError - - - -
-
+ +
+
-
- Error message and culprit -
-
+ +
+
+ - -
- - - - - About to blow up! - - - - -
- - -
- elasticapm.contrib.django.client.capture -
-
-
-
- -
-
+ + +
-
-
+
-
- Occurrences -
-
- 75 -
-
+ + +
+ Error message and culprit +
+
- Latest occurrence -
-
List should render with data 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} > - 1337 minutes ago (mocking 1515578797) + + AssertionError: + -
-
-
- Group ID - +
- - -
-
- - - - f3ac9 - - - + opbeans.views.oopsie +
+ -
+ +
+
+
-
- Type -
-
+
+ 75 +
+
+
+ Latest occurrence +
+
+ - - - - AssertionError - - - -
-
+ +
+
-
- Error message and culprit -
-
+ +
+
+ - -
- - - - - AssertionError: - - - - -
- - -
- opbeans.views.oopsie -
-
-
-
- -
-
+ + +
-
-
+
-
- Occurrences -
-
- 75 -
-
+ + +
+ Error message and culprit +
+
- Latest occurrence -
-
List should render with data 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} > - 1337 minutes ago (mocking 1515578797) + + AssertionError: Bad luck! + -
-
-
- Group ID - +
- - -
-
- - - - e9086 - - - + opbeans.tasks.update_stats +
+ -
+ +
+
+
-
- Type -
-
+
+ 24 +
+
+
+ Latest occurrence +
+
+ - - - - AssertionError - - - -
-
+ +
+
-
- Error message and culprit -
-
+ +
+
+ - -
- - - - - AssertionError: Bad luck! - - - - -
- - -
- opbeans.tasks.update_stats -
-
-
-
- -
-
+ + +
-
-
+
-
- Occurrences -
-
- 24 -
-
+ + +
+ Error message and culprit +
+
- Latest occurrence -
-
List should render with data 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} > - 1337 minutes ago (mocking 1515578796) + + Customer with ID 8517 not found + -
-
-
- Group ID - +
- - -
-
- - - - 8673d - - - + opbeans.views.customer +
+ -
+ -
- Type -
- -
+
+
+
-
- Error message and culprit -
-
- -
- - - - - Customer with ID 8517 not found - - - - -
- - -
- opbeans.views.customer -
-
-
-
-
-
-
+
-
-
+ +
-
- Occurrences -
-
- 15 -
-
+
-
- Latest occurrence -
-
- - 1337 minutes ago (mocking 1515578773) - -
-
- -
+ 1337 minutes ago (mocking 1515578773) + +
+ +
+
+
+
+
- -
-
-
+
-
- + + + + +
- +
`; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 4ff42b151dc8..55da021e0468 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -13,7 +13,10 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { keyBy } from 'lodash'; import React from 'react'; +import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/offset_previous_period_coordinate'; +import { Coordinate } from '../../../../../typings/timeseries'; import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; import { asMillisecondDuration, @@ -32,6 +35,7 @@ import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink'; import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link'; import { SpanIcon } from '../../../shared/span_icon'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; @@ -39,12 +43,68 @@ interface Props { serviceName: string; } +type ServiceDependencyPeriods = ServiceDependencyItem & { + latency: { previousPeriodTimeseries?: Coordinate[] }; + throughput: { previousPeriodTimeseries?: Coordinate[] }; + errorRate: { previousPeriodTimeseries?: Coordinate[] }; + previousPeriodImpact?: number; +}; + +function mergeCurrentAndPreviousPeriods({ + currentPeriod = [], + previousPeriod = [], +}: { + currentPeriod?: ServiceDependencyItem[]; + previousPeriod?: ServiceDependencyItem[]; +}): ServiceDependencyPeriods[] { + const previousPeriodMap = keyBy(previousPeriod, 'name'); + + return currentPeriod.map((currentDependency) => { + const previousDependency = previousPeriodMap[currentDependency.name]; + if (!previousDependency) { + return currentDependency; + } + return { + ...currentDependency, + latency: { + ...currentDependency.latency, + previousPeriodTimeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentDependency.latency.timeseries, + previousPeriodTimeseries: previousDependency.latency?.timeseries, + }), + }, + throughput: { + ...currentDependency.throughput, + previousPeriodTimeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentDependency.throughput.timeseries, + previousPeriodTimeseries: previousDependency.throughput?.timeseries, + }), + }, + errorRate: { + ...currentDependency.errorRate, + previousPeriodTimeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentDependency.errorRate.timeseries, + previousPeriodTimeseries: previousDependency.errorRate?.timeseries, + }), + }, + previousPeriodImpact: previousDependency.impact, + }; + }); +} + export function ServiceOverviewDependenciesTable({ serviceName }: Props) { const { - urlParams: { start, end, environment }, + urlParams: { start, end, environment, comparisonEnabled, comparisonType }, } = useUrlParams(); - const columns: Array> = [ + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonEnabled, + comparisonType, + }); + + const columns: Array> = [ { field: 'name', name: i18n.translate( @@ -102,6 +162,9 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { ); @@ -121,6 +184,11 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { compact color="euiColorVis0" series={throughput.timeseries} + comparisonSeries={ + comparisonEnabled + ? throughput.previousPeriodTimeseries + : undefined + } valueLabel={asTransactionRate(throughput.value)} /> ); @@ -142,6 +210,9 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { compact color="euiColorVis7" series={errorRate.timeseries} + comparisonSeries={ + comparisonEnabled ? errorRate.previousPeriodTimeseries : undefined + } valueLabel={asPercent(errorRate.value, 1)} /> ); @@ -157,13 +228,28 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { } ), width: px(unit * 5), - render: (_, { impact }) => { - return ; + render: (_, { impact, previousPeriodImpact }) => { + return ( + + + + + {comparisonEnabled && previousPeriodImpact !== undefined && ( + + + + )} + + ); }, sortable: true, }, ]; - + // Fetches current period dependencies const { data, status } = useFetcher( (callApmApi) => { if (!start || !end) { @@ -173,22 +259,41 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/dependencies', params: { - path: { - serviceName, - }, + path: { serviceName }, + query: { start, end, environment, numBuckets: 20 }, + }, + }); + }, + [start, end, serviceName, environment] + ); + + // Fetches previous period dependencies + const { data: previousPeriodData, status: previousPeriodStatus } = useFetcher( + (callApmApi) => { + if (!comparisonStart || !comparisonEnd) { + return; + } + + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/dependencies', + params: { + path: { serviceName }, query: { - start, - end, + start: comparisonStart, + end: comparisonEnd, environment, numBuckets: 20, }, }, }); }, - [start, end, serviceName, environment] + [comparisonStart, comparisonEnd, serviceName, environment] ); - const serviceDependencies = data?.serviceDependencies ?? []; + const serviceDependencies = mergeCurrentAndPreviousPeriods({ + currentPeriod: data?.serviceDependencies, + previousPeriod: previousPeriodData?.serviceDependencies, + }); // need top-level sortable fields for the managed table const items = serviceDependencies.map((item) => ({ @@ -238,7 +343,10 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { columns={columns} items={items} allowNeutralSort={false} - loading={status === FETCH_STATUS.LOADING} + loading={ + status === FETCH_STATUS.LOADING || + previousPeriodStatus === FETCH_STATUS.LOADING + } pagination={{ initialPageSize: 5, pageSizeOptions: [5], diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index 8305b5a0dde3..8513e0835d37 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -33,15 +33,16 @@ export interface MainStatsServiceInstanceItem { cpuUsage: number; memoryUsage: number; } +type ApiResponseMainStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics'>; +type ApiResponseDetailedStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; const INITIAL_STATE_MAIN_STATS = { - mainStatsItems: [] as MainStatsServiceInstanceItem[], - mainStatsRequestId: undefined, - mainStatsItemCount: 0, + currentPeriodItems: [] as ApiResponseMainStats['currentPeriod'], + previousPeriodItems: [] as ApiResponseMainStats['previousPeriod'], + requestId: undefined, + currentPeriodItemsCount: 0, }; -type ApiResponseDetailedStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; - const INITIAL_STATE_DETAILED_STATISTICS: ApiResponseDetailedStats = { currentPeriod: {}, previousPeriod: {}, @@ -117,28 +118,17 @@ export function ServiceOverviewInstancesChartAndTable({ start, end, transactionType, + comparisonStart, + comparisonEnd, }, }, }).then((response) => { - const mainStatsItems = orderBy( - // need top-level sortable fields for the managed table - response.serviceInstances.map((item) => ({ - ...item, - latency: item.latency ?? 0, - throughput: item.throughput ?? 0, - errorRate: item.errorRate ?? 0, - cpuUsage: item.cpuUsage ?? 0, - memoryUsage: item.memoryUsage ?? 0, - })), - field, - direction - ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); - return { // Everytime the main statistics is refetched, updates the requestId making the detailed API to be refetched. - mainStatsRequestId: uuid(), - mainStatsItems, - mainStatsItemCount: response.serviceInstances.length, + requestId: uuid(), + currentPeriodItems: response.currentPeriod, + currentPeriodItemsCount: response.currentPeriod.length, + previousPeriodItems: response.previousPeriod, }; }); }, @@ -162,11 +152,26 @@ export function ServiceOverviewInstancesChartAndTable({ ); const { - mainStatsItems, - mainStatsRequestId, - mainStatsItemCount, + currentPeriodItems, + previousPeriodItems, + requestId, + currentPeriodItemsCount, } = mainStatsData; + const currentPeriodOrderedItems = orderBy( + // need top-level sortable fields for the managed table + currentPeriodItems.map((item) => ({ + ...item, + latency: item.latency ?? 0, + throughput: item.throughput ?? 0, + errorRate: item.errorRate ?? 0, + cpuUsage: item.cpuUsage ?? 0, + memoryUsage: item.memoryUsage ?? 0, + })), + field, + direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + const { data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS, status: detailedStatsStatus, @@ -177,7 +182,7 @@ export function ServiceOverviewInstancesChartAndTable({ !end || !transactionType || !latencyAggregationType || - !mainStatsItemCount + !currentPeriodItemsCount ) { return; } @@ -198,7 +203,7 @@ export function ServiceOverviewInstancesChartAndTable({ numBuckets: 20, transactionType, serviceNodeIds: JSON.stringify( - mainStatsItems.map((item) => item.serviceNodeName) + currentPeriodOrderedItems.map((item) => item.serviceNodeName) ), comparisonStart, comparisonEnd, @@ -208,7 +213,7 @@ export function ServiceOverviewInstancesChartAndTable({ }, // only fetches detailed statistics when requestId is invalidated by main statistics api call // eslint-disable-next-line react-hooks/exhaustive-deps - [mainStatsRequestId], + [requestId], { preservePreviousData: false } ); @@ -217,16 +222,17 @@ export function ServiceOverviewInstancesChartAndTable({

{i18n.translate('xpack.apm.serviceOverview.instancesTableTitle', { - defaultMessage: 'All instances', + defaultMessage: 'Instances', })}

diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx index 02aad49ddfc9..9ac1c7d64d8b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -162,7 +162,7 @@ export function getColumns({ - {comparisonEnabled && previousImpact && ( + {comparisonEnabled && previousImpact !== undefined && ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx index 394d5b5410d4..ce4f36ced790 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx @@ -30,33 +30,32 @@ import { } from '../../../../../common/utils/formatters'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { MainStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import * as urlHelpers from '../../Links/url_helpers'; import { ChartContainer } from '../chart_container'; import { getResponseTimeTickFormatter } from '../transaction_charts/helper'; import { CustomTooltip } from './custom_tooltip'; +type ApiResponseMainStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics'>; + export interface InstancesLatencyDistributionChartProps { height: number; - items?: MainStatsServiceInstanceItem[]; + items?: ApiResponseMainStats['currentPeriod']; status: FETCH_STATUS; + comparisonItems?: ApiResponseMainStats['previousPeriod']; } export function InstancesLatencyDistributionChart({ height, items = [], status, + comparisonItems = [], }: InstancesLatencyDistributionChartProps) { const history = useHistory(); const hasData = items.length > 0; const theme = useTheme(); - const chartTheme = { - ...useChartTheme(), - bubbleSeriesStyle: { - point: { strokeWidth: 0, fill: theme.eui.euiColorVis1, radius: 4 }, - }, - }; + const chartTheme = useChartTheme(); const maxLatency = Math.max(...items.map((item) => item.latency ?? 0)); const latencyFormatter = getDurationFormatter(maxLatency); @@ -96,7 +95,13 @@ export function InstancesLatencyDistributionChart({ // there's just a single instance) they'll show along the origin. Make sure // the x-axis domain is [0, maxThroughput]. const maxThroughput = Math.max(...items.map((item) => item.throughput ?? 0)); - const xDomain = { min: 0, max: maxThroughput }; + const maxComparisonThroughput = Math.max( + ...comparisonItems.map((item) => item.throughput ?? 0) + ); + const xDomain = { + min: 0, + max: Math.max(maxThroughput, maxComparisonThroughput), + }; return ( @@ -118,7 +123,7 @@ export function InstancesLatencyDistributionChart({ xDomain={xDomain} /> item.latency]} yScaleType={ScaleType.Linear} + bubbleSeriesStyle={{ + point: { + strokeWidth: 0, + radius: 4, + fill: theme.eui.euiColorVis0, + }, + }} /> + + {!!comparisonItems.length && ( + item.throughput} + xScaleType={ScaleType.Linear} + yAccessors={[(item) => item.latency]} + yScaleType={ScaleType.Linear} + color={theme.eui.euiColorMediumShade} + bubbleSeriesStyle={{ + point: { + shape: 'square', + radius: 4, + fill: theme.eui.euiColorLightestShade, + stroke: theme.eui.euiColorMediumShade, + strokeWidth: 2, + }, + }} + /> + )} { expect(wrapper.find(EuiAccordion)).toHaveLength(1); expect(wrapper.find(EuiTableRow)).toHaveLength(2); - expect(wrapper.find(EuiLinkTo)).toHaveLength(0); + expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(0); }); it('renders document buttons', () => { const wrapper = shallow(); - expect(wrapper.find(EuiLinkTo)).toHaveLength(2); + expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(2); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx index c41781deafb9..09f499e540e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiAccordion, + EuiButton, EuiFlexGroup, EuiFlexItem, EuiTable, @@ -19,10 +20,12 @@ import { EuiTableRowCell, } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonEmptyTo } from '../react_router_helpers'; import { TruncatedContent } from '../truncate'; +import './schema_errors_accordion.scss'; + import { ERROR_TABLE_ID_HEADER, ERROR_TABLE_ERROR_HEADER, @@ -60,14 +63,19 @@ export const SchemaErrorsAccordion: React.FC = ({ - - + + + + - {schema[fieldName]} + {schema[fieldName]} - {ERROR_TABLE_REVIEW_CONTROL} + {/* href is needed here because a button cannot be nested in a button or console will error and EuiAccordion uses a button to wrap this. */} + + {ERROR_TABLE_REVIEW_CONTROL} + ); @@ -76,12 +84,12 @@ export const SchemaErrorsAccordion: React.FC = ({ - + {ERROR_TABLE_ID_HEADER} {ERROR_TABLE_ERROR_HEADER} @@ -93,34 +101,21 @@ export const SchemaErrorsAccordion: React.FC = ({ const documentPath = getRoute && itemId ? getRoute(itemId, error.external_id) : ''; const viewButton = showViewButton && ( - - - - {ERROR_TABLE_VIEW_LINK} - - + + {ERROR_TABLE_VIEW_LINK} ); return ( - - -
- -
-
- - {error.error} + + + + {error.error} {showViewButton ? viewButton : } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index 29cb2b758922..7f7b26e380c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -10,8 +10,6 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiSpacer } from '@elastic/eui'; - import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; @@ -32,16 +30,13 @@ export const SchemaChangeErrors: React.FC = () => { }, []); return ( -
+ <> - -
- -
-
+ + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index a9712cc4e1dc..2cf867446b7f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -33,6 +33,7 @@ describe('SourceLogic', () => { flashAPIErrors, setSuccessMessage, setQueuedSuccessMessage, + setErrorMessage, } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; const { mount, getListeners } = new LogicMounter(SourceLogic); @@ -204,6 +205,19 @@ describe('SourceLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith(NOT_FOUND_PATH); }); + + it('renders error messages passed in success response from server', async () => { + const errors = ['ERROR']; + const promise = Promise.resolve({ + ...contentSource, + errors, + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await promise; + + expect(setErrorMessage).toHaveBeenCalledWith(errors); + }); }); describe('initializeFederatedSummary', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index ff3e1e83925d..2e6a3c65597e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -13,6 +13,7 @@ import { DEFAULT_META } from '../../../shared/constants'; import { flashAPIErrors, setSuccessMessage, + setErrorMessage, setQueuedSuccessMessage, clearFlashMessages, } from '../../../shared/flash_messages'; @@ -148,6 +149,11 @@ export const SourceLogic = kea>({ if (response.isFederatedSource) { actions.initializeFederatedSummary(sourceId); } + if (response.errors) { + setErrorMessage(response.errors); + } else { + clearFlashMessages(); + } } catch (e) { if (e.response.status === 404) { KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 463468d1304b..528065da23af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -8,6 +8,8 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { mockLocation } from '../../../__mocks__/react_router_history.mock'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; @@ -30,6 +32,7 @@ import { SourceRouter } from './source_router'; describe('SourceRouter', () => { const initializeSource = jest.fn(); + const resetSourceState = jest.fn(); const contentSource = contentSources[1]; const customSource = contentSources[0]; const mockValues = { @@ -40,10 +43,11 @@ describe('SourceRouter', () => { beforeEach(() => { setMockActions({ initializeSource, + resetSourceState, }); setMockValues({ ...mockValues }); (useParams as jest.Mock).mockImplementationOnce(() => ({ - sourceId: '1', + sourceId: contentSource.id, })); }); @@ -114,4 +118,22 @@ describe('SourceRouter', () => { NAV.DISPLAY_SETTINGS, ]); }); + + describe('reset state', () => { + it('does not reset state when switching between source tree views', () => { + mockLocation.pathname = `/sources/${contentSource.id}`; + shallow(); + unmountHandler(); + + expect(resetSourceState).not.toHaveBeenCalled(); + }); + + it('resets state when leaving source tree', () => { + mockLocation.pathname = '/home'; + shallow(); + unmountHandler(); + + expect(resetSourceState).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index b14ea4ebd7a7..cd20e32def16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -6,7 +6,8 @@ */ import React, { useEffect } from 'react'; -import { Route, Switch, useParams } from 'react-router-dom'; + +import { Route, Switch, useLocation, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import moment from 'moment'; @@ -47,14 +48,18 @@ import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { const { sourceId } = useParams() as { sourceId: string }; + const { pathname } = useLocation(); const { initializeSource, resetSourceState } = useActions(SourceLogic); const { contentSource, dataLoading } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); useEffect(() => { initializeSource(sourceId); - return resetSourceState; - }, []); + return () => { + // We only want to reset the state when leaving the source section. Otherwise there is an unwanted flash of UI. + if (!pathname.includes(sourceId)) resetSourceState(); + }; + }, [pathname]); if (dataLoading) return ; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index e5394fd580ef..2fc0a13f2ff7 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -79,9 +79,7 @@ export class EnterpriseSearchRequestHandler { // Set up API options const { method } = request.route; const headers = { Authorization: request.headers.authorization as string, ...JSON_HEADER }; - const body = !this.isEmptyObj(request.body as object) - ? JSON.stringify(request.body) - : undefined; + const body = this.getBodyAsString(request.body as object | Buffer); // Call the Enterprise Search API const apiResponse = await fetch(url, { method, headers, body }); @@ -147,6 +145,18 @@ export class EnterpriseSearchRequestHandler { }; } + /** + * There are a number of different expected incoming bodies that we handle & pass on to Enterprise Search for ingestion: + * - Standard object data (should be JSON stringified) + * - Empty (should be passed as undefined and not as an empty obj) + * - Raw buffers (passed on as a string, occurs when using the `skipBodyValidation` lib helper) + */ + getBodyAsString(body: object | Buffer): string | undefined { + if (Buffer.isBuffer(body)) return body.toString(); + if (this.isEmptyObj(body)) return undefined; + return JSON.stringify(body); + } + /** * This path helper is similar to React Router's generatePath, but much simpler & * does not use regexes. It enables us to pass a static '/foo/:bar/baz' string to diff --git a/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.test.ts b/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.test.ts new file mode 100644 index 000000000000..1843d5f942e3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +const mockBuffer = {}; +jest.mock('@kbn/config-schema', () => ({ + schema: { + buffer: () => mockBuffer, + object: () => ({}), + }, +})); + +const mockSchema = schema.object({}); + +import { skipBodyValidation } from './route_config_helpers'; + +describe('skipBodyValidation', () => { + it('adds "options.body.parse" and "validate.body" properties to a route config', () => { + expect( + skipBodyValidation({ + path: '/example/path', + validate: {}, + }) + ).toEqual({ + path: '/example/path', + validate: { + body: mockBuffer, + }, + options: { body: { parse: false } }, + }); + }); + + it('persists all other properties, e.g. "path" & other non-"body" properties on "options" & "validate"', () => { + expect( + skipBodyValidation({ + path: '/example/path', + validate: { + params: mockSchema, + }, + options: { + authRequired: true, + }, + }) + ).toEqual({ + path: '/example/path', + validate: { + params: mockSchema, + body: mockBuffer, + }, + options: { + authRequired: true, + body: { parse: false }, + }, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.ts b/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.ts new file mode 100644 index 000000000000..ab5bbc994a10 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/route_config_helpers.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DestructiveRouteMethod, + RouteConfig, + RouteConfigOptions, + RouteMethod, + RouteValidatorFullConfig, +} from 'kibana/server'; + +import { schema } from '@kbn/config-schema'; + +type Config = RouteConfig; + +// We disallow options to set 'body' because we override them. +interface ConfigWithoutBodyOptions + extends RouteConfig { + validate: Omit, 'body'>; + options?: Omit, 'body'>; +} + +/** + * Kibana Enterprise Search Plugin API endpoints often times pass through the request + * body to the Enterprise Search API endpoints for validation. In those cases, we do not + * need to validate them in Kibana. + * + * The safe way to do that is to turn off body parsing entirely using `options.body.parse: false`. + * The will pass a String Buffer to the route handler. The proper way to validate this when validation + * is enabled to to use `body: schema.buffer()`. + * + * @see https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.routeconfigoptionsbody.md + * @see https://github.com/elastic/kibana/blob/master/packages/kbn-config-schema/README.md#schemabuffer + * + * Example: + * router.put({ + * path: '/api/app_search/engines/{engineName}/example', + * validate: { + * params: schema.object({ + * engineName: schema.string(), + * }), + * body: schema.buffer(), + * }, + * options: { body: { parse: false } }, + * }, + * ... + * + * This helper applies that pattern, while maintaining existing options: + * + * router.put(skipBodyValidation({ + * path: '/api/app_search/engines/{engineName}/example', + * validate: { + * params: schema.object({ + * engineName: schema.string(), + * }), + * }, + * }, + * ... + */ +export const skipBodyValidation = ( + // DestructiveRouteMethod is the Kibana type for everything except 'get' and 'options'. + // Body configuration doesn't apply to those types so we disallow it with this helper. + config: ConfigWithoutBodyOptions +): Config => { + return { + ...config, + validate: { + ...config.validate, + body: schema.buffer(), + }, + options: { + ...(config.options || {}), + body: { + parse: false, + } as RouteConfigOptions['body'], + }, + }; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts index af54d340ad15..b584412a7a8f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -31,28 +31,6 @@ describe('documents routes', () => { path: '/as/engines/:engineName/documents/new', }); }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: { documents: [{ foo: 'bar' }] } }; - mockRouter.shouldValidate(request); - }); - - it('missing documents', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - - it('wrong document type', () => { - const request = { body: { documents: ['test'] } }; - mockRouter.shouldThrow(request); - }); - - it('non-array documents type', () => { - const request = { body: { documents: { foo: 'bar' } } }; - mockRouter.shouldThrow(request); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts index 78463fc8724a..929e9f7aef69 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -7,6 +7,8 @@ import { schema } from '@kbn/config-schema'; +import { skipBodyValidation } from '../../lib/route_config_helpers'; + import { RouteDependencies } from '../../plugin'; export function registerDocumentsRoutes({ @@ -14,17 +16,14 @@ export function registerDocumentsRoutes({ enterpriseSearchRequestHandler, }: RouteDependencies) { router.post( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/documents', validate: { params: schema.object({ engineName: schema.string(), }), - body: schema.object({ - documents: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), - }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/documents/new', }) diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index e38380d60c6e..eab4acd1ea4e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -71,22 +71,6 @@ describe('result settings routes', () => { path: '/as/engines/:engineName/result_settings', }); }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - result_fields: resultFields, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); }); describe('POST /api/app_search/engines/{name}/sample_response_search', () => { @@ -115,21 +99,5 @@ describe('result settings routes', () => { path: '/as/engines/:engineName/sample_response_search', }); }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - query: 'test', - result_fields: resultFields, - }, - }; - mockRouter.shouldValidate(request); - }); - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index b091ae7a539c..9a81b8a9f590 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; +import { skipBodyValidation } from '../../lib/route_config_helpers'; -const resultFields = schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })); +import { RouteDependencies } from '../../plugin'; export function registerResultSettingsRoutes({ router, @@ -30,35 +30,28 @@ export function registerResultSettingsRoutes({ ); router.put( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/result_settings', validate: { params: schema.object({ engineName: schema.string(), }), - body: schema.object({ - result_fields: resultFields, - }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/result_settings', }) ); router.post( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/sample_response_search', validate: { params: schema.object({ engineName: schema.string(), }), - body: schema.object({ - query: schema.string(), - result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), - }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/sample_response_search', }) diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts index 26204916deec..0c11a10ea10f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts @@ -107,18 +107,6 @@ describe('search settings routes', () => { path: '/as/engines/:engineName/search_settings', }); }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: searchSettings }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); }); describe('POST /api/app_search/engines/{name}/search_settings/reset', () => { @@ -169,23 +157,6 @@ describe('search settings routes', () => { }); }); - describe('validates body', () => { - it('correctly', () => { - const request = { - body: { - boosts, - search_fields: searchFields, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - describe('validates query', () => { it('correctly', () => { const request = { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts index 7291f7cfe64f..c8801d608973 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts @@ -7,23 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; - -// We only do a very light type check here, and allow unknowns, because the request is validated -// on the ent-search server, so it would be redundant to check it here as well. -const boosts = schema.recordOf( - schema.string(), - schema.arrayOf(schema.object({}, { unknowns: 'allow' })) -); -const resultFields = schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })); -const searchFields = schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })); +import { skipBodyValidation } from '../../lib/route_config_helpers'; -const searchSettingsSchema = schema.object({ - boosts, - result_fields: resultFields, - search_fields: searchFields, - precision: schema.number(), -}); +import { RouteDependencies } from '../../plugin'; export function registerSearchSettingsRoutes({ router, @@ -58,36 +44,31 @@ export function registerSearchSettingsRoutes({ ); router.put( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/search_settings', validate: { params: schema.object({ engineName: schema.string(), }), - body: searchSettingsSchema, }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/search_settings', }) ); router.post( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/search_settings_search', validate: { params: schema.object({ engineName: schema.string(), }), - body: schema.object({ - boosts: schema.maybe(boosts), - search_fields: searchFields, - }), query: schema.object({ query: schema.string(), }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/search_settings_search', }) diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 793a349f730f..8d75726fbe2d 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -43,8 +43,13 @@ export class PackageOperationNotSupportedError extends IngestManagerError {} export class FleetAdminUserInvalidError extends IngestManagerError {} export class ConcurrentInstallOperationError extends IngestManagerError {} export class AgentReassignmentError extends IngestManagerError {} -export class AgentUnenrollmentError extends IngestManagerError {} -export class AgentPolicyDeletionError extends IngestManagerError {} +export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError { + constructor(message = 'Cannot perform that action') { + super( + `${message} in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.` + ); + } +} export class FleetSetupError extends IngestManagerError {} export class GenerateServiceTokenError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index deb2da8dee55..c0a2e80af4bf 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -46,11 +46,7 @@ import type { Installation, Output, } from '../../common'; -import { - AgentPolicyNameExistsError, - AgentPolicyDeletionError, - IngestManagerError, -} from '../errors'; +import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; @@ -216,6 +212,7 @@ class AgentPolicyService { SAVED_OBJECT_TYPE, { ...agentPolicy, + status: 'active', is_managed: agentPolicy.is_managed ?? false, revision: 1, updated_at: new Date().toISOString(), @@ -476,7 +473,9 @@ class AgentPolicyService { } if (oldAgentPolicy.is_managed && !options?.force) { - throw new IngestManagerError(`Cannot update integrations of hosted agent policy ${id}`); + throw new HostedAgentPolicyRestrictionRelatedError( + `Cannot update integrations of hosted agent policy ${id}` + ); } return await this._update( @@ -507,7 +506,9 @@ class AgentPolicyService { } if (oldAgentPolicy.is_managed && !options?.force) { - throw new IngestManagerError(`Cannot remove integrations of hosted agent policy ${id}`); + throw new HostedAgentPolicyRestrictionRelatedError( + `Cannot remove integrations of hosted agent policy ${id}` + ); } return await this._update( @@ -550,7 +551,7 @@ class AgentPolicyService { } if (agentPolicy.is_managed) { - throw new AgentPolicyDeletionError(`Cannot delete hosted agent policy ${id}`); + throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`); } const { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 4dfc29df8c39..63085b7729c4 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -9,7 +9,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import type { SavedObject } from 'kibana/server'; import type { AgentPolicy } from '../../types'; -import { AgentReassignmentError } from '../../errors'; +import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { reassignAgent, reassignAgents } from './reassign'; @@ -54,7 +54,7 @@ describe('reassignAgent (singular)', () => { const { soClient, esClient } = createClientsMock(); await expect( reassignAgent(soClient, esClient, agentInRegularDoc._id, hostedAgentPolicySO.id) - ).rejects.toThrowError(AgentReassignmentError); + ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); // does not call ES update expect(esClient.update).toBeCalledTimes(0); @@ -64,13 +64,13 @@ describe('reassignAgent (singular)', () => { const { soClient, esClient } = createClientsMock(); await expect( reassignAgent(soClient, esClient, agentInHostedDoc._id, regularAgentPolicySO.id) - ).rejects.toThrowError(AgentReassignmentError); + ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); // does not call ES update expect(esClient.update).toBeCalledTimes(0); await expect( reassignAgent(soClient, esClient, agentInHostedDoc._id, hostedAgentPolicySO.id) - ).rejects.toThrowError(AgentReassignmentError); + ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); // does not call ES update expect(esClient.update).toBeCalledTimes(0); }); diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 4c95d19e2f13..e72f441afd03 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '../agent_policy'; -import { AgentReassignmentError } from '../../errors'; +import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { getAgentDocuments, @@ -56,14 +56,14 @@ export async function reassignAgentIsAllowed( ) { const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); if (agentPolicy?.is_managed) { - throw new AgentReassignmentError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot reassign an agent from hosted agent policy ${agentPolicy.id}` ); } const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (newAgentPolicy?.is_managed) { - throw new AgentReassignmentError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot reassign an agent to hosted agent policy ${newAgentPolicy.id}` ); } diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index 24a3dea3bcb9..33f12dc52dc0 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -9,7 +9,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import type { SavedObject } from 'kibana/server'; import type { AgentPolicy } from '../../types'; -import { AgentUnenrollmentError } from '../../errors'; +import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { unenrollAgent, unenrollAgents } from './unenroll'; @@ -49,7 +49,7 @@ describe('unenrollAgent (singular)', () => { it('cannot unenroll from hosted agent policy by default', async () => { const { soClient, esClient } = createClientMock(); await expect(unenrollAgent(soClient, esClient, agentInHostedDoc._id)).rejects.toThrowError( - AgentUnenrollmentError + HostedAgentPolicyRestrictionRelatedError ); // does not call ES update expect(esClient.update).toBeCalledTimes(0); @@ -59,7 +59,7 @@ describe('unenrollAgent (singular)', () => { const { soClient, esClient } = createClientMock(); await expect( unenrollAgent(soClient, esClient, agentInHostedDoc._id, { revoke: true }) - ).rejects.toThrowError(AgentUnenrollmentError); + ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); // does not call ES update expect(esClient.update).toBeCalledTimes(0); }); diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index fc1f80fe7521..4d062e8bd536 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import type { Agent, BulkActionResult } from '../../types'; import * as APIKeyService from '../api_keys'; -import { AgentUnenrollmentError } from '../../errors'; +import { HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { createAgentAction, bulkCreateAgentActions } from './actions'; import type { GetAgentsOptions } from './crud'; @@ -28,7 +28,7 @@ async function unenrollAgentIsAllowed( ) { const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); if (agentPolicy?.is_managed) { - throw new AgentUnenrollmentError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot unenroll ${agentId} from a hosted agent policy ${agentPolicy.id}` ); } diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 61e785828bf2..988d3c63223f 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -10,7 +10,11 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import type { Agent, AgentAction, AgentActionSOAttributes, BulkActionResult } from '../../types'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { agentPolicyService } from '../../services'; -import { AgentReassignmentError, IngestManagerError } from '../../errors'; +import { + AgentReassignmentError, + HostedAgentPolicyRestrictionRelatedError, + IngestManagerError, +} from '../../errors'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; @@ -46,7 +50,7 @@ export async function sendUpgradeAgentAction({ const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); if (agentPolicy?.is_managed) { - throw new IngestManagerError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot upgrade agent ${agentId} in hosted agent policy ${agentPolicy.id}` ); } @@ -142,7 +146,7 @@ export async function sendUpgradeAgentsActions( } if (!options.force && isHostedAgent(agent)) { - throw new IngestManagerError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot upgrade agent in hosted agent policy ${agent.policy_id}` ); } diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 7c009299a3de..234fa4df5168 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -30,7 +30,11 @@ import type { ListResult, } from '../../common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; -import { IngestManagerError, ingestErrorToResponseOptions } from '../errors'; +import { + HostedAgentPolicyRestrictionRelatedError, + IngestManagerError, + ingestErrorToResponseOptions, +} from '../errors'; import { NewPackagePolicySchema, UpdatePackagePolicySchema } from '../types'; import type { NewPackagePolicy, @@ -75,7 +79,7 @@ class PackagePolicyService { throw new Error('Agent policy not found'); } if (parentAgentPolicy.is_managed && !options?.force) { - throw new IngestManagerError( + throw new HostedAgentPolicyRestrictionRelatedError( `Cannot add integrations to hosted agent policy ${parentAgentPolicy.id}` ); } diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 0016e4716725..24b204665c01 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -160,12 +160,15 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await savedObjectsClient.update( + // update() will perform a deep merge. We use create() with overwrite: true instead. mergeSourceConfiguration() + // ensures the correct and intended merging of properties. + await savedObjectsClient.create( infraSourceConfigurationSavedObjectName, - sourceId, pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, { + id: sourceId, version, + overwrite: true, } ) ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx index fabb6a46c494..4677ea4b747b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -92,9 +92,11 @@ const createActions = (testBed: TestBed) => { jsonContent: JSON.stringify(options), }); }); + component.update(); await act(async () => { find('addProcessorForm.submitButton').simulate('click'); }); + component.update(); }, removeProcessor(processorSelector: string) { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx index e89e91c1cbaa..fbc46159c4e1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx @@ -13,6 +13,7 @@ const testProcessors: Pick = { { script: { source: 'ctx._type = null', + description: 'my script', }, }, { @@ -252,5 +253,66 @@ describe('Pipeline Editor', () => { expect(data.processors).toEqual([testProcessors.processors[1], testProcessors.processors[2]]); expect(data.on_failure).toEqual([testProcessors.processors[0]]); }); + + it('shows user provided descriptions rather than default descriptions and default descriptions rather than no description', async () => { + const { actions, find } = testBed; + + await actions.addProcessor('processors', 'test', { if: '1 == 1' }); + + const processorDescriptions = { + userProvided: 'my script', + default: 'Sets value of "test" to "test"', + none: 'No description', + }; + + const createAssertForProcessor = (processorIndex: string) => ({ + description, + descriptionVisible, + }: { + description: string; + descriptionVisible: boolean; + }) => { + expect(find(`processors>${processorIndex}.inlineTextInputNonEditableText`).text()).toBe( + description + ); + expect( + (find(`processors>${processorIndex}.pipelineProcessorItemDescriptionContainer`).props() + .className as string).includes('--displayNone') + ).toBe(!descriptionVisible); + }; + + const assertScriptProcessor = createAssertForProcessor('0'); + const assertSetProcessor = createAssertForProcessor('2'); + const assertTestProcessor = createAssertForProcessor('3'); + + assertScriptProcessor({ + description: processorDescriptions.userProvided, + descriptionVisible: true, + }); + + assertSetProcessor({ + description: processorDescriptions.default, + descriptionVisible: true, + }); + + assertTestProcessor({ description: processorDescriptions.none, descriptionVisible: true }); + + // Enter "move" mode + find('processors>0.moveItemButton').simulate('click'); + + // We expect that descriptions remain exactly the same, but the processor with "No description" has + // its description hidden + assertScriptProcessor({ + description: processorDescriptions.userProvided, + descriptionVisible: true, + }); + + assertSetProcessor({ + description: processorDescriptions.default, + descriptionVisible: true, + }); + + assertTestProcessor({ description: processorDescriptions.none, descriptionVisible: false }); + }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx index cd7c91685467..f23442426d71 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/inline_text_input.tsx @@ -92,7 +92,10 @@ function _InlineTextInput({ > -
+
{text || {placeholder}}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 1ba883990ce1..4d3238bdb7e4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -87,12 +87,16 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( 'pipelineProcessorsEditor__item--dimmed': isDimmed, }); + const defaultDescription = processorDescriptor?.getDefaultDescription(processor.options); + + const hasNoDescription = !defaultDescription && !processor.options.description; + const inlineTextInputContainerClasses = classNames( 'pipelineProcessorsEditor__item__descriptionContainer', { // eslint-disable-next-line @typescript-eslint/naming-convention 'pipelineProcessorsEditor__item__descriptionContainer--displayNone': - isInMoveMode && !processor.options.description, + isInMoveMode && hasNoDescription, } ); @@ -208,16 +212,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx index 5dc50625546a..7950313bedfa 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item_tooltip/processor_information.tsx @@ -15,17 +15,23 @@ interface Props { } export const ProcessorInformation: FunctionComponent = memo(({ processor }) => { - const label = getProcessorDescriptor(processor.type)?.label ?? processor.type; + const processorDescriptor = getProcessorDescriptor(processor.type); + const label = processorDescriptor?.label ?? processor.type; + const description = + processor.options.description ?? processorDescriptor?.getDefaultDescription(processor.options); + return ( - {label} + + {label} + - {processor.options.description ? ( + {description ? ( - - {processor.options.description} + + {description} ) : undefined} diff --git a/x-pack/plugins/lens/jest.config.js b/x-pack/plugins/lens/jest.config.js index 9a3f12e1ead3..615e540eaedc 100644 --- a/x-pack/plugins/lens/jest.config.js +++ b/x-pack/plugins/lens/jest.config.js @@ -9,7 +9,4 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/lens'], - - // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95202 - testEnvironment: 'jest-environment-jsdom-thirteen', }; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 23da48b35a9d..286fee6de542 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -51,4 +51,12 @@ export { export { buildExceptionFilter } from './exceptions'; -export { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from './constants'; +export { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, +} from './constants'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx index 5b3e754f7e42..833034aa0a54 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -7,4 +7,4 @@ export { BuilderEntryItem } from './entry_renderer'; export { BuilderExceptionListItemComponent } from './exception_item_renderer'; -export { ExceptionBuilderComponent } from './exception_items_renderer'; +export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer'; diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 39825e5feb6b..0aecf8a8f6c8 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -39,3 +39,4 @@ export { UseExceptionListsSuccess, } from './exceptions/types'; export * as ExceptionBuilder from './exceptions/components/builder/index'; +export { transformNewItemOutput } from './exceptions/transforms'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 66c9a2462736..9ec6cbcb5d4a 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -26,7 +26,7 @@ import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { MapSettings } from '../../reducers/map'; -import { Goto } from '../../../common/descriptor_types'; +import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, KBN_TOO_MANY_FEATURES_IMAGE_ID, @@ -35,8 +35,12 @@ import { } from '../../../common/constants'; import { getGlyphUrl, isRetina } from '../../util'; import { syncLayerOrder } from './sort_layers'; -// @ts-expect-error -import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils'; +import { + addSpriteSheetToMapFromImageData, + loadSpriteSheetImageData, + removeOrphanedSourcesAndLayers, + // @ts-expect-error +} from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; @@ -172,8 +176,7 @@ export class MBMap extends Component { }; } - async _createMbMapInstance(): Promise { - const initialView = await getInitialView(this.props.goto, this.props.settings); + async _createMbMapInstance(initialView: MapCenterAndZoom | null): Promise { return new Promise((resolve) => { const mbStyle = { version: 8, @@ -237,9 +240,14 @@ export class MBMap extends Component { } async _initializeMap() { + const initialView = await getInitialView(this.props.goto, this.props.settings); + if (!this._isMounted) { + return; + } + let mbMap: MapboxMap; try { - mbMap = await this._createMbMapInstance(); + mbMap = await this._createMbMapInstance(initialView); } catch (error) { this.props.setMapInitError(error.message); return; @@ -293,10 +301,13 @@ export class MBMap extends Component { }); } - _loadMakiSprites(mbMap: MapboxMap) { - const sprites = isRetina() ? sprites2 : sprites1; + async _loadMakiSprites(mbMap: MapboxMap) { + const spritesUrl = isRetina() ? sprites2 : sprites1; const json = isRetina() ? spritesheet[2] : spritesheet[1]; - addSpritesheetToMap(json, sprites, mbMap); + const spritesData = await loadSpriteSheetImageData(spritesUrl); + if (this._isMounted) { + addSpriteSheetToMapFromImageData(json, spritesData, mbMap); + } } _syncMbMapWithMapState = () => { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js index f79f9bdffe36..5a2a98a24fca 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js @@ -51,11 +51,6 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLa mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId)); } -export async function addSpritesheetToMap(json, imgUrl, mbMap) { - const imgData = await loadSpriteSheetImageData(imgUrl); - addSpriteSheetToMapFromImageData(json, imgData, mbMap); -} - function getImageData(img) { const canvas = window.document.createElement('canvas'); const context = canvas.getContext('2d'); diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/plugins/ml/common/util/errors/process_errors.ts index 821ba670e2dd..e5c6ed38161a 100644 --- a/x-pack/plugins/ml/common/util/errors/process_errors.ts +++ b/x-pack/plugins/ml/common/util/errors/process_errors.ts @@ -14,6 +14,7 @@ import { isEsErrorBody, isMLResponseError, } from './types'; +import { isPopulatedObject } from '../object_utils'; export const extractErrorProperties = (error: ErrorType): MLErrorObject => { // extract properties of the error object from within the response error @@ -73,6 +74,14 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { error.body.attributes.body.error.caused_by?.caused_by?.reason || error.body.attributes.body.error.caused_by?.reason; } + if ( + Array.isArray(error.body.attributes.body.error.root_cause) && + typeof error.body.attributes.body.error.root_cause[0] === 'object' && + isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) + ) { + errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; + errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; + } return errObj; } else { return { diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts index 39e9ed4e2575..3110f09e441c 100644 --- a/x-pack/plugins/ml/common/util/errors/types.ts +++ b/x-pack/plugins/ml/common/util/errors/types.ts @@ -12,6 +12,7 @@ export interface EsErrorRootCause { type: string; reason: string; caused_by?: EsErrorRootCause; + script?: string; } export interface EsErrorBody { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index edbfc11343e3..3123a43594c9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -83,10 +83,12 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => } setCurrentProgress(progressStats); + // Clear if job is completed or stopped (after having started) if ( (progressStats.currentPhase === progressStats.totalPhases && progressStats.progress === 100) || - jobStats.state === DATA_FRAME_TASK_STATE.STOPPED + (jobStats.state === DATA_FRAME_TASK_STATE.STOPPED && + !(progressStats.currentPhase === 1 && progressStats.progress === 0)) ) { clearInterval(interval); // Check job has started. Jobs that fail to start will also have STOPPED state diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx index 1fcc301fbdba..588d85f24a02 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx @@ -41,6 +41,7 @@ export const DocumentCountContent: FC = ({ config, totalCount }) => { chartPoints={chartPoints} timeRangeEarliest={timeRangeEarliest} timeRangeLatest={timeRangeLatest} + interval={documentCounts.interval} /> ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx index 4d0f43237533..4c8740cc76b6 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; @@ -29,6 +29,7 @@ interface Props { chartPoints: DocumentCountChartPoint[]; timeRangeEarliest: number; timeRangeLatest: number; + interval?: number; } const SPEC_ID = 'document_count'; @@ -38,6 +39,7 @@ export const DocumentCountChart: FC = ({ chartPoints, timeRangeEarliest, timeRangeLatest, + interval, }) => { const seriesName = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.seriesLabel', { defaultMessage: 'document count', @@ -50,6 +52,21 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); + const adjustedChartPoints = useMemo(() => { + // Display empty chart when no data in range + if (chartPoints.length < 1) return [{ time: timeRangeEarliest, value: 0 }]; + + // If chart has only one bucket + // it won't show up correctly unless we add an extra data point + if (chartPoints.length === 1) { + return [ + ...chartPoints, + { time: interval ? Number(chartPoints[0].time) + interval : timeRangeEarliest, value: 0 }, + ]; + } + return chartPoints; + }, [chartPoints, timeRangeEarliest, timeRangeLatest, interval]); + return (
= ({ yScaleType={ScaleType.Linear} xAccessor="time" yAccessors={['value']} - // Display empty chart when no data in range - data={chartPoints.length > 0 ? chartPoints : [{ time: timeRangeEarliest, value: 0 }]} + data={adjustedChartPoints} />
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts index 0bf3b951f424..aa7bd2f5ecf6 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts @@ -39,6 +39,7 @@ export interface FieldVisStats { latest?: number; documentCounts?: { buckets?: DocumentCountBuckets; + interval?: number; }; avg?: number; distribution?: { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json index c4b6b18f56bc..e9cd932ce264 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json @@ -1,7 +1,7 @@ { "id": "apache_ecs", - "title": "Apache access logs", - "description": "Find unusual activity in HTTP access logs from filebeat (ECS).", + "title": "Apache access logs (Filebeat)", + "description": "Legacy jobs for finding unusual activity in HTTP access logs. The latest versions are installed with the Apache integration in Fleet.", "type": "Web Access Logs", "logoFile": "logo.json", "defaultIndexPattern": "filebeat-*", diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json index c39e4e5e4faf..e21903517894 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json @@ -1,7 +1,7 @@ { "id": "nginx_ecs", - "title": "Nginx access logs", - "description": "Find unusual activity in HTTP access logs from filebeat (ECS).", + "title": "Nginx access logs (Filebeat)", + "description": "Legacy jobs for finding unusual activity in HTTP access logs. The latest versions are installed with the Nginx integration in Fleet.", "type": "Web Access Logs", "logoFile": "logo.json", "defaultIndexPattern": "filebeat-*", diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 8fa0629d013c..71dc4919237e 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -232,6 +232,11 @@ function createHrefModule(core: CoreStart) { $attr.$set('href', core.http.basePath.prepend(url)); } }); + + _$scope.$on('$locationChangeSuccess', () => { + const url = getSafeForExternalLink($attr.href as string); + $attr.$set('href', core.http.basePath.prepend(url)); + }); }, }, }; diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index c285ff27c5a6..8ea37d04c146 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -19,7 +19,9 @@ const MOCKED_PATHS = [ beforeEach(() => { const spy = jest.spyOn(fs, 'readFileSync').mockImplementation(); - MOCKED_PATHS.forEach((file) => when(spy).calledWith(file).mockReturnValue(`contents-of-${file}`)); + MOCKED_PATHS.forEach((file) => + when(spy).calledWith(file, 'utf8').mockReturnValue(`contents-of-${file}`) + ); }); describe('config schema', () => { diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index aaae0d4dc25e..033df0df6c45 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -45,6 +45,11 @@ export { Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, osType, osTypeArray, OsTypeArray, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 8962f5e6c514..36986f5f8d35 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -29,6 +29,10 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; + +jest.mock('../../hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; jest.mock('../../../timelines/components/graph_overlay', () => ({ GraphOverlay: jest.fn(() =>
), @@ -135,6 +139,7 @@ describe('EventsViewer', () => { }); describe('event details', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); beforeEach(() => { mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_event_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_event_fields.json new file mode 100644 index 000000000000..2677006d1afa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_event_fields.json @@ -0,0 +1,332 @@ +[ + "@timestamp", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "data_stream.dataset", + "data_stream.namespace", + "data_stream.type", + "destination.address", + "destination.bytes", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.location", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.ip", + "destination.packets", + "destination.port", + "destination.registered_domain", + "destination.top_level_domain", + "dll.code_signature.exists", + "dll.code_signature.status", + "dll.code_signature.subject_name", + "dll.code_signature.trusted", + "dll.code_signature.valid", + "dll.Ext", + "dll.Ext.code_signature", + "dll.Ext.code_signature.exists", + "dll.Ext.code_signature.status", + "dll.Ext.code_signature.subject_name", + "dll.Ext.code_signature.trusted", + "dll.Ext.code_signature.valid", + "dll.Ext.load_index", + "dll.hash.md5", + "dll.hash.sha1", + "dll.hash.sha256", + "dll.hash.sha512", + "dll.name", + "dll.path", + "dll.pe.company", + "dll.pe.description", + "dll.pe.file_version", + "dll.pe.imphash", + "dll.pe.original_file_name", + "dll.pe.product", + "dns.Ext", + "dns.Ext.options", + "dns.Ext.status", + "dns.question.name", + "dns.question.registered_domain", + "dns.question.subdomain", + "dns.question.top_level_domain", + "dns.question.type", + "dns.resolved_ip", + "ecs.version", + "elastic.agent", + "elastic.agent.id", + "Endpoint.policy", + "Endpoint.policy.applied", + "Endpoint.policy.applied.id", + "Endpoint.policy.applied.name", + "Endpoint.policy.applied.status", + "Endpoint.status", + "event.action", + "event.category", + "event.code", + "event.created", + "event.dataset", + "event.Ext", + "event.Ext.correlation", + "event.Ext.correlation.id", + "event.hash", + "event.id", + "event.ingested", + "event.module", + "event.outcome", + "event.provider", + "event.sequence", + "event.severity", + "event.type", + "file.accessed", + "file.attributes", + "file.created", + "file.ctime", + "file.device", + "file.directory", + "file.drive_letter", + "file.Ext", + "file.Ext.code_signature", + "file.Ext.code_signature.exists", + "file.Ext.code_signature.status", + "file.Ext.code_signature.subject_name", + "file.Ext.code_signature.trusted", + "file.Ext.code_signature.valid", + "file.Ext.entropy", + "file.Ext.header_data", + "file.Ext.monotonic_id", + "file.Ext.original", + "file.Ext.original.gid", + "file.Ext.original.group", + "file.Ext.original.mode", + "file.Ext.original.name", + "file.Ext.original.owner", + "file.Ext.original.path", + "file.Ext.original.uid", + "file.Ext.windows", + "file.Ext.windows.zone_identifier", + "file.extension", + "file.gid", + "file.group", + "file.hash.md5", + "file.hash.sha1", + "file.hash.sha256", + "file.hash.sha512", + "file.inode", + "file.mime_type", + "file.mode", + "file.mtime", + "file.name", + "file.owner", + "file.path", + "file.path.caseless", + "file.path.text", + "file.pe.company", + "file.pe.description", + "file.pe.file_version", + "file.pe.imphash", + "file.pe.original_file_name", + "file.pe.product", + "file.size", + "file.target_path", + "file.target_path.caseless", + "file.target_path.text", + "file.type", + "file.uid", + "group.domain", + "group.Ext", + "group.Ext.real", + "group.Ext.real.id", + "group.Ext.real.name", + "group.id", + "group.name", + "host.architecture", + "host.domain", + "host.hostname", + "host.id", + "host.ip", + "host.mac", + "host.name", + "host.os.Ext", + "host.os.Ext.variant", + "host.os.family", + "host.os.full", + "host.os.full.caseless", + "host.os.full.text", + "host.os.kernel", + "host.os.name", + "host.os.name.caseless", + "host.os.name.text", + "host.os.platform", + "host.os.version", + "host.type", + "host.uptime", + "http.request.body.bytes", + "http.request.body.content", + "http.request.body.content.text", + "http.request.bytes", + "http.response.body.bytes", + "http.response.body.content", + "http.response.body.content.text", + "http.response.bytes", + "http.response.Ext", + "http.response.Ext.version", + "http.response.status_code", + "message", + "network.bytes", + "network.community_id", + "network.direction", + "network.iana_number", + "network.packets", + "network.protocol", + "network.transport", + "network.type", + "package.name", + "process.args", + "process.args_count", + "process.code_signature.exists", + "process.code_signature.status", + "process.code_signature.subject_name", + "process.code_signature.trusted", + "process.code_signature.valid", + "process.command_line", + "process.command_line.caseless", + "process.command_line.text", + "process.entity_id", + "process.executable", + "process.executable.caseless", + "process.executable.text", + "process.exit_code", + "process.Ext", + "process.Ext.ancestry", + "process.Ext.authentication_id", + "process.Ext.code_signature", + "process.Ext.code_signature.exists", + "process.Ext.code_signature.status", + "process.Ext.code_signature.subject_name", + "process.Ext.code_signature.trusted", + "process.Ext.code_signature.valid", + "process.Ext.defense_evasions", + "process.Ext.session", + "process.Ext.token.elevation", + "process.Ext.token.elevation_type", + "process.Ext.token.integrity_level_name", + "process.hash.md5", + "process.hash.sha1", + "process.hash.sha256", + "process.hash.sha512", + "process.name", + "process.name.caseless", + "process.name.text", + "process.parent.args", + "process.parent.args_count", + "process.parent.code_signature.exists", + "process.parent.code_signature.status", + "process.parent.code_signature.subject_name", + "process.parent.code_signature.trusted", + "process.parent.code_signature.valid", + "process.parent.command_line", + "process.parent.command_line.caseless", + "process.parent.command_line.text", + "process.parent.entity_id", + "process.parent.executable", + "process.parent.executable.caseless", + "process.parent.executable.text", + "process.parent.exit_code", + "process.parent.Ext", + "process.parent.Ext.code_signature", + "process.parent.Ext.code_signature.exists", + "process.parent.Ext.code_signature.status", + "process.parent.Ext.code_signature.subject_name", + "process.parent.Ext.code_signature.trusted", + "process.parent.Ext.code_signature.valid", + "process.parent.Ext.real", + "process.parent.Ext.real.pid", + "process.parent.hash.md5", + "process.parent.hash.sha1", + "process.parent.hash.sha256", + "process.parent.hash.sha512", + "process.parent.name", + "process.parent.name.caseless", + "process.parent.name.text", + "process.parent.pe.company", + "process.parent.pe.description", + "process.parent.pe.file_version", + "process.parent.pe.imphash", + "process.parent.pe.original_file_name", + "process.parent.pe.product", + "process.parent.pgid", + "process.parent.pid", + "process.parent.ppid", + "process.parent.thread.id", + "process.parent.thread.name", + "process.parent.title", + "process.parent.title.text", + "process.parent.uptime", + "process.parent.working_directory", + "process.parent.working_directory.caseless", + "process.parent.working_directory.text", + "process.pe.company", + "process.pe.description", + "process.pe.file_version", + "process.pe.imphash", + "process.pe.original_file_name", + "process.pe.product", + "process.pgid", + "process.pid", + "process.ppid", + "process.thread.id", + "process.thread.name", + "process.title", + "process.title.text", + "process.uptime", + "process.working_directory", + "process.working_directory.caseless", + "process.working_directory.text", + "registry.data.bytes", + "registry.data.strings", + "registry.hive", + "registry.key", + "registry.path", + "registry.value", + "source.address", + "source.bytes", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.location", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.ip", + "source.packets", + "source.port", + "source.registered_domain", + "source.top_level_domain", + "user.domain", + "user.email", + "user.Ext", + "user.Ext.real", + "user.Ext.real.id", + "user.Ext.real.name", + "user.full_name", + "user.full_name.text", + "user.group.domain", + "user.group.Ext", + "user.group.Ext.real", + "user.group.Ext.real.id", + "user.group.Ext.real.name", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user.name.text" +] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json rename to x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 69ec3120a064..81383bb9c0fa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -49,18 +49,29 @@ import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { addIdToItem, removeIdFromItem } from '../../../../common'; -import exceptionableFields from './exceptionable_fields.json'; +import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; +import exceptionableEndpointEventFields from './exceptionable_endpoint_event_fields.json'; export const filterIndexPatterns = ( patterns: IIndexPattern, type: ExceptionListType ): IIndexPattern => { - return type === 'endpoint' - ? { + switch (type) { + case 'endpoint': + return { ...patterns, - fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - } - : patterns; + fields: patterns.fields.filter(({ name }) => exceptionableEndpointFields.includes(name)), + }; + case 'endpoint_events': + return { + ...patterns, + fields: patterns.fields.filter(({ name }) => + exceptionableEndpointEventFields.includes(name) + ), + }; + default: + return patterns; + } }; export const addIdToEntries = (entries: EntriesArray): EntriesArray => { diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index 7e42afc44002..1987edc0e730 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -8,6 +8,7 @@ import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action'; import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action'; +import { EventFiltersPageAction } from '../../management/pages/event_filters/store/action'; export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; @@ -19,4 +20,5 @@ export type AppAction = | EndpointAction | RoutingAction | PolicyDetailsAction - | TrustedAppsPageAction; + | TrustedAppsPageAction + | EventFiltersPageAction; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 26b9662a8f19..a2f568efd3a7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { getOr } from 'lodash/fp'; +import { indexOf } from 'lodash'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; @@ -47,6 +48,7 @@ import { ExceptionListType } from '../../../../../common/shared_imports'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; +import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal'; interface AlertContextMenuProps { ariaLabel?: string; @@ -81,6 +83,8 @@ const AlertContextMenuComponent: React.FC = ({ '', [ecsRowData] ); + + const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); const ruleIndices = useMemo((): string[] => { if ( ecsRowData.signal?.rule && @@ -107,6 +111,7 @@ const AlertContextMenuComponent: React.FC = ({ setPopover(false); }, []); const [exceptionModalType, setOpenAddExceptionModal] = useState(null); + const [isAddEventExceptionModalOpen, setIsAddEventExceptionModalOpen] = useState(false); const [{ canUserCRUD, hasIndexWrite, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData(); const isEndpointAlert = useMemo((): boolean => { @@ -124,6 +129,10 @@ const AlertContextMenuComponent: React.FC = ({ setOpenAddExceptionModal(null); }, []); + const closeAddEventExceptionModal = useCallback((): void => { + setIsAddEventExceptionModalOpen(false); + }, []); + const onAddExceptionCancel = useCallback(() => { closeAddExceptionModal(); }, [closeAddExceptionModal]); @@ -355,6 +364,28 @@ const AlertContextMenuComponent: React.FC = ({ ); }, [handleAddExceptionClick, canUserCRUD, hasIndexWrite]); + const handleAddEventExceptionClick = useCallback((): void => { + closePopover(); + setIsAddEventExceptionModalOpen(true); + }, [closePopover]); + + const addEventExceptionComponent = useMemo( + () => ( + + + {i18n.ACTION_ADD_EVENT_EXCEPTION} + + + ), + [handleAddEventExceptionClick] + ); + const statusFilters = useMemo(() => { if (!alertStatus) { return []; @@ -378,8 +409,18 @@ const AlertContextMenuComponent: React.FC = ({ ]); const items = useMemo( - () => [...statusFilters, addEndpointExceptionComponent, addExceptionComponent], - [addEndpointExceptionComponent, addExceptionComponent, statusFilters] + () => + !isEvent && ruleId + ? [...statusFilters, addEndpointExceptionComponent, addExceptionComponent] + : [addEventExceptionComponent], + [ + addEndpointExceptionComponent, + addExceptionComponent, + addEventExceptionComponent, + statusFilters, + ruleId, + isEvent, + ] ); return ( @@ -412,6 +453,9 @@ const AlertContextMenuComponent: React.FC = ({ onRuleChange={onRuleChange} /> )} + {isAddEventExceptionModalOpen && ecsRowData != null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 1829b3822e6a..56f6337d5a55 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -151,6 +151,13 @@ export const ACTION_ADD_EXCEPTION = i18n.translate( } ); +export const ACTION_ADD_EVENT_EXCEPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addEventException', + { + defaultMessage: 'Add Endpoint event exception', + } +); + export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException', { diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 0441cee8d2cf..08612e0b6d00 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -33,22 +33,21 @@ describe('useUserInfo', () => { const { result, waitForNextUpdate } = renderHook(() => useUserInfo()); await waitForNextUpdate(); - expect(result).toEqual({ - current: { - canUserCRUD: null, - hasEncryptionKey: null, - hasIndexManage: null, - hasIndexMaintenance: null, - hasIndexWrite: null, - hasIndexUpdateDelete: null, - isAuthenticated: null, - isSignalIndexExists: null, - loading: true, - signalIndexName: null, - signalIndexMappingOutdated: null, - }, - error: undefined, + expect(result.all).toHaveLength(1); + expect(result.current).toEqual({ + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexMaintenance: null, + hasIndexWrite: null, + hasIndexUpdateDelete: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + signalIndexMappingOutdated: null, }); + expect(result.error).toBeUndefined(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 208fe0444870..1b5ddd28d328 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -26,6 +26,8 @@ export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints'; /** Namespace within the Management state where trusted apps page state is maintained */ export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps'; +/** Namespace within the Management state where event filters page state is maintained */ +export const MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE = 'eventFilters'; export const MANAGEMENT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50]; export const MANAGEMENT_DEFAULT_PAGE = 0; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts new file mode 100644 index 000000000000..882b964d54bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ExceptionListType, + ExceptionListTypeEnum, + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, +} from '../../../../common/shared_imports'; + +export const EVENT_FILTER_LIST_TYPE: ExceptionListType = ExceptionListTypeEnum.ENDPOINT_EVENTS; +export const EVENT_FILTER_LIST = { + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + namespace_type: 'agnostic', + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + type: EVENT_FILTER_LIST_TYPE, +}; + +export { ENDPOINT_EVENT_FILTERS_LIST_ID, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts new file mode 100644 index 000000000000..b3521d749936 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from 'kibana/public'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { EVENT_FILTER_LIST, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../constants'; + +export interface EventFiltersService { + addEventFilters( + exception: Immutable + ): Promise; +} +export class EventFiltersHttpService implements EventFiltersService { + private listHasBeenCreated: boolean; + + constructor(private http: HttpStart) { + this.listHasBeenCreated = false; + } + + private async createEndpointEventList() { + try { + await this.http.post(EXCEPTION_LIST_URL, { + body: JSON.stringify(EVENT_FILTER_LIST), + }); + } catch (err) { + // Ignore 409 errors. List already created + if (err.response.status === 409) this.listHasBeenCreated = true; + else throw err; + } + } + + private async httpWrapper() { + if (!this.listHasBeenCreated) await this.createEndpointEventList(); + return this.http; + } + + async addEventFilters(exception: ExceptionListItemSchema | CreateExceptionListItemSchema) { + return (await this.httpWrapper()).post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts new file mode 100644 index 000000000000..b5e867e64888 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { AsyncResourceState } from '../../../state/async_resource_state'; +export interface EventFiltersListPageState { + entries: ExceptionListItemSchema[]; + form: { + entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined; + hasNameError: boolean; + hasItemsError: boolean; + submissionResourceState: AsyncResourceState; + }; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts new file mode 100644 index 000000000000..de2a74ed6f72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Action } from 'redux'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { AsyncResourceState } from '../../../state/async_resource_state'; + +export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & { + payload: { + entry: ExceptionListItemSchema | CreateExceptionListItemSchema; + }; +}; + +export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & { + payload: { + entry: ExceptionListItemSchema | CreateExceptionListItemSchema; + hasNameError?: boolean; + hasItemsError?: boolean; + }; +}; + +export type EventFiltersCreateStart = Action<'eventFiltersCreateStart'>; +export type EventFiltersCreateSuccess = Action<'eventFiltersCreateSuccess'> & { + payload: { + exception: ExceptionListItemSchema; + }; +}; +export type EventFiltersCreateError = Action<'eventFiltersCreateError'>; + +export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged'> & { + payload: AsyncResourceState; +}; + +export type EventFiltersPageAction = + | EventFiltersCreateStart + | EventFiltersInitForm + | EventFiltersChangeForm + | EventFiltersCreateStart + | EventFiltersCreateSuccess + | EventFiltersCreateError + | EventFiltersFormStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts new file mode 100644 index 000000000000..86ba9b1e49c3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EventFiltersListPageState } from '../state'; + +export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ + entries: [], + form: { + entry: undefined, + hasNameError: false, + hasItemsError: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, + }, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts new file mode 100644 index 000000000000..afb82ebe011f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { applyMiddleware, createStore, Store } from 'redux'; + +import { + createSpyMiddleware, + MiddlewareActionSpyHelper, +} from '../../../../common/store/test_utils'; +import { AppAction } from '../../../../common/store/actions'; +import { createEventFiltersPageMiddleware } from './middleware'; +import { eventFiltersPageReducer } from './reducer'; +import { EventFiltersService } from '../service'; +import { EventFiltersListPageState } from '../state'; +import { initialEventFiltersPageState } from './builders'; +import { getInitialExceptionFromEvent } from './utils'; +import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils'; + +const initialState: EventFiltersListPageState = initialEventFiltersPageState(); + +const createEventFiltersServiceMock = (): jest.Mocked => ({ + addEventFilters: jest.fn(), +}); + +const createStoreSetup = (eventFiltersService: EventFiltersService) => { + const spyMiddleware = createSpyMiddleware(); + + return { + spyMiddleware, + store: createStore( + eventFiltersPageReducer, + applyMiddleware( + createEventFiltersPageMiddleware(eventFiltersService), + spyMiddleware.actionSpyMiddleware + ) + ), + }; +}; + +describe('middleware', () => { + describe('initial state', () => { + it('sets initial state properly', async () => { + expect(createStoreSetup(createEventFiltersServiceMock()).store.getState()).toStrictEqual( + initialState + ); + }); + }); + + describe('submit creation event filter', () => { + let service: jest.Mocked; + let store: Store; + let spyMiddleware: MiddlewareActionSpyHelper; + + beforeEach(() => { + service = createEventFiltersServiceMock(); + const storeSetup = createStoreSetup(service); + store = storeSetup.store as Store; + spyMiddleware = storeSetup.spyMiddleware; + }); + + it('does not submit when entry is undefined', async () => { + store.dispatch({ type: 'eventFiltersCreateStart' }); + expect(store.getState()).toStrictEqual({ + ...initialState, + form: { + ...store.getState().form, + submissionResourceState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + + it('does submit when entry is not undefined', async () => { + service.addEventFilters.mockResolvedValue(createdEventFilterEntryMock()); + const entry = getInitialExceptionFromEvent(ecsEventMock()); + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + + store.dispatch({ type: 'eventFiltersCreateStart' }); + + await spyMiddleware.waitForAction('eventFiltersFormStateChanged'); + expect(store.getState()).toStrictEqual({ + ...initialState, + form: { + ...store.getState().form, + submissionResourceState: { + type: 'LoadedResourceState', + data: createdEventFilterEntryMock(), + }, + }, + }); + }); + + it('does throw error when creating', async () => { + service.addEventFilters.mockRejectedValue({ + body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' }, + }); + const entry = getInitialExceptionFromEvent(ecsEventMock()); + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + + store.dispatch({ type: 'eventFiltersCreateStart' }); + + await spyMiddleware.waitForAction('eventFiltersFormStateChanged'); + expect(store.getState()).toStrictEqual({ + ...initialState, + form: { + ...store.getState().form, + submissionResourceState: { + type: 'FailedResourceState', + lastLoadedState: undefined, + error: { + error: 'Internal Server Error', + message: 'error message', + statusCode: 500, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts new file mode 100644 index 000000000000..2f2c7a9e2266 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppAction } from '../../../../common/store/actions'; +import { + ImmutableMiddleware, + ImmutableMiddlewareAPI, + ImmutableMiddlewareFactory, +} from '../../../../common/store'; + +import { EventFiltersHttpService, EventFiltersService } from '../service'; + +import { EventFiltersListPageState } from '../state'; +import { getLastLoadedResourceState } from '../../../state/async_resource_state'; +import { CreateExceptionListItemSchema, transformNewItemOutput } from '../../../../shared_imports'; + +const eventFiltersCreate = async ( + store: ImmutableMiddlewareAPI, + eventFiltersService: EventFiltersService +) => { + const submissionResourceState = store.getState().form.submissionResourceState; + try { + const formEntry = store.getState().form.entry; + if (!formEntry) return; + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + + const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema); + + const exception = await eventFiltersService.addEventFilters(sanitizedEntry); + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadedResourceState', + data: exception, + }, + }); + } catch (error) { + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'FailedResourceState', + error: error.body || error, + lastLoadedState: getLastLoadedResourceState(submissionResourceState), + }, + }); + } +}; + +export const createEventFiltersPageMiddleware = ( + eventFiltersService: EventFiltersService +): ImmutableMiddleware => { + return (store) => (next) => async (action) => { + next(action); + + if (action.type === 'eventFiltersCreateStart') { + await eventFiltersCreate(store, eventFiltersService); + } + }; +}; + +export const eventFiltersPageMiddlewareFactory: ImmutableMiddlewareFactory = ( + coreStart +) => createEventFiltersPageMiddleware(new EventFiltersHttpService(coreStart.http)); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts new file mode 100644 index 000000000000..5ee956ddac3e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { initialEventFiltersPageState } from './builders'; +import { eventFiltersPageReducer } from './reducer'; +import { getInitialExceptionFromEvent } from './utils'; +import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils'; + +const initialState = initialEventFiltersPageState(); + +describe('reducer', () => { + describe('EventFiltersForm', () => { + it('sets the initial form values', () => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + const result = eventFiltersPageReducer(initialState, { + type: 'eventFiltersInitForm', + payload: { entry }, + }); + + expect(result).toStrictEqual({ + ...initialState, + form: { + ...initialState.form, + entry, + hasNameError: !entry.name, + submissionResourceState: { + type: 'UninitialisedResourceState', + }, + }, + }); + }); + + it('change form values', () => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + const nameChanged = 'name changed'; + const result = eventFiltersPageReducer(initialState, { + type: 'eventFiltersChangeForm', + payload: { entry: { ...entry, name: nameChanged } }, + }); + + expect(result).toStrictEqual({ + ...initialState, + form: { + ...initialState.form, + entry: { + ...entry, + name: nameChanged, + }, + hasNameError: false, + submissionResourceState: { + type: 'UninitialisedResourceState', + }, + }, + }); + }); + + it('change form status', () => { + const result = eventFiltersPageReducer(initialState, { + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadedResourceState', + data: createdEventFilterEntryMock(), + }, + }); + + expect(result).toStrictEqual({ + ...initialState, + form: { + ...initialState.form, + submissionResourceState: { + type: 'LoadedResourceState', + data: createdEventFilterEntryMock(), + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts new file mode 100644 index 000000000000..c8f80eaee185 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ImmutableReducer } from '../../../../common/store'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; + +import { + EventFiltersInitForm, + EventFiltersChangeForm, + EventFiltersFormStateChanged, +} from './action'; + +import { EventFiltersListPageState } from '../state'; +import { initialEventFiltersPageState } from './builders'; + +type StateReducer = ImmutableReducer; +type CaseReducer = ( + state: Immutable, + action: Immutable +) => Immutable; + +const eventFiltersInitForm: CaseReducer = (state, action) => { + return { + ...state, + form: { + ...state.form, + entry: action.payload.entry, + hasNameError: !action.payload.entry.name, + submissionResourceState: { + type: 'UninitialisedResourceState', + }, + }, + }; +}; + +const eventFiltersChangeForm: CaseReducer = (state, action) => { + return { + ...state, + form: { + ...state.form, + entry: action.payload.entry, + hasItemsError: + action.payload.hasItemsError !== undefined + ? action.payload.hasItemsError + : state.form.hasItemsError, + hasNameError: + action.payload.hasNameError !== undefined + ? action.payload.hasNameError + : state.form.hasNameError, + }, + }; +}; + +const eventFiltersFormStateChanged: CaseReducer = (state, action) => { + return { + ...state, + form: { + ...state.form, + submissionResourceState: action.payload, + }, + }; +}; + +export const eventFiltersPageReducer: StateReducer = ( + state = initialEventFiltersPageState(), + action +) => { + switch (action.type) { + case 'eventFiltersInitForm': + return eventFiltersInitForm(state, action); + case 'eventFiltersChangeForm': + return eventFiltersChangeForm(state, action); + case 'eventFiltersFormStateChanged': + return eventFiltersFormStateChanged(state, action); + } + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts new file mode 100644 index 000000000000..ece754f71b31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EventFiltersListPageState } from '../state'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { ServerApiError } from '../../../../common/types'; +import { + isLoadingResourceState, + isLoadedResourceState, + isFailedResourceState, +} from '../../../state/async_resource_state'; + +export const getFormEntry = ( + state: EventFiltersListPageState +): CreateExceptionListItemSchema | ExceptionListItemSchema | undefined => { + return state.form.entry; +}; + +export const getFormHasError = (state: EventFiltersListPageState): boolean => { + return state.form.hasItemsError || state.form.hasNameError; +}; + +export const isCreationInProgress = (state: EventFiltersListPageState): boolean => { + return isLoadingResourceState(state.form.submissionResourceState); +}; + +export const isCreationSuccessful = (state: EventFiltersListPageState): boolean => { + return isLoadedResourceState(state.form.submissionResourceState); +}; + +export const getCreationError = (state: EventFiltersListPageState): ServerApiError | undefined => { + const submissionResourceState = state.form.submissionResourceState; + + return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts new file mode 100644 index 000000000000..94f15907fb58 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { initialEventFiltersPageState } from './builders'; +import { getFormEntry, getFormHasError } from './selector'; +import { ecsEventMock } from '../test_utils'; +import { getInitialExceptionFromEvent } from './utils'; + +const initialState = initialEventFiltersPageState(); + +describe('selectors', () => { + describe('getFormEntry()', () => { + it('returns undefined when there is no entry', () => { + expect(getFormEntry(initialState)).toBe(undefined); + }); + it('returns entry when there is an entry on form', () => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + const state = { + ...initialState, + form: { + ...initialState.form, + entry, + }, + }; + expect(getFormEntry(state)).toBe(entry); + }); + }); + describe('getFormHasError()', () => { + it('returns false when there is no entry', () => { + expect(getFormHasError(initialState)).toBeFalsy(); + }); + it('returns true when entry with name error', () => { + const state = { + ...initialState, + form: { + ...initialState.form, + hasNameError: true, + }, + }; + expect(getFormHasError(state)).toBeTruthy(); + }); + it('returns true when entry with item error', () => { + const state = { + ...initialState, + form: { + ...initialState.form, + hasItemsError: true, + }, + }; + expect(getFormHasError(state)).toBeTruthy(); + }); + it('returns true when entry with item error and name error', () => { + const state = { + ...initialState, + form: { + ...initialState.form, + hasItemsError: true, + hasNameError: true, + }, + }; + expect(getFormHasError(state)).toBeTruthy(); + }); + + it('returns false when entry without errors', () => { + const state = { + ...initialState, + form: { + ...initialState.form, + hasItemsError: false, + hasNameError: false, + }, + }; + expect(getFormHasError(state)).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts new file mode 100644 index 000000000000..251aaef0897e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { Ecs } from '../../../../../common/ecs'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../constants'; + +export const getInitialExceptionFromEvent = (data: Ecs): CreateExceptionListItemSchema => ({ + comments: [], + description: '', + entries: + data.event && data.process + ? [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: (data.event.category ?? [])[0], + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: (data.process.executable ?? [])[0], + }, + ] + : [], + item_id: undefined, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + meta: { + temporaryUuid: uuid.v4(), + }, + name: '', + namespace_type: 'agnostic', + tags: [], + type: 'simple', + // TODO: Try to fix this type casting + os_types: [(data.host ? data.host.os?.family ?? [] : [])[0] as 'windows' | 'linux' | 'macos'], +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts new file mode 100644 index 000000000000..c38de842521f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { combineReducers, createStore } from 'redux'; +import { Ecs } from '../../../../../common/ecs'; + +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, +} from '../../../common/constants'; +import { ExceptionListItemSchema } from '../../../../shared_imports'; + +import { eventFiltersPageReducer } from '../store/reducer'; + +export const createGlobalNoMiddlewareStore = () => { + return createStore( + combineReducers({ + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ + [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer, + }), + }) + ); +}; + +export const ecsEventMock = (): Ecs => ({ + _id: 'unLfz3gB2mJZsMY3ytx3', + timestamp: '2021-04-14T15:34:15.330Z', + _index: '.ds-logs-endpoint.events.process-default-2021.04.12-000001', + event: { + category: ['network'], + id: ['2c4f51be-7736-4ab8-a255-54e7023c4653'], + kind: ['event'], + type: ['start'], + }, + host: { + name: ['Host-tvs68wo3qc'], + os: { + family: ['windows'], + }, + id: ['a563b365-2bee-40df-adcd-ae84d889f523'], + ip: ['10.242.233.187'], + }, + user: { + name: ['uegem17ws4'], + domain: ['hr8jofpkxp'], + }, + agent: { + type: ['endpoint'], + }, + process: { + hash: { + md5: ['c4653870-99b8-4f36-abde-24812d08a289'], + }, + parent: { + pid: [4852], + }, + pid: [3652], + name: ['lsass.exe'], + args: ['"C:\\lsass.exe" \\6z9'], + entity_id: ['9qotd1i8rf'], + executable: ['C:\\lsass.exe'], + }, +}); + +export const createdEventFilterEntryMock = (): ExceptionListItemSchema => ({ + _version: 'WzM4MDgsMV0=', + meta: undefined, + comments: [], + created_at: '2021-04-19T10:30:36.425Z', + created_by: 'elastic', + description: '', + entries: [ + { field: 'event.category', operator: 'included', type: 'match', value: 'process' }, + { field: 'process.executable', operator: 'included', type: 'match', value: 'C:\\iexlorer.exe' }, + ], + id: '47598790-a0fa-11eb-8458-69ac85f1fa18', + item_id: '93f65a04-6f5c-4f9e-9be5-e674b3c2392f', + list_id: '.endpointEventFilterList', + name: 'Test', + namespace_type: 'agnostic', + os_types: ['windows'], + tags: [], + tie_breaker_id: 'c42f3dbd-292f-49e8-83ab-158d024a4d8b', + type: 'simple', + updated_at: '2021-04-19T10:30:36.428Z', + updated_by: 'elastic', +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx new file mode 100644 index 000000000000..d0d8ea12cf16 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EventFiltersForm } from '.'; +import { RenderResult, act, render } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { stubIndexPatternWithFields } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; +import { getInitialExceptionFromEvent } from '../../../store/utils'; +import { Provider } from 'react-redux'; +import { useFetchIndex } from '../../../../../../common/containers/source'; +import { ThemeProvider } from 'styled-components'; +import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils'; +import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; +import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; + +jest.mock('../../../../../../common/lib/kibana'); +jest.mock('../../../../../../common/containers/source'); + +const mockTheme = getMockTheme({ + eui: { + paddingSizes: { m: '2' }, + }, +}); + +describe('Event filter form', () => { + let component: RenderResult; + let store: ReturnType; + + const renderForm = () => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { wrapper: Wrapper }); + }; + + const renderComponentWithdata = () => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + act(() => { + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + }); + return renderForm(); + }; + + beforeEach(() => { + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, + { + indexPatterns: stubIndexPatternWithFields, + }, + ]); + (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + data: {}, + notifications: {}, + }, + }); + store = createGlobalNoMiddlewareStore(); + }); + it('should renders correctly without data', () => { + component = renderForm(); + expect(component.getByTestId('loading-spinner')).not.toBeNull(); + }); + + it('should renders correctly with data', () => { + component = renderComponentWithdata(); + + expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull(); + expect(component.getByText(NAME_ERROR)).not.toBeNull(); + }); + + it('should change name', async () => { + component = renderComponentWithdata(); + + const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: 'Exception name', + }, + }); + }); + + expect(store.getState()!.management!.eventFilters!.form!.entry!.name).toBe('Exception name'); + expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeFalsy(); + }); + + it('should change comments', async () => { + component = renderComponentWithdata(); + + const commentInput = component.getByPlaceholderText('Add a new comment...'); + + act(() => { + fireEvent.change(commentInput, { + target: { + value: 'Exception comment', + }, + }); + }); + + expect(store.getState()!.management!.eventFilters!.form!.entry!.comments![0].comment).toBe( + 'Exception comment' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx new file mode 100644 index 000000000000..2aeb53ed5bcb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { + EuiFieldText, + EuiSpacer, + EuiForm, + EuiFormRow, + EuiSuperSelect, + EuiSuperSelectOption, + EuiText, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { AddExceptionComments } from '../../../../../../common/components/exceptions/add_exception_comments'; +import { filterIndexPatterns } from '../../../../../../common/components/exceptions/helpers'; +import { Loader } from '../../../../../../common/components/loader'; +import { useKibana } from '../../../../../../common/lib/kibana'; +import { useFetchIndex } from '../../../../../../common/containers/source'; +import { AppAction } from '../../../../../../common/store/actions'; +import { ExceptionListItemSchema, ExceptionBuilder } from '../../../../../../shared_imports'; + +import { useEventFiltersSelector } from '../../hooks'; +import { getFormEntry } from '../../../store/selector'; +import { + FORM_DESCRIPTION, + NAME_LABEL, + NAME_ERROR, + NAME_PLACEHOLDER, + OS_LABEL, + RULE_NAME, +} from './translations'; +import { OS_TITLES } from '../../../../../common/translations'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../../constants'; + +const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ + OperatingSystem.MAC, + OperatingSystem.WINDOWS, + OperatingSystem.LINUX, +]; + +interface EventFiltersFormProps { + allowSelectOs?: boolean; +} +export const EventFiltersForm: React.FC = memo( + ({ allowSelectOs = false }) => { + const { http, data } = useKibana().services; + const dispatch = useDispatch>(); + const exception = useEventFiltersSelector(getFormEntry); + + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(['logs-endpoint.events.*']); + + const osOptions: Array> = useMemo( + () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), + [] + ); + + const [hasNameError, setHasNameError] = useState(!exception || !exception.name); + const [comment, setComment] = useState(''); + + const handleOnBuilderChange = useCallback( + (arg: ExceptionBuilder.OnChangeProps) => { + if (isEmpty(arg.exceptionItems)) return; + dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { + ...arg.exceptionItems[0], + name: exception?.name ?? '', + comments: exception?.comments ?? [], + }, + hasItemsError: arg.errorExists, + }, + }); + }, + [dispatch, exception?.name, exception?.comments] + ); + + const handleOnChangeName = useCallback( + (e: React.ChangeEvent) => { + if (!exception) return; + setHasNameError(!e.target.value); + dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { ...exception, name: e.target.value.toString() }, + hasNameError: !e.target.value, + }, + }); + }, + [dispatch, exception] + ); + + const handleOnChangeComment = useCallback( + (value: string) => { + setComment(value); + if (!exception) return; + dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { ...exception, comments: [{ comment: value }] }, + }, + }); + }, + [dispatch, exception, setComment] + ); + + const exceptionBuilderComponentMemo = useMemo( + () => ( + + ), + [data, handleOnBuilderChange, http, indexPatterns, exception] + ); + + const nameInputMemo = useMemo( + () => ( + + + + ), + [hasNameError, exception?.name, handleOnChangeName] + ); + + const osInputMemo = useMemo( + () => ( + + + + ), + [exception?.os_types, osOptions] + ); + + const commentsInputMemo = useMemo( + () => ( + + ), + [comment, handleOnChangeComment] + ); + + return !isIndexPatternLoading && exception ? ( + + {FORM_DESCRIPTION} + + {nameInputMemo} + + {allowSelectOs ? ( + <> + {osInputMemo} + + + ) : null} + {exceptionBuilderComponentMemo} + + {commentsInputMemo} + + ) : ( + + ); + } +); + +EventFiltersForm.displayName = 'EventFiltersForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts new file mode 100644 index 000000000000..79cf296928e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FORM_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.eventFilter.modal.description', + { + defaultMessage: "Events are filtered when the rule's conditions are met:", + } +); + +export const NAME_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.eventFilter.form.name.placeholder', + { + defaultMessage: 'Event exception name', + } +); + +export const NAME_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.name.label', { + defaultMessage: 'Name your event exception', +}); + +export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.form.name.error', { + defaultMessage: "The name can't be empty", +}); + +export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', { + defaultMessage: 'Seelct OS', +}); + +export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', { + defaultMessage: 'Endpoint Event Filtering', +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx new file mode 100644 index 000000000000..0c976b357151 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EventFiltersModal } from '.'; +import { RenderResult, act, render } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { Provider } from 'react-redux'; +import { ThemeProvider } from 'styled-components'; +import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils'; +import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations'; +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, +} from '../../../../../../shared_imports'; + +jest.mock('../form'); +jest.mock('../../hooks', () => { + const originalModule = jest.requireActual('../../hooks'); + const useEventFiltersNotification = jest.fn().mockImplementation(() => {}); + + return { + ...originalModule, + useEventFiltersNotification, + }; +}); + +const mockTheme = getMockTheme({ + eui: { + paddingSizes: { m: '2' }, + euiBreakpoints: { l: '2' }, + }, +}); + +describe('Event filter modal', () => { + let component: RenderResult; + let store: ReturnType; + let onCancelMock: jest.Mock; + + const renderForm = () => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { + wrapper: Wrapper, + }); + }; + + beforeEach(() => { + store = createGlobalNoMiddlewareStore(); + onCancelMock = jest.fn(); + }); + + it('should renders correctly', () => { + component = renderForm(); + expect(component.getAllByText(MODAL_TITLE)).not.toBeNull(); + expect(component.getByText(MODAL_SUBTITLE)).not.toBeNull(); + expect(component.getAllByText(ACTIONS_CONFIRM)).not.toBeNull(); + expect(component.getByText(ACTIONS_CANCEL)).not.toBeNull(); + }); + + it('should dispatch action to init form store on mount', () => { + component = renderForm(); + expect(store.getState()!.management!.eventFilters!.form!.entry).not.toBeNull(); + }); + + it('should confirm form when button is disabled', () => { + component = renderForm(); + const confirmButton = component.getByTestId('add-exception-confirm-button'); + act(() => { + fireEvent.click(confirmButton); + }); + expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( + 'UninitialisedResourceState' + ); + }); + + it('should confirm form when button is enabled', () => { + component = renderForm(); + store.dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { + ...(store.getState()!.management!.eventFilters!.form! + .entry as CreateExceptionListItemSchema), + name: 'test', + }, + hasNameError: false, + }, + }); + const confirmButton = component.getByTestId('add-exception-confirm-button'); + act(() => { + fireEvent.click(confirmButton); + }); + expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( + 'UninitialisedResourceState' + ); + expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); + }); + + it('should close when exception has been submitted correctly', () => { + component = renderForm(); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + act(() => { + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadedResourceState', + data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema, + }, + }); + }); + + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + + it('should close when click on cancel button', () => { + component = renderForm(); + const cancelButton = component.getByText(ACTIONS_CANCEL); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + act(() => { + fireEvent.click(cancelButton); + }); + + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + + it('should close when close modal', () => { + component = renderForm(); + const modalCloseButton = component.getByLabelText('Closes this modal window'); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + act(() => { + fireEvent.click(modalCloseButton); + }); + + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + + it('should prevent close when is loading action', () => { + component = renderForm(); + act(() => { + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + + const cancelButton = component.getByText(ACTIONS_CANCEL); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + act(() => { + fireEvent.click(cancelButton); + }); + + expect(onCancelMock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx new file mode 100644 index 000000000000..50102d09248b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useEffect, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import styled, { css } from 'styled-components'; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { AppAction } from '../../../../../../common/store/actions'; +import { Ecs } from '../../../../../../../common/ecs'; +import { EventFiltersForm } from '../form'; +import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks'; +import { + getFormHasError, + isCreationInProgress, + isCreationSuccessful, +} from '../../../store/selector'; +import { getInitialExceptionFromEvent } from '../../../store/utils'; +import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations'; + +export interface EventFiltersModalProps { + data: Ecs; + onCancel(): void; +} + +const Modal = styled(EuiModal)` + ${({ theme }) => css` + width: ${theme.eui.euiBreakpoints.l}; + max-width: ${theme.eui.euiBreakpoints.l}; + `} +`; + +const ModalHeader = styled(EuiModalHeader)` + flex-direction: column; + align-items: flex-start; +`; + +const ModalHeaderSubtitle = styled.div` + ${({ theme }) => css` + color: ${theme.eui.euiColorMediumShade}; + `} +`; + +const ModalBodySection = styled.section` + ${({ theme }) => css` + padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; + overflow-y: scroll; + `} +`; + +export const EventFiltersModal: React.FC = memo(({ data, onCancel }) => { + useEventFiltersNotification(); + const dispatch = useDispatch>(); + const formHasError = useEventFiltersSelector(getFormHasError); + const creationInProgress = useEventFiltersSelector(isCreationInProgress); + const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); + + useEffect(() => { + if (creationSuccessful) { + onCancel(); + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + } + }, [creationSuccessful, onCancel, dispatch]); + + // Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount + useEffect(() => { + dispatch({ + type: 'eventFiltersInitForm', + payload: { entry: getInitialExceptionFromEvent(data) }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleOnCancel = useCallback(() => { + if (creationInProgress) return; + onCancel(); + }, [creationInProgress, onCancel]); + + const confirmButtonMemo = useMemo( + () => ( + { + dispatch({ type: 'eventFiltersCreateStart' }); + }} + isLoading={creationInProgress} + > + {ACTIONS_CONFIRM} + + ), + [dispatch, formHasError, creationInProgress] + ); + + return ( + + + {MODAL_TITLE} + {MODAL_SUBTITLE} + + + + + + + + + {ACTIONS_CANCEL} + + {confirmButtonMemo} + + + ); +}); + +EventFiltersModal.displayName = 'EventFiltersModal'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/translations.ts new file mode 100644 index 000000000000..982d9b3bb12b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/translations.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const MODAL_TITLE = i18n.translate('xpack.securitySolution.eventFilter.modal.title', { + defaultMessage: 'Add Endpoint Event Filter', +}); + +export const MODAL_SUBTITLE = i18n.translate('xpack.securitySolution.eventFilter.modal.subtitle', { + defaultMessage: 'Endpoint Security', +}); + +export const ACTIONS_CONFIRM = i18n.translate( + 'xpack.securitySolution.eventFilter.modal.actions.confirm', + { + defaultMessage: 'Add Endpoint Event Filter', + } +); + +export const ACTIONS_CANCEL = i18n.translate( + 'xpack.securitySolution.eventFilter.modal.actions.cancel', + { + defaultMessage: 'cancel', + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts new file mode 100644 index 000000000000..407dee896f5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { isCreationSuccessful, getFormEntry, getCreationError } from '../store/selector'; + +import { useToasts } from '../../../../common/lib/kibana'; +import { getCreationSuccessMessage, getCreationErrorMessage } from './translations'; + +import { State } from '../../../../common/store'; +import { EventFiltersListPageState } from '../state'; + +import { + MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE as EVENT_FILTER_NS, + MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS, +} from '../../../common/constants'; + +export function useEventFiltersSelector(selector: (state: EventFiltersListPageState) => R): R { + return useSelector((state: State) => + selector(state[GLOBAL_NS][EVENT_FILTER_NS] as EventFiltersListPageState) + ); +} + +export const useEventFiltersNotification = () => { + const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); + const creationError = useEventFiltersSelector(getCreationError); + const formEntry = useEventFiltersSelector(getFormEntry); + const toasts = useToasts(); + const [wasAlreadyHandled] = useState(new WeakSet()); + + if (creationSuccessful && formEntry && !wasAlreadyHandled.has(formEntry)) { + wasAlreadyHandled.add(formEntry); + toasts.addSuccess(getCreationSuccessMessage(formEntry)); + } else if (creationError && !wasAlreadyHandled.has(creationError)) { + wasAlreadyHandled.add(creationError); + toasts.addDanger(getCreationErrorMessage(creationError)); + } +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts new file mode 100644 index 000000000000..711ab8224ea6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; +import { ServerApiError } from '../../../../common/types'; + +export const getCreationSuccessMessage = ( + entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined +) => { + return i18n.translate('xpack.securitySolution.eventFilter.form.successToastTitle', { + defaultMessage: '"{name}" has been added to the event exceptions list.', + values: { name: entry?.name }, + }); +}; + +export const getCreationErrorMessage = (creationError: ServerApiError) => { + return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle', { + defaultMessage: 'There was an error creating the new exception: "{error}"', + values: { error: creationError.message }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx new file mode 100644 index 000000000000..37f7dff8408a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Provider } from 'react-redux'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { NotificationsStart } from 'kibana/public'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public/context'; +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../shared_imports'; + +import { createGlobalNoMiddlewareStore, ecsEventMock } from '../test_utils'; +import { useEventFiltersNotification } from './hooks'; +import { getCreationErrorMessage, getCreationSuccessMessage } from './translations'; +import { getInitialExceptionFromEvent } from '../store/utils'; +import { + getLastLoadedResourceState, + FailedResourceState, +} from '../../../state/async_resource_state'; + +const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications; + +const renderNotifications = ( + store: ReturnType, + notifications: NotificationsStart +) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + return renderHook(useEventFiltersNotification, { wrapper: Wrapper }); +}; + +describe('EventFiltersNotification', () => { + it('renders correctly initially', () => { + const notifications = mockNotifications(); + + renderNotifications(createGlobalNoMiddlewareStore(), notifications); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows success notification when creation successful', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + act(() => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + }); + + act(() => { + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'LoadedResourceState', + data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema, + }, + }); + }); + + expect(notifications.toasts.addSuccess).toBeCalledWith( + getCreationSuccessMessage( + store.getState()!.management!.eventFilters!.form!.entry as CreateExceptionListItemSchema + ) + ); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows error notification when creation fails', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + act(() => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + }); + + act(() => { + store.dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'FailedResourceState', + error: { message: 'error message', statusCode: 500, error: 'error' }, + lastLoadedState: getLastLoadedResourceState( + store.getState()!.management!.eventFilters!.form!.submissionResourceState + ), + }, + }); + }); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).toBeCalledWith( + getCreationErrorMessage( + (store.getState()!.management!.eventFilters!.form! + .submissionResourceState as FailedResourceState).error + ) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts index 308dbf3df5f7..a06fceab29d4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './async_resource_state'; +export * from '../../../state/async_resource_state'; export * from './trusted_apps_list_page_state'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index e37b0f262603..5041f0a6118d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -6,7 +6,7 @@ */ import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; -import { AsyncResourceState } from '.'; +import { AsyncResourceState } from '../../../state/async_resource_state'; import { GetPolicyListResponse } from '../../policy/types'; export interface Pagination { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts b/x-pack/plugins/security_solution/public/management/state/async_resource_state.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts rename to x-pack/plugins/security_solution/public/management/state/async_resource_state.test.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts b/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts similarity index 97% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts rename to x-pack/plugins/security_solution/public/management/state/async_resource_state.ts index 5208d534221a..1b6dec54ec0b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts +++ b/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts @@ -15,8 +15,8 @@ * - update can fail due to multiple reasons and also needs to be communicated to the user */ -import { Immutable } from '../../../../../common/endpoint/types'; -import { ServerApiError } from '../../../../common/types'; +import { Immutable } from '../../../common/endpoint/types'; +import { ServerApiError } from '../../common/types'; /** * Data type to represent uninitialised state of asynchronous resource. diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index aff1f7db7f06..dae2357d5bb1 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -15,10 +15,12 @@ import { MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, + MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware'; import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware'; +import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/store/middleware'; type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE]; @@ -42,5 +44,9 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( createSubStateSelector(MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE), trustedAppsPageMiddlewareFactory(coreStart, depsStart) ), + substateMiddlewareFactory( + createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE), + eventFiltersPageMiddlewareFactory(coreStart, depsStart) + ), ]; }; diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 683840d85227..bf8cd416a3e3 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -14,6 +14,7 @@ import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, + MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; @@ -24,6 +25,8 @@ import { } from '../pages/endpoint_hosts/store/reducer'; import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders'; import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer'; +import { initialEventFiltersPageState } from '../pages/event_filters/store/builders'; +import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer'; const immutableCombineReducers: ImmutableCombineReducers = combineReducers; @@ -34,6 +37,7 @@ export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState, [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(), + [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(), }; /** @@ -43,4 +47,5 @@ export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer, [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, + [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index a21f182fb9d6..902010a97603 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -10,6 +10,7 @@ import { SecurityPageName } from '../app/types'; import { PolicyDetailsState } from './pages/policy/types'; import { EndpointState } from './pages/endpoint_hosts/types'; import { TrustedAppsListPageState } from './pages/trusted_apps/state'; +import { EventFiltersListPageState } from './pages/event_filters/state'; /** * The type for the management store global namespace. Used mostly internally to reference @@ -21,6 +22,7 @@ export type ManagementState = CombinedState<{ policyDetails: PolicyDetailsState; endpoints: EndpointState; trustedApps: TrustedAppsListPageState; + eventFilters: EventFiltersListPageState; }>; /** diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 757191fdb54e..4a1fdbf0564d 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -59,4 +59,5 @@ export { addEndpointExceptionList, withOptionalSignal, ExceptionBuilder, + transformNewItemOutput, } from '../../lists/public'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx index 7e3db95e7ddc..8cdb263fe42b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx @@ -196,7 +196,7 @@ const AddressLinksItemComponent: React.FC = ({ ) : ( - + { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default); const props = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index a0a0aeb23e8f..cb1fff1cf956 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { CellValueElementProps } from '../../cell_rendering'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; @@ -96,6 +97,8 @@ export const EventColumnView = React.memo( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType ); + const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled'); + // Each action button shall announce itself to screen readers via an `aria-label` // in the following format: // "button description, for the event in row {ariaRowindex}, with columns {columnValues}", @@ -183,7 +186,7 @@ export const EventColumnView = React.memo( key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal'} + disabled={eventType !== 'signal' && (!isEventFilteringEnabled || eventType !== 'raw')} refetch={refetch} onRuleChange={onRuleChange} />, @@ -205,6 +208,7 @@ export const EventColumnView = React.memo( timelineId, timelineType, toggleShowNotes, + isEventFilteringEnabled, ] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 76dbfc553d22..b526ce7e0d99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -21,6 +21,10 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { defaultRowRenderers } from './renderers'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +jest.mock('../../../../common/hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; const mockSort: Sort[] = [ { @@ -88,6 +92,8 @@ describe('Body', () => { totalPages: 1, }; + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + describe('rendering', () => { test('it renders the column headers', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index 92dc02eec48d..e40ccec7bef3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -70,7 +70,7 @@ const HostNameComponent: React.FC = ({ fieldName, contextId, eventId, val { + before(() => esArchiver.load('auditbeat/hosts')); + after(() => esArchiver.unload('auditbeat/hosts')); + + it('Make sure that we get Timeline data', async () => { + await retry.try(async () => { + const resp = await supertest + .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + defaultIndex: ['auditbeat-*'], + docValueFields: DOC_VALUE_FIELDS, + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: FIELD_REQUESTED, + fields: [], + filterQuery: FILTER_VALUE, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }) + .expect(200); + + const timeline = resp.body; + expect(timeline.edges.length).to.be(EDGE_LENGTH); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.totalCount).to.be(TOTAL_COUNT); + expect(timeline.pageInfo.activePage).to.equal(ACTIVE_PAGE); + expect(timeline.pageInfo.querySize).to.equal(PAGE_SIZE); + }); + }); + + it('Make sure that pagination is working in Timeline query', async () => { + await retry.try(async () => { + const resp = await supertest + .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + defaultIndex: ['auditbeat-*'], + docValueFields: DOC_VALUE_FIELDS, + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: FIELD_REQUESTED, + fields: [], + filterQuery: FILTER_VALUE, + pagination: { + activePage: 0, + querySize: LIMITED_PAGE_SIZE, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }) + .expect(200); + + const timeline = resp.body; + expect(timeline.edges.length).to.be(LIMITED_PAGE_SIZE); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.totalCount).to.be(TOTAL_COUNT); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.edges[0]!.node.ecs.host!.name).to.eql([HOST_NAME]); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index 18c315a3b8c3..3f9afba18b9e 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('SecuritySolution Endpoints', () => { loadTestFile(require.resolve('./authentications')); + loadTestFile(require.resolve('./events')); loadTestFile(require.resolve('./hosts')); loadTestFile(require.resolve('./host_details')); loadTestFile(require.resolve('./kpi_network')); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts index 8e085fab98eb..f40ade6c4cfa 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { pick, sortBy } from 'lodash'; +import moment from 'moment'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -33,22 +34,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { path: { serviceName: 'opbeans-java' }, query: { latencyAggregationType: LatencyAggregationType.avg, - start, - end, transactionType: 'request', + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), }, }, }); - expect(response.status).to.be(200); - expect(response.body.serviceInstances).to.eql([]); + expect(response.body.currentPeriod).to.eql([]); + expect(response.body.previousPeriod).to.eql([]); }); }); } ); registry.when( - 'Service overview instances main statistics when data is loaded', + 'Service overview instances main statistics when data is loaded without comparison', { config: 'basic', archives: [archiveName] }, () => { describe('fetching java data', () => { @@ -72,11 +75,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns a service node item', () => { - expect(response.body.serviceInstances.length).to.be.greaterThan(0); + expect(response.body.currentPeriod.length).to.be.greaterThan(0); }); it('returns statistics for each service node', () => { - const item = response.body.serviceInstances[0]; + const item = response.body.currentPeriod[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -86,7 +89,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); + const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); @@ -141,7 +144,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns statistics for each service node', () => { - const item = response.body.serviceInstances[0]; + const item = response.body.currentPeriod[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -151,7 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); + const items = sortBy(response.body.currentPeriod, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); @@ -181,4 +184,90 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } ); + + registry.when( + 'Service overview instances main statistics when data is loaded with comparison', + { config: 'basic', archives: [archiveName] }, + () => { + describe('fetching java data', () => { + let response: { + body: APIReturnType<`GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`>; + }; + + beforeEach(async () => { + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, + params: { + path: { serviceName: 'opbeans-java' }, + query: { + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }, + }); + }); + + it('returns a service node item', () => { + expect(response.body.currentPeriod.length).to.be.greaterThan(0); + expect(response.body.previousPeriod.length).to.be.greaterThan(0); + }); + + it('returns statistics for each service node', () => { + const currentItem = response.body.currentPeriod[0]; + + expect(isFiniteNumber(currentItem.cpuUsage)).to.be(true); + expect(isFiniteNumber(currentItem.memoryUsage)).to.be(true); + expect(isFiniteNumber(currentItem.errorRate)).to.be(true); + expect(isFiniteNumber(currentItem.throughput)).to.be(true); + expect(isFiniteNumber(currentItem.latency)).to.be(true); + + const previousItem = response.body.previousPeriod[0]; + + expect(isFiniteNumber(previousItem.cpuUsage)).to.be(true); + expect(isFiniteNumber(previousItem.memoryUsage)).to.be(true); + expect(isFiniteNumber(previousItem.errorRate)).to.be(true); + expect(isFiniteNumber(previousItem.throughput)).to.be(true); + expect(isFiniteNumber(previousItem.latency)).to.be(true); + }); + + it('returns the right data', () => { + const items = sortBy(response.body.previousPeriod, 'serviceNodeName'); + + const serviceNodeNames = items.map((item) => item.serviceNodeName); + + expectSnapshot(items.length).toMatchInline(`1`); + + expectSnapshot(serviceNodeNames).toMatchInline(` + Array [ + "02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c", + ] + `); + + const item = items[0]; + + const values = pick(item, [ + 'cpuUsage', + 'memoryUsage', + 'errorRate', + 'throughput', + 'latency', + ]); + + expectSnapshot(values).toMatchInline(` + Object { + "cpuUsage": 0.0120666666666667, + "errorRate": 0.111111111111111, + "latency": 379742.555555556, + "memoryUsage": 0.939879608154297, + "throughput": 3, + } + `); + }); + }); + } + ); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 7d69e006666c..4ae949d0cba8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -20,6 +20,7 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, + getOpenSignals, getRuleForSignalTesting, getSignalsByIds, getSignalsByRuleIds, @@ -39,9 +40,9 @@ export const ID = 'BhbXBmkBR346wHgn4PeZ'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/97584 - describe.skip('Generating signals from source indexes', () => { + describe('Generating signals from source indexes', () => { beforeEach(async () => { await createSignalsIndex(supertest); }); @@ -728,9 +729,8 @@ export default ({ getService }: FtrProviderContext) => { ], }, }; - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]); + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); expect(signalsOpen.hits.hits.length).eql(0); }); @@ -753,9 +753,8 @@ export default ({ getService }: FtrProviderContext) => { ], }, }; - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]); + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); expect(signalsOpen.hits.hits.length).eql(0); }); @@ -778,9 +777,8 @@ export default ({ getService }: FtrProviderContext) => { ], }, }; - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]); + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); expect(signalsOpen.hits.hits.length).eql(1); const signal = signalsOpen.hits.hits[0]; expect(signal._source.signal.threshold_result).eql({ @@ -814,9 +812,8 @@ export default ({ getService }: FtrProviderContext) => { value: 22, }, }; - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]); + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); expect(signalsOpen.hits.hits.length).eql(0); }); @@ -833,9 +830,8 @@ export default ({ getService }: FtrProviderContext) => { value: 21, }, }; - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]); + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); expect(signalsOpen.hits.hits.length).eql(1); const signal = signalsOpen.hits.hits[0]; expect(signal._source.signal.threshold_result).eql({ diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 779c4d767c00..33a4e4048278 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -34,6 +34,7 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); expect(body.item.is_managed).to.equal(false); + expect(body.item.status).to.be('active'); }); it('sets given is_managed value', async () => { @@ -140,6 +141,7 @@ export default function ({ getService }: FtrProviderContext) { expect(newPolicy).to.eql({ name: 'Copied policy', + status: 'active', description: 'Test', is_managed: false, namespace: 'default', @@ -242,6 +244,7 @@ export default function ({ getService }: FtrProviderContext) { const { id, updated_at, ...newPolicy } = updatedPolicy; expect(newPolicy).to.eql({ + status: 'active', name: 'Updated name', description: 'Updated description', namespace: 'default', diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index ad3c224bb923..ac5aabc5c508 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -157,7 +157,8 @@ export default function (providerContext: FtrProviderContext) { expect(body).to.eql({ agent2: { success: false, - error: 'Cannot reassign an agent from hosted agent policy policy1', + error: + 'Cannot reassign an agent from hosted agent policy policy1 in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.', }, INVALID_ID: { success: false, @@ -165,7 +166,8 @@ export default function (providerContext: FtrProviderContext) { }, agent3: { success: false, - error: 'Cannot reassign an agent from hosted agent policy policy1', + error: + 'Cannot reassign an agent from hosted agent policy policy1 in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.', }, }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index f0e41d75136c..df213e82bac7 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -138,11 +138,13 @@ export default function (providerContext: FtrProviderContext) { expect(unenrolledBody).to.eql({ agent2: { success: false, - error: 'Cannot unenroll agent2 from a hosted agent policy policy1', + error: + 'Cannot unenroll agent2 from a hosted agent policy policy1 in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.', }, agent3: { success: false, - error: 'Cannot unenroll agent3 from a hosted agent policy policy1', + error: + 'Cannot unenroll agent3 from a hosted agent policy policy1 in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.', }, }); // but agents are still enrolled diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 142c360e9232..b692699182ca 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -593,7 +593,11 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(body).to.eql({ - agent1: { success: false, error: 'Cannot upgrade agent in hosted agent policy policy1' }, + agent1: { + success: false, + error: + 'Cannot upgrade agent in hosted agent policy policy1 in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.', + }, agent2: { success: true }, }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts index 98272294b330..8740deaeddd6 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -33,7 +33,7 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { const $ = await table.parseDomContent(); return $.findTestSubjects('alert-instance-row') .toArray() - .map((row: CheerioElement) => { + .map((row) => { return { instance: $(row) .findTestSubject('alertInstancesTableCell-instance') @@ -87,7 +87,7 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { $.findTestSubjects('alert-instance-row') .toArray() .filter( - (row: CheerioElement) => + (row) => $(row) .findTestSubject('alertInstancesTableCell-instance') .find('.euiTableCellContent') diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index e5971ddba415..5fa442e28903 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -6,7 +6,10 @@ */ import expect from '@kbn/expect'; -import { CustomCheerioStatic } from 'test/functional/services/lib/web_element_wrapper/custom_cheerio_api'; +import { + CustomCheerio, + CustomCheerioStatic, +} from 'test/functional/services/lib/web_element_wrapper/custom_cheerio_api'; import { FtrProviderContext } from '../ftr_provider_context'; const ENTER_KEY = '\uE007'; @@ -16,7 +19,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) const retry = getService('retry'); const testSubjects = getService('testSubjects'); - function getRowItemData(row: CheerioElement, $: CustomCheerioStatic) { + function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) { return { name: $(row).findTestSubject('alertsTableCell-name').find('.euiTableCellContent').text(), tagsText: $(row) @@ -79,7 +82,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) const $ = await table.parseDomContent(); return $.findTestSubjects('connectors-row') .toArray() - .map((row: CheerioElement) => { + .map((row) => { return { name: $(row) .findTestSubject('connectorsTableCell-name') @@ -97,7 +100,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) const $ = await table.parseDomContent(); return $.findTestSubjects('alert-row') .toArray() - .map((row: CheerioElement) => { + .map((row) => { return getRowItemData(row, $); }); }, @@ -106,7 +109,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) const $ = await table.parseDomContent(); return $.findTestSubjects('alert-row') .toArray() - .map((row: CheerioElement) => { + .map((row) => { const rowItem = getRowItemData(row, $); return { ...rowItem, diff --git a/yarn.lock b/yarn.lock index f8549853b478..f4d768417496 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1147,7 +1147,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -2071,7 +2071,7 @@ chalk "^2.0.1" slash "^2.0.0" -"@jest/console@^26.5.2", "@jest/console@^26.6.2": +"@jest/console@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== @@ -2127,15 +2127,6 @@ "@types/node" "*" jest-mock "^26.6.2" -"@jest/fake-timers@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" - integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== - dependencies: - "@jest/types" "^24.9.0" - jest-message-util "^24.9.0" - jest-mock "^24.9.0" - "@jest/fake-timers@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" @@ -2157,38 +2148,6 @@ "@jest/types" "^26.6.2" expect "^26.6.2" -"@jest/reporters@^26.5.2": - version "26.5.3" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.3.tgz#e810e9c2b670f33f1c09e9975749260ca12f1c17" - integrity sha512-X+vR0CpfMQzYcYmMFKNY9n4jklcb14Kffffp7+H/MqitWnb0440bW2L76NGWKAa+bnXhNoZr+lCVtdtPmfJVOQ== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.5.2" - "@jest/test-result" "^26.5.2" - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^26.5.2" - jest-resolve "^26.5.2" - jest-util "^26.5.2" - jest-worker "^26.5.0" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^6.0.1" - optionalDependencies: - node-notifier "^8.0.0" - "@jest/reporters@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" @@ -2248,7 +2207,7 @@ "@jest/types" "^24.9.0" "@types/istanbul-lib-coverage" "^2.0.0" -"@jest/test-result@^26.5.2", "@jest/test-result@^26.6.2": +"@jest/test-result@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== @@ -2269,7 +2228,7 @@ jest-runner "^26.6.3" jest-runtime "^26.6.3" -"@jest/transform@^26.0.0", "@jest/transform@^26.5.2", "@jest/transform@^26.6.2": +"@jest/transform@^26.0.0", "@jest/transform@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== @@ -2309,7 +2268,7 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^26.5.2", "@jest/types@^26.6.2": +"@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== @@ -2726,7 +2685,7 @@ version "0.0.0" uid "" -"@kbn/std@link:packages/kbn-std": +"@kbn/std@link:bazel-bin/packages/kbn-std/npm_module": version "0.0.0" uid "" @@ -4292,23 +4251,24 @@ resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" integrity sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw== -"@testing-library/dom@^7.24.2": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e" - integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA== +"@testing-library/dom@^7.28.1", "@testing-library/dom@^7.30.3": + version "7.30.3" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.30.3.tgz#779ea9bbb92d63302461800a388a5a890ac22519" + integrity sha512-7JhIg2MW6WPwyikH2iL3o7z+FTVgSOd2jqCwTAHqK7Qal2gRRYiUQyURAxtbK9VXm/UTyG9bRihv8C5Tznr2zw== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.10.3" + "@babel/runtime" "^7.12.5" "@types/aria-query" "^4.2.0" aria-query "^4.2.2" chalk "^4.1.0" - dom-accessibility-api "^0.5.1" - pretty-format "^26.4.2" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" -"@testing-library/jest-dom@^5.11.4": - version "5.11.4" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.4.tgz#f325c600db352afb92995c2576022b35621ddc99" - integrity sha512-6RRn3epuweBODDIv3dAlWjOEHQLpGJHB2i912VS3JQtsD22+ENInhdDNl4ZZQiViLlIfFinkSET/J736ytV9sw== +"@testing-library/jest-dom@^5.11.10": + version "5.11.10" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.10.tgz#1cd90715023e1627f5ed26ab3b38e6f22d77046c" + integrity sha512-FuKiq5xuk44Fqm0000Z9w0hjOdwZRNzgx7xGGxQYepWFZy+OYUMOT/wPI4nLYXCaVltNVpU1W/qmD88wLWDsqQ== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" @@ -4319,28 +4279,32 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react-hooks@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.4.1.tgz#1f8ccd21208086ec228d9743fe40b69d0efcd7e5" - integrity sha512-LbzvE7oKsVzuW1cxA/aOeNgeVvmHWG2p/WSzalIGyWuqZT3jVcNDT5KPEwy36sUYWde0Qsh32xqIUFXukeywXg== +"@testing-library/react-hooks@*", "@testing-library/react-hooks@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-5.1.1.tgz#1fbaae8a4e8a4a7f97b176c23e1e890c41bbbfa5" + integrity sha512-52D2XnpelFDefnWpy/V6z2qGNj8JLIvW5DjYtelMvFXdEyWiykSaI7IXHwFy4ICoqXJDmmwHAiFRiFboub/U5g== dependencies: - "@babel/runtime" "^7.5.4" - "@types/testing-library__react-hooks" "^3.3.0" + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + filter-console "^0.1.1" + react-error-boundary "^3.1.0" -"@testing-library/react@^11.0.4": - version "11.0.4" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.0.4.tgz#c84082bfe1593d8fcd475d46baee024452f31dee" - integrity sha512-U0fZO2zxm7M0CB5h1+lh31lbAwMSmDMEMGpMT3BUPJwIjDEKYWOV4dx7lb3x2Ue0Pyt77gmz/VropuJnSz/Iew== +"@testing-library/react@^11.2.6": + version "11.2.6" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" + integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ== dependencies: - "@babel/runtime" "^7.11.2" - "@testing-library/dom" "^7.24.2" + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" -"@testing-library/user-event@^12.1.6": - version "12.1.6" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.1.6.tgz#f550b138dfdc20387b89cbe3e9f3d969ab10c2bd" - integrity sha512-BdSe6cmzDEapTBH3s1NKbzu+GyX5bJKraKwVpM2vZF1+EEWxZr0EiA0z9bA5Nux8P+6nKMOZKsXQrj5q/kicfQ== +"@testing-library/user-event@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.1.1.tgz#1e011de944cf4d2a917cef6c3046c26389943e24" + integrity sha512-B4roX+0mpXKGj8ndd38YoIo3IV9pmTTWxr/2cOke5apTtrNabEUE0KMBccpcAcYlfPcr7uMu+dxeeC3HdXd9qQ== dependencies: - "@babel/runtime" "^7.10.2" + "@babel/runtime" "^7.12.5" "@ts-morph/common@~0.7.0": version "0.7.3" @@ -4621,10 +4585,12 @@ resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" integrity sha512-jtV6Bv/j+xk4gcXeLlESwNc/m/I/dIZA0xrt29g0uKcjyPob8iisj/5z0ARE+Ldfx4MxjNFNECG0z++J7zJgqg== -"@types/cheerio@*", "@types/cheerio@^0.22.10": - version "0.22.10" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6" - integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg== +"@types/cheerio@*", "@types/cheerio@^0.22.22", "@types/cheerio@^0.22.28": + version "0.22.28" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.28.tgz#90808aabb44fec40fa2950f4c72351e3e4eb065b" + integrity sha512-ehUMGSW5IeDxJjbru4awKYMlKGmo1wSSGUVqXtYwlgmUM8X1a0PZttEIm6yEY7vHsY/hh6iPnklF213G0UColw== + dependencies: + "@types/node" "*" "@types/chroma-js@^1.4.2": version "1.4.2" @@ -4763,10 +4729,10 @@ resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.33.tgz#b0fd37dc674f498223b6d68c313bdfd71f4d812b" integrity sha512-n/g9pqJEpE4fyUE8VvHNGtl7E2Wv8TCroNwfgAeJKRV4ghDENahtrAo1KMsFNIejBD2gDAlEUa4CM4oEEd8p9Q== -"@types/enzyme@^3.10.5": - version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" - integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== +"@types/enzyme@^3.10.8": + version "3.10.8" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.8.tgz#ad7ac9d3af3de6fd0673773123fafbc63db50d42" + integrity sha512-vlOuzqsTHxog6PV79+tvOHFb6hq4QZKMq1lLD9MaWD1oec2lHTKndn76XOpSwCA0oFTaIbKVPrgM3k78Jjd16g== dependencies: "@types/cheerio" "*" "@types/react" "*" @@ -4881,11 +4847,6 @@ dependencies: "@types/node" "*" -"@types/graphql@^0.13.2": - version "0.13.4" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.13.4.tgz#55ae9c29f0fd6b85ee536f5c72b4769d5c5e06b1" - integrity sha512-B4yel4ro2nTb3v0pYO8vO6SjgvFJSrwUY+IO6TUSLdOSB+gQFslylrhRCHxvXMIhxB71mv5PEE9dAX+24S8sew== - "@types/gulp-zip@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/gulp-zip/-/gulp-zip-4.0.1.tgz#96cd0b994219f9ae3bbbec7ec3baa043fba9d9ef" @@ -5068,27 +5029,27 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" - integrity sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g== +"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.5.tgz#47ce738870be99898ed6d7b08dbf0240c74ae553" + integrity sha512-AaPPw2tE8ewfjD6qGLkEd4DOfM6pPOK7ob/RSOe1Z8Oo70r9Jgo0SlWyfxslPAOvLfQukQtiVPm6DcnjSoZU5A== dependencies: "@types/jest" "*" -"@types/jest-when@^2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@types/jest-when/-/jest-when-2.7.1.tgz#0b04a33a48a17370c390e9830a975822b3ac5e32" - integrity sha512-PRrGzDkU859cdkFL2KwWN4fRLRDGIUkRNT0StbthhKmj+naU4wImpoJeMnhjprvSou4pKAzU0dKfdQvjceJVhg== +"@types/jest-when@^2.7.2": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/jest-when/-/jest-when-2.7.2.tgz#619fbc5f623bcd0b29efde0e4993c7f0d50d026d" + integrity sha512-vOtj0cev6vO1VX7Jbfg/qvy+sfLI64STsHbKVkggK+1kd11rcMGzFpZKBxUvQfsm4JRULCBISu+qrfs7fYZFGg== dependencies: "@types/jest" "*" -"@types/jest@*", "@types/jest@^26.0.14": - version "26.0.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.14.tgz#078695f8f65cb55c5a98450d65083b2b73e5a3f3" - integrity sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg== +"@types/jest@*", "@types/jest@^26.0.22": + version "26.0.22" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6" + integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw== dependencies: - jest-diff "^25.2.1" - pretty-format "^25.2.1" + jest-diff "^26.0.0" + pretty-format "^26.0.0" "@types/jest@^25.1.1": version "25.2.3" @@ -5564,7 +5525,7 @@ dependencies: "@types/react" "*" -"@types/react-dom@^16.9.8": +"@types/react-dom@>=16.9.0", "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== @@ -5639,7 +5600,7 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@*", "@types/react-test-renderer@^16.9.1": +"@types/react-test-renderer@>=16.9.0", "@types/react-test-renderer@^16.9.1": version "16.9.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ== @@ -5668,7 +5629,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.36": +"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.9.36": version "16.9.36" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.36.tgz#ade589ff51e2a903e34ee4669e05dbfa0c1ce849" integrity sha512-mGgUb/Rk/vGx4NCvquRuSH0GHBQKb1OqpGS9cT9lFxlTLHZgkksgI60TuIxubmn7JuCb+sENHhQciqa0npm0AQ== @@ -5859,19 +5820,19 @@ resolved "https://registry.yarnpkg.com/@types/tempy/-/tempy-0.2.0.tgz#8b7a93f6912aef25cc0b8d8a80ff974151478685" integrity sha512-YaX74QljqR45Xu7dd22wMvzTS+ItUiSyDl9XJl6WTgYNE09r2TF+mV2FDjWRM5Sdzf9C9dXRTUdz9J5SoEYxXg== -"@types/testing-library__jest-dom@^5.9.1", "@types/testing-library__jest-dom@^5.9.3": - version "5.9.3" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.3.tgz#574039e210140a536c6ec891063289fb742a75eb" - integrity sha512-5YxiCFA2vk0cxq2LIxYgHBpFlnJvMH9bkUIVNin+1GXT+LZgVOgXBeEyyo2ZrGXMO/KWe1ZV3p7Kb6LJAvJasw== +"@types/testing-library__jest-dom@^5.9.1", "@types/testing-library__jest-dom@^5.9.5": + version "5.9.5" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0" + integrity sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ== dependencies: "@types/jest" "*" -"@types/testing-library__react-hooks@^3.3.0", "@types/testing-library__react-hooks@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.0.tgz#be148b7fa7d19cd3349c4ef9d9534486bc582fcc" - integrity sha512-QYLZipqt1hpwYsBU63Ssa557v5wWbncqL36No59LI7W3nCMYKrLWTnYGn2griZ6v/3n5nKXNYkTeYpqPHY7Ukg== +"@types/testing-library__react-hooks@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-4.0.0.tgz#2612eabbbb762968985fc1aa35f979caaa78f118" + integrity sha512-UzZUXthQtVjDruR2YA+hqg9ux5AfmZ8Kaw+QDungax+T7wb/5NC4x7YOpIqRx7oY3KksGQ69bzNE/xwzb5NslQ== dependencies: - "@types/react-test-renderer" "*" + "@testing-library/react-hooks" "*" "@types/through@*": version "0.0.30" @@ -6544,21 +6505,20 @@ airbnb-js-shims@^2.2.1: string.prototype.padstart "^3.0.0" symbol.prototype.description "^1.0.0" -airbnb-prop-types@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" - integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== +airbnb-prop-types@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" + integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== dependencies: - array.prototype.find "^2.1.0" - function.prototype.name "^1.1.1" - has "^1.0.3" - is-regex "^1.0.4" - object-is "^1.0.1" + array.prototype.find "^2.1.1" + function.prototype.name "^1.1.2" + is-regex "^1.1.0" + object-is "^1.1.2" object.assign "^4.1.0" - object.entries "^1.1.0" + object.entries "^1.1.2" prop-types "^15.7.2" prop-types-exact "^1.2.0" - react-is "^16.9.0" + react-is "^16.13.1" ajv-errors@^1.0.0: version "1.0.0" @@ -7120,13 +7080,13 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" - integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== +array.prototype.find@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" + integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== dependencies: define-properties "^1.1.3" - es-abstract "^1.13.0" + es-abstract "^1.17.4" array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: version "1.2.3" @@ -8831,6 +8791,14 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -9012,7 +8980,7 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9569,17 +9537,12 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-convert@~0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" - integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= - color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -11708,10 +11671,10 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.2.tgz#ef3cdb5d3f0d599d8f9c8b18df2fb63c9793739d" - integrity sha512-k7hRNKAiPJXD2aBqfahSo4/01cTsKWXf+LqJgglnkN2Nz8TsxXKQBXHhKe0Ye9fEfHEZY49uSA5Sr3AqP/sWKA== +dom-accessibility-api@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" + integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== dom-converter@~0.2: version "0.2.0" @@ -12218,46 +12181,48 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== -enzyme-adapter-react-16@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501" - integrity sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q== +enzyme-adapter-react-16@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901" + integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g== dependencies: - enzyme-adapter-utils "^1.13.0" - enzyme-shallow-equal "^1.0.1" + enzyme-adapter-utils "^1.14.0" + enzyme-shallow-equal "^1.0.4" has "^1.0.3" - object.assign "^4.1.0" - object.values "^1.1.1" + object.assign "^4.1.2" + object.values "^1.1.2" prop-types "^15.7.2" - react-is "^16.12.0" + react-is "^16.13.1" react-test-renderer "^16.0.0-0" semver "^5.7.0" -enzyme-adapter-utils@^1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz#01c885dde2114b4690bf741f8dc94cee3060eb78" - integrity sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ== +enzyme-adapter-utils@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0" + integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== dependencies: - airbnb-prop-types "^2.15.0" - function.prototype.name "^1.1.2" - object.assign "^4.1.0" - object.fromentries "^2.0.2" + airbnb-prop-types "^2.16.0" + function.prototype.name "^1.1.3" + has "^1.0.3" + object.assign "^4.1.2" + object.fromentries "^2.0.3" prop-types "^15.7.2" semver "^5.7.1" -enzyme-shallow-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e" - integrity sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ== +enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" + integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== dependencies: has "^1.0.3" - object-is "^1.0.2" + object-is "^1.1.2" -enzyme-to-json@^3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.4.tgz#b30726c59091d273521b6568c859e8831e94d00e" - integrity sha512-50LELP/SCPJJGic5rAARvU7pgE3m1YaNj7JLM+Qkhl5t7PAs6fiyc8xzc50RnkKPFQCv0EeFVjEWdIFRGPWMsA== +enzyme-to-json@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz#d60740950bc7ca6384dfe6fe405494ec5df996bc" + integrity sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg== dependencies: + "@types/cheerio" "^0.22.22" lodash "^4.17.15" react-is "^16.12.0" @@ -12329,57 +12294,27 @@ error-stack-parser@^2.0.4, error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.9.0: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.17.2: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5, es-abstract@^1.18.0-next.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.9.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== dependencies: + call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" + get-intrinsic "^1.1.1" has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.2" + is-string "^1.0.5" + object-inspect "^1.9.0" object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" es-array-method-boxes-properly@^1.0.0: version "1.0.0" @@ -12702,10 +12637,10 @@ eslint-plugin-import@^2.22.1: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-plugin-jest@^24.0.2: - version "24.0.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.0.2.tgz#4bf0fcdc86289d702a7dacb430b4363482af773b" - integrity sha512-DSBLNpkKDOpUJQkTGSs5sVJWsu0nDyQ2rYxkr0Eh7nrkc5bMUr/dlDbtTj3l8y6UaCVsem6rryF1OZrKnz1S5g== +eslint-plugin-jest@^24.3.4: + version "24.3.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.4.tgz#6d90c3554de0302e879603dd6405474c98849f19" + integrity sha512-3n5oY1+fictanuFkTWPwSlehugBTAgwLnYLFsCllzE3Pl1BwywHl5fL0HFxmMjoQY8xhUDk8uAWc3S4JOHGh3A== dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" @@ -13552,6 +13487,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-console@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c" + integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg== + finalhandler@1.1.2, finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -14071,24 +14011,25 @@ function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.0, function.prototype.name@^1.1.1, function.prototype.name@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45" - integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg== +function.prototype.name@^1.1.0, function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83" + integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - functions-have-names "^1.2.0" + es-abstract "^1.18.0-next.2" + functions-have-names "^1.2.2" functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" - integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== +functions-have-names@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" + integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== fuse.js@^3.6.1: version "3.6.1" @@ -14170,6 +14111,15 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -14705,18 +14655,6 @@ graphlib@^2.1.8: dependencies: lodash "^4.17.15" -graphql-tag@^2.10.3: - version "2.10.3" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" - integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== - -graphql@^0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" - integrity sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog== - dependencies: - iterall "^1.2.1" - grid-index@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7" @@ -14963,6 +14901,11 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-color@~0.1.0: version "0.1.7" resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" @@ -14990,10 +14933,10 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has-unicode@^2.0.0: version "2.0.1" @@ -16050,14 +15993,14 @@ is-arrayish@^0.2.1: integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-arrayish@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" - integrity sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0= + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== +is-bigint@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" + integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== is-binary-path@^1.0.0: version "1.0.1" @@ -16073,10 +16016,12 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.0, is-boolean-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== +is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" + integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + dependencies: + call-bind "^1.0.0" is-buffer@^1.0.2, is-buffer@^1.1.0, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" @@ -16088,15 +16033,10 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - -is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== is-ci@^2.0.0: version "2.0.0" @@ -16314,10 +16254,10 @@ is-negated-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== is-nil@^1.0.0: version "1.0.1" @@ -16334,7 +16274,7 @@ is-npm@^4.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== -is-number-object@^1.0.3, is-number-object@^1.0.4: +is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== @@ -16448,11 +16388,12 @@ is-redirect@^1.0.0: resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== +is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1, is-regex@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" + integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== dependencies: + call-bind "^1.0.2" has-symbols "^1.0.1" is-regexp@^2.0.0: @@ -16770,11 +16711,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterall@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" - integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== - iterate-iterator@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" @@ -16803,13 +16739,13 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" -jest-canvas-mock@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7" - integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw== +jest-canvas-mock@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz#9535d14bc18ccf1493be36ac37dd349928387826" + integrity sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg== dependencies: cssfontparser "^1.2.1" - parse-color "^1.0.0" + moo-color "^1.0.2" jest-changed-files@^26.6.2: version "26.6.2" @@ -16910,7 +16846,7 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" -jest-diff@^26.6.2: +jest-diff@^26.0.0, jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== @@ -16938,15 +16874,6 @@ jest-each@^26.6.2: jest-util "^26.6.2" pretty-format "^26.6.2" -jest-environment-jsdom-thirteen@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom-thirteen/-/jest-environment-jsdom-thirteen-1.0.1.tgz#113e3c8aed945dadbc826636fa21139c69567bb5" - integrity sha512-Zi7OuKF7HMLlBvomitd5eKp5Ykc4Wvw0d+i+cpbCaE+7kmvL24SO4ssDmKrT++aANXR4T8+pmoJIlav5gr2peQ== - dependencies: - jest-mock "^24.0.0" - jest-util "^24.0.0" - jsdom "^13.0.0" - jest-environment-jsdom@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" @@ -16987,7 +16914,7 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.5.2, jest-haste-map@^26.6.2: +jest-haste-map@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== @@ -17089,13 +17016,6 @@ jest-message-util@^26.6.2: slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^24.0.0, jest-mock@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" - integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== - dependencies: - "@jest/types" "^24.9.0" - jest-mock@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" @@ -17144,7 +17064,7 @@ jest-resolve@^24.9.0: jest-pnp-resolver "^1.2.1" realpath-native "^1.1.0" -jest-resolve@^26.5.2, jest-resolve@^26.6.2: +jest-resolve@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== @@ -17225,13 +17145,13 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-silent-reporter@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/jest-silent-reporter/-/jest-silent-reporter-0.2.1.tgz#554dd62b800989cdbcfba22bf30a1c0db6ad289c" - integrity sha512-nEO3oOFHtEXFjlRCbJOlvEWA7ZHyyyvMsU4WHuAhinYBOI4PiX1EIbsZfQZ/cxHcYliHBU9zY8bPxMPdBGksYw== +jest-silent-reporter@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jest-silent-reporter/-/jest-silent-reporter-0.5.0.tgz#5fd8ccd61665227e3bf19d908b7350719d06ff38" + integrity sha512-epdLt8Oj0a1AyRiR6F8zx/1SVT1Mi7VU3y4wB2uOBHs/ohIquC7v2eeja7UN54uRPyHInIKWdL+RdG228n5pJQ== dependencies: - chalk "^2.3.1" - jest-util "^24.0.0" + chalk "^4.0.0" + jest-util "^26.0.0" jest-snapshot@^24.1.0: version "24.9.0" @@ -17288,32 +17208,14 @@ jest-specific-snapshot@^4.0.0: dependencies: jest-snapshot "^26.3.0" -jest-styled-components@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-7.0.2.tgz#b7711871ea74a04491b12bad123fa35cc65a2a80" - integrity sha512-i1Qke8Jfgx0Why31q74ohVj9S2FmMLUE8bNRSoK4DgiurKkXG6HC4NPhcOLAz6VpVd9wXkPn81hOt4aAQedqsA== +jest-styled-components@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-7.0.3.tgz#cc0b031f910484e68f175568682f3969ff774b2c" + integrity sha512-jj9sWyshehUnB0P9WFUaq9Bkh6RKYO8aD8lf3gUrXRwg/MRddTFk7U9D9pC4IAI3v9fbz4vmrMxwaecTpG8NKA== dependencies: css "^2.2.4" -jest-util@^24.0.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" - integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== - dependencies: - "@jest/console" "^24.9.0" - "@jest/fake-timers" "^24.9.0" - "@jest/source-map" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - callsites "^3.0.0" - chalk "^2.0.1" - graceful-fs "^4.1.15" - is-ci "^2.0.0" - mkdirp "^0.5.1" - slash "^2.0.0" - source-map "^0.6.0" - -jest-util@^26.5.2, jest-util@^26.6.2: +jest-util@^26.0.0, jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== @@ -17350,10 +17252,10 @@ jest-watcher@^26.6.2: jest-util "^26.6.2" string-length "^4.0.1" -jest-when@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/jest-when/-/jest-when-2.7.2.tgz#b7b4225e8882bd84a1cfd09216b2c63d22f892bd" - integrity sha512-GuVzimG0wW18A5JlYwhHrvuwmWRAQpsnilRVdJktvrZX5V0++al1f/iwITE7+Cud8Rbw/U2eka4tyy7kvxIWnw== +jest-when@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jest-when/-/jest-when-3.2.1.tgz#69b58ff641a399a0f2db5bfee6d8dd40cd065eb8" + integrity sha512-7OuFR5f2AdDPoRs/uk99dEWI+Isc2SFThugPjVUZgLLhWqeGr64rCFuuYcxVXQKwBmF3GG/MCS6zcKR9H86qiw== dependencies: bunyan "^1.8.12" expect "^24.8.0" @@ -17366,7 +17268,7 @@ jest-worker@^25.4.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.2.1, jest-worker@^26.3.0, jest-worker@^26.5.0, jest-worker@^26.6.2: +jest-worker@^26.2.1, jest-worker@^26.3.0, jest-worker@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== @@ -17517,7 +17419,7 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@13.1.0, jsdom@^13.0.0: +jsdom@13.1.0: version "13.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-13.1.0.tgz#fa7356f0cc8111d0f1077cb7800d06f22f1d66c7" integrity sha512-C2Kp0qNuopw0smXFaHeayvharqF3kkcNqlcIlSX71+3XrsOFwkEPLt/9f5JksMmaul2JZYIQuY+WTpqHpQQcLg== @@ -19613,6 +19515,13 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== +moo-color@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64" + integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg== + dependencies: + color-name "^1.1.4" + moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -20419,15 +20328,10 @@ object-identity-map@^1.0.2: dependencies: object.entries "^1.1.0" -object-inspect@^1.6.0, object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-inspect@^1.6.0, object-inspect@^1.7.0, object-inspect@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" + integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== object-inspect@~1.6.0: version "1.6.0" @@ -20442,7 +20346,7 @@ object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: define-properties "^1.1.3" es-abstract "^1.17.5" -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -20461,23 +20365,13 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.4, object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== +object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" has-symbols "^1.0.1" object-keys "^1.1.1" @@ -20500,14 +20394,14 @@ object.entries@^1.0.4, object.entries@^1.1.0, object.entries@^1.1.1, object.entr es-abstract "^1.17.5" has "^1.0.3" -"object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" - integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== +"object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.2, object.fromentries@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8" + integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.18.0-next.2" has "^1.0.3" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: @@ -20541,14 +20435,14 @@ object.reduce@^1.0.0: for-own "^1.0.0" make-iterator "^1.0.0" -object.values@^1.1.0, object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== +object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" + integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.18.0-next.2" has "^1.0.3" objectorarray@^1.0.4: @@ -21044,13 +20938,6 @@ parse-bmfont-xml@^1.1.4: xml-parse-from-string "^1.0.0" xml2js "^0.4.5" -parse-color@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" - integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= - dependencies: - color-convert "~0.5.0" - parse-data-uri@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/parse-data-uri/-/parse-data-uri-0.2.0.tgz#bf04d851dd5c87b0ab238e5d01ace494b604b4c9" @@ -22076,7 +21963,7 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.4.0, pretty-format@^26.4.2, pretty-format@^26.6.2: +pretty-format@^26.0.0, pretty-format@^26.4.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -22824,6 +22711,13 @@ react-element-to-jsx-string@^14.3.1: "@base2/pretty-print-object" "1.0.0" is-plain-object "3.0.0" +react-error-boundary@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.1.tgz#932c5ca5cbab8ec4fe37fd7b415aa5c3a47597e7" + integrity sha512-W3xCd9zXnanqrTUeViceufD3mIW8Ut29BUD+S2f0eO2XCOU8b6UrJfY46RDGe5lxCJzfe4j0yvIfh0RbTZhKJw== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -22927,7 +22821,7 @@ react-intl@^2.8.0: intl-relativeformat "^2.1.0" invariant "^2.1.1" -react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -25874,21 +25768,21 @@ string.prototype.trim@~1.1.2: es-abstract "^1.5.0" function-bind "^1.0.2" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.5" -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.5" string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1: version "1.1.1" @@ -27368,6 +27262,16 @@ umd@^3.0.0: resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== +unbox-primitive@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -28091,15 +27995,6 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1, v8-compile-cache@^2.2.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== -v8-to-istanbul@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz#7ef0e32faa10f841fe4c1b0f8de96ed067c0be1e" - integrity sha512-PzM1WlqquhBvsV+Gco6WSFeg1AGdD53ccMRkFeyHRE/KRZaVacPOmQYP3EeVgDBtKD2BJ8kgynBQ5OtKiHCH+w== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" - v8-to-istanbul@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" @@ -29015,16 +28910,16 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" which-collection@^1.0.1: version "1.0.1"