diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 33204d7396461..b9880c410fc68 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -29,16 +29,20 @@ kibanaPipeline(timeoutMinutes: 180) { catchErrors { print "Agent ${agentNumberInside} - ${agentExecutions} executions" - workers.functional('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + withEnv([ + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { + workers.functional('flaky-test-runner', { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + } } } } diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f64b9e95fbaab..238a21161b129 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,9 +9,14 @@ on: jobs: backport: name: Backport PR - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + if: | + github.event.pull_request.merged == true + && contains(github.event.pull_request.labels.*.name, 'auto-backport') + && ( + (github.event.action == 'labeled' && github.event.label.name == 'auto-backport') + || (github.event.action == 'closed') + ) runs-on: ubuntu-latest - steps: - name: 'Get backport config' run: | diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index 7cd54db5562b7..ad73d03bcbfd8 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -2,7 +2,7 @@ [[profiler-getting-started]] === Getting Started -The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *Search Profiler* +The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *{searchprofiler}* to get started. {searchprofiler} displays the names of the indices searched, the shards in each index, diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 263addc98ee62..613f2d0fbf20c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -318,10 +318,6 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |The cloud plugin adds cloud specific features to Kibana. -|{kib-repo}blob/{branch}/x-pack/plugins/code[code] -|WARNING: Missing README. - - |{kib-repo}blob/{branch}/x-pack/plugins/console_extensions/README.md[consoleExtensions] |This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 0ee7fbc741e00..0c5bef489dd01 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -15,7 +15,7 @@ a| <> | Interact with the REST API of Elasticsearch, including sending requests and viewing API documentation. -a| <> +a| <> | Inspect and analyze your search queries. diff --git a/package.json b/package.json index aac576dbc3561..0fa8ef31ab251 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,7 @@ "moment": "^2.24.0", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.27", - "monaco-editor": "^0.17.0", + "monaco-editor": "^0.22.3", "mustache": "^2.3.2", "ngreact": "^0.5.1", "nock": "12.0.3", @@ -772,7 +772,7 @@ "react-fast-compare": "^2.0.4", "react-grid-layout": "^0.16.2", "react-markdown": "^4.3.1", - "react-monaco-editor": "^0.27.0", + "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 66a00fff089b6..a07d979e2022b 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -39,6 +39,8 @@ const mapLanguageIdToWorker: { [key: string]: any } = { // @ts-ignore window.MonacoEnvironment = { + // needed for functional tests so that we can get value from 'editor' + monaco, getWorker: (module: string, languageId: string) => { const workerSrc = mapLanguageIdToWorker[languageId] || defaultWorkerSrc; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d939e7b3000fa..375ad634cbc15 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48147,13 +48147,13 @@ async function installBazelTools(repoRootPath) { const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed - _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Test if bazelisk is already installed in the correct version + _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Check if we need to remove bazelisk from yarn - const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available + await tryRemoveBazeliskFromYarnGlobal(); // Test if bazelisk is already installed in the correct version - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Check if we need to remove bazelisk from yarn + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available - await tryRemoveBazeliskFromYarnGlobal(); // Install bazelisk if not installed + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index b547c2bc141bd..93acbe09b4eab 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -83,15 +83,15 @@ export async function installBazelTools(repoRootPath: string) { // Check what globals are installed log.debug(`[bazel_tools] verify if bazelisk is installed`); + // Check if we need to remove bazelisk from yarn + await tryRemoveBazeliskFromYarnGlobal(); + // Test if bazelisk is already installed in the correct version const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); - // Check if we need to remove bazelisk from yarn - await tryRemoveBazeliskFromYarnGlobal(); - // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 717be8f413b48..79fc3db86e066 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -93,9 +93,9 @@ module.exports = { // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation transformIgnorePatterns: [ - // ignore all node_modules except monaco-editor which requires babel transforms to handle dynamic import() + // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', ], diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index d04f6e30f3161..7ff5978e1f2ea 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -85,6 +85,13 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ }, ], }, + { + test: /\.(ttf)(\?|$)/, + loader: 'url-loader', + options: { + limit: 8192, + }, + }, ], }, diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json new file mode 100644 index 0000000000000..cbb214b575701 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "codeCoverageTestPlugin", + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts new file mode 100644 index 0000000000000..5499a33fbf739 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Plugin } from './plugin'; + +export function plugin() { + return new Plugin(); +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts new file mode 100644 index 0000000000000..d4704ba05b59c --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CoreSetup } from 'kibana/server'; + +export class Plugin { + constructor() {} + + public setup(core: CoreSetup) {} + + public start() { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS index 1822c3fd95e34..77b2202820350 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS @@ -3,4 +3,4 @@ # For more info, see https://help.github.com/articles/about-codeowners/ # App -/x-pack/plugins/code/ @elastic/kibana-tre +/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin @elastic/kibana-tre diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js index 8fc34d29103b3..8fe61ed76a923 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -39,11 +39,8 @@ describe('Team Assignment', () => { const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); const lines = stdout.split('\n').filter((line) => !line.includes('/target')); expect(lines).toEqual([ - 'x-pack/plugins/code/jest.config.js kibana-tre', - 'x-pack/plugins/code/server/config.ts kibana-tre', - 'x-pack/plugins/code/server/index.ts kibana-tre', - 'x-pack/plugins/code/server/plugin.test.ts kibana-tre', - 'x-pack/plugins/code/server/plugin.ts kibana-tre', + 'src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts kibana-tre', + 'src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts kibana-tre', ]); }); }); diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts index 6debf4b476590..b88ca7e5c22b1 100644 --- a/src/optimize/bundles_route/bundles_route.ts +++ b/src/optimize/bundles_route/bundles_route.ts @@ -10,6 +10,7 @@ import { extname, join } from 'path'; import Hapi from '@hapi/hapi'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; +import agent from 'elastic-apm-node'; import { createDynamicAssetResponse } from './dynamic_asset_response'; import { FileHashCache } from './file_hash_cache'; @@ -101,6 +102,8 @@ function buildRouteForBundles({ method(request: Hapi.Request, h: Hapi.ResponseToolkit) { const ext = extname(request.params.path); + agent.setTransactionName('GET ?/bundles/?'); + if (ext !== '.js' && ext !== '.css') { return h.continue; } diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx index 57fcdef86179f..a5fdfe773a2f8 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx @@ -165,7 +165,6 @@ storiesOf('CodeEditor', module) provideCompletionItems: provideSuggestions, }} options={{ - wordBasedSuggestions: false, quickSuggestions: true, }} /> diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx index c3d465d4f09e9..33f0f311d3a4a 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx @@ -50,9 +50,6 @@ test('editor mount setup', () => { suggestions: [], }), }; - const signatureProvider = { - provideSignatureHelp: () => ({ signatures: [], activeParameter: 0, activeSignature: 0 }), - }; const hoverProvider = { provideHover: (model: monaco.editor.ITextModel, position: monaco.Position) => ({ contents: [], @@ -82,7 +79,6 @@ test('editor mount setup', () => { onChange={() => {}} editorWillMount={editorWillMount} suggestionProvider={suggestionProvider} - signatureProvider={signatureProvider} hoverProvider={hoverProvider} /> ); @@ -99,6 +95,5 @@ test('editor mount setup', () => { // Verify our language features have been registered expect((monaco.languages.onLanguage as jest.Mock).mock.calls.length).toBe(1); expect((monaco.languages.registerCompletionItemProvider as jest.Mock).mock.calls.length).toBe(1); - expect((monaco.languages.registerSignatureHelpProvider as jest.Mock).mock.calls.length).toBe(1); expect((monaco.languages.registerHoverProvider as jest.Mock).mock.calls.length).toBe(1); }); diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index 6667280d0a23b..83ccabe46cdc4 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -64,6 +64,7 @@ export function DashboardPicker(props: DashboardPickerProps) { return ( ( documentId || disableDashboardOptions ? null : 'existing' diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 9f6fd5eabf5c5..c2b5eac4dbb83 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -21,7 +21,6 @@ import { EuiSpacer, } from '@elastic/eui'; -import { pluginServices } from '../services'; import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; import './saved_object_save_modal_dashboard.scss'; @@ -37,9 +36,6 @@ export interface SaveModalDashboardSelectorProps { export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) { const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props; - const { capabilities } = pluginServices.getHooks(); - const { canCreateNewDashboards, canEditDashboards } = capabilities.useService(); - const isDisabled = !copyOnSave && !!documentId; return ( @@ -70,50 +66,44 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp >
- {canEditDashboards() && ( - <> - {' '} - onChange('existing')} - disabled={isDisabled} - /> -
- -
- - - )} - {canCreateNewDashboards() && ( - <> - {' '} - onChange('new')} - disabled={isDisabled} + <> + onChange('existing')} + disabled={isDisabled} + /> +
+ - - - )} +
+ + + <> + onChange('new')} + disabled={isDisabled} + /> + + boolean; canCreateNewDashboards: () => boolean; - canEditDashboards: () => boolean; } export interface PresentationUtilServices { diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index a191e970591f4..546281d083f2f 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -21,6 +21,5 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreSta return { canAccessDashboards: () => Boolean(dashboard.show), canCreateNewDashboards: () => Boolean(dashboard.createNew), - canEditDashboards: () => !Boolean(dashboard.hideWriteControls), }; }; diff --git a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx index 2cb244a4d270b..d518d9718d5e7 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx @@ -115,7 +115,6 @@ function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputPro minimap: { enabled: false, }, - wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', }} diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index bc766d63db5a7..1f1f8c0b5ac80 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -149,6 +149,7 @@ export const VisualizeListing = () => { const calloutMessage = ( <> { request = await browser.execute( - () => (window as any).monaco.editor.getModels()[0].getValue() as string + () => (window as any).MonacoEnvironment.monaco.editor.getModels()[0].getValue() as string ); }); diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 1442a0f728727..fd1166b07f322 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,7 +2,5 @@ source src/dev/ci_setup/setup_env.sh -export NODE_OPTIONS="--max-old-space-size=2048" - checks-reporter-with-killswitch "Jest Unit Tests" \ node scripts/jest --ci --verbose --maxWorkers=8 diff --git a/tsconfig.refs.json b/tsconfig.refs.json index d5482a85856fe..4105f23fd5b3e 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -87,6 +87,7 @@ { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/observability/tsconfig.json" }, + { "path": "./x-pack/plugins/osquery/tsconfig.json" }, { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, { "path": "./x-pack/plugins/reporting/tsconfig.json" }, { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts index a94c3614ae7a8..ac4f89b639c22 100644 --- a/x-pack/plugins/beats_management/common/constants/index.ts +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -7,7 +7,7 @@ export { UNIQUENESS_ENFORCING_TYPES } from './configuration_blocks'; export { INDEX_NAMES } from './index_names'; -export { PLUGIN } from './plugin'; +export { PLUGIN, MANAGEMENT_SECTION } from './plugin'; export { LICENSES, REQUIRED_LICENSES, REQUIRED_ROLES } from './security'; export { TABLE_CONFIG } from './table'; export const BASE_PATH = '/management/ingest/beats_management'; diff --git a/x-pack/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts index 87b600b975fe6..912bc75b98f60 100644 --- a/x-pack/plugins/beats_management/common/constants/plugin.ts +++ b/x-pack/plugins/beats_management/common/constants/plugin.ts @@ -9,3 +9,4 @@ export const PLUGIN = { ID: 'beats_management', }; export const CONFIG_PREFIX = 'xpack.beats'; +export const MANAGEMENT_SECTION = 'beats_management'; diff --git a/x-pack/plugins/beats_management/public/bootstrap.tsx b/x-pack/plugins/beats_management/public/bootstrap.tsx index 04d3eada61129..4a4d3a893286b 100644 --- a/x-pack/plugins/beats_management/public/bootstrap.tsx +++ b/x-pack/plugins/beats_management/public/bootstrap.tsx @@ -15,11 +15,18 @@ import { CoreSetup } from '../../../../src/core/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { BeatsManagementConfigType } from '../common'; +import { MANAGEMENT_SECTION } from '../common/constants'; async function startApp(libs: FrontendLibs, core: CoreSetup) { - await libs.framework.waitUntilFrameworkReady(); + const [startServices] = await Promise.all([ + core.getStartServices(), + libs.framework.waitUntilFrameworkReady(), + ]); - if (libs.framework.licenseIsAtLeast('standard')) { + const capabilities = startServices[0].application.capabilities; + const hasBeatsCapability = capabilities.management.ingest?.[MANAGEMENT_SECTION] ?? false; + + if (libs.framework.licenseIsAtLeast('standard') && hasBeatsCapability) { const mount = async (params: any) => { const [coreStart, pluginsStart] = await core.getStartServices(); setServices(coreStart, pluginsStart, params); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index f86f9c5eb8c74..03a9a77608498 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -11,6 +11,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { MANAGEMENT_SECTION } from '../../../../common/constants'; import { SecurityPluginSetup } from '../../../../../security/public'; import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types'; import { @@ -107,7 +108,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { public registerManagementUI(mount: RegisterManagementAppArgs['mount']) { const section = this.management.sections.section.ingest; section.registerApp({ - id: 'beats_management', + id: MANAGEMENT_SECTION, title: i18n.translate('xpack.beatsManagement.centralManagementLinkLabel', { defaultMessage: 'Beats Central Management', }), diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 0e5300eeb1b07..66b02bdc16408 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -13,6 +13,8 @@ import { i18n } from '@kbn/i18n'; import { Provider } from 'react-redux'; import { BehaviorSubject } from 'rxjs'; +import { includes, remove } from 'lodash'; + import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; @@ -39,6 +41,11 @@ import { initFunctions } from './functions'; // @ts-expect-error untyped local import { appUnload } from './state/actions/app'; +// @ts-expect-error Not going to convert +import { size } from '../canvas_plugin_src/renderers/plot/plugins/size'; +// @ts-expect-error Not going to convert +import { text } from '../canvas_plugin_src/renderers/plot/plugins/text'; + import './style/index.scss'; const { ReadOnlyBadge: strings } = CapabilitiesStrings; @@ -147,6 +154,17 @@ export const initializeCanvas = async ( export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { destroyRegistries(); + // Canvas pollutes the jQuery plot plugins collection with custom plugins that only work in Canvas. + // Remove them when Canvas is unmounted. + // see: ../canvas_plugin_src/renderers/plot/plugins/index.ts + if (includes($.plot.plugins, size)) { + remove($.plot.plugins, size); + } + + if (includes($.plot.plugins, text)) { + remove($.plot.plugins, text); + } + // TODO: Not cleaning these up temporarily. // We have an issue where if requests are inflight, and you navigate away, // those requests could still be trying to act on the store and possibly require services. diff --git a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx index 27d1f76c2bf8b..53ba7996eb641 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx +++ b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx @@ -324,7 +324,6 @@ export class ExpressionInput extends React.Component { minimap: { enabled: false, }, - wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', }} diff --git a/x-pack/plugins/code/kibana.json b/x-pack/plugins/code/kibana.json deleted file mode 100644 index 815bc147d3cfe..0000000000000 --- a/x-pack/plugins/code/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "code", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["xpack", "code"], - "server": true, - "ui": false -} diff --git a/x-pack/plugins/code/server/index.ts b/x-pack/plugins/code/server/index.ts deleted file mode 100644 index ccea83ca1ff9f..0000000000000 --- a/x-pack/plugins/code/server/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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { CodeConfigSchema } from './config'; -import { CodePlugin } from './plugin'; - -export const config = { schema: CodeConfigSchema }; -export const plugin = (initializerContext: PluginInitializerContext) => - new CodePlugin(initializerContext); diff --git a/x-pack/plugins/code/server/plugin.test.ts b/x-pack/plugins/code/server/plugin.test.ts deleted file mode 100644 index 512658ca4da82..0000000000000 --- a/x-pack/plugins/code/server/plugin.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { coreMock } from '../../../../src/core/server/mocks'; - -import { CodePlugin } from './plugin'; - -describe('Code Plugin', () => { - describe('setup()', () => { - it('does not log deprecation message if no xpack.code.* configurations are set', async () => { - const context = coreMock.createPluginInitializerContext(); - const plugin = new CodePlugin(context); - - await plugin.setup(); - - expect(context.logger.get).not.toHaveBeenCalled(); - }); - - it('logs deprecation message if any xpack.code.* configurations are set', async () => { - const context = coreMock.createPluginInitializerContext({ - foo: 'bar', - }); - const warn = jest.fn(); - context.logger.get = jest.fn().mockReturnValue({ warn }); - const plugin = new CodePlugin(context); - - await plugin.setup(); - - expect(context.logger.get).toHaveBeenCalledWith('config', 'deprecation'); - expect(warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The experimental app \\"Code\\" has been removed from Kibana. Remove all xpack.code.* configurations from kibana.yml so Kibana does not fail to start up in the next major version."` - ); - }); - }); -}); diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts deleted file mode 100644 index eb7481d12387d..0000000000000 --- a/x-pack/plugins/code/server/plugin.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext, Plugin } from 'src/core/server'; -import { CodeConfigSchema } from './config'; - -/** - * Represents Code Plugin instance that will be managed by the Kibana plugin system. - */ -export class CodePlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup() { - const config = this.initializerContext.config.get>(); - - if (config && Object.keys(config).length > 0) { - this.initializerContext.logger - .get('config', 'deprecation') - .warn( - 'The experimental app "Code" has been removed from Kibana. Remove all xpack.code.* ' + - 'configurations from kibana.yml so Kibana does not fail to start up in the next major version.' - ); - } - } - - public start() {} - - public stop() {} -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 3655c60bde3bf..211995b2a7d18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -11,3 +11,29 @@ export const RELEVANCE_TUNING_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', { defaultMessage: 'Relevance Tuning' } ); + +export const UPDATE_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.updateSuccess', + { + defaultMessage: 'Relevance successfully tuned. The changes will impact your results shortly.', + } +); +export const DELETE_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteSuccess', + { + defaultMessage: + 'Relevance has been reset to default values. The change will impact your results shortly.', + } +); +export const RESET_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.resetConfirmation', + { + defaultMessage: 'Are you sure you want to restore relevance defaults?', + } +); +export const DELETE_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteConfirmation', + { + defaultMessage: 'Are you sure you want to delete this boost?', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 7f7bce1b7ba95..194848bcfc86c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -5,12 +5,18 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; -import { BoostType } from './types'; +import { nextTick } from '@kbn/test/jest'; + +import { Boost, BoostType } from './types'; import { RelevanceTuningLogic } from './'; +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'test-engine' } }, +})); + describe('RelevanceTuningLogic', () => { const { mount } = new LogicMounter(RelevanceTuningLogic); @@ -32,13 +38,27 @@ describe('RelevanceTuningLogic', () => { schema, schemaConflicts, }; - const searchResults = [{}, {}]; + const searchResults = [ + { + id: { + raw: '1', + }, + _meta: { + id: '1', + score: 100, + engine: 'my-engine', + }, + }, + ]; const DEFAULT_VALUES = { dataLoading: true, schema: {}, schemaConflicts: {}, - searchSettings: {}, + searchSettings: { + boosts: {}, + search_fields: {}, + }, unsavedChanges: false, filterInputValue: '', query: '', @@ -188,6 +208,873 @@ describe('RelevanceTuningLogic', () => { }); }); }); + + describe('setSearchSettingsResponse', () => { + it('should set searchSettings state and unsavedChanges to false', () => { + mount({ + unsavedChanges: true, + }); + RelevanceTuningLogic.actions.setSearchSettingsResponse(searchSettings); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchSettings, + unsavedChanges: false, + }); + }); + }); + }); + + describe('listeners', () => { + const { http } = mockHttpValues; + const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + let scrollToSpy: jest.SpyInstance; + let confirmSpy: jest.SpyInstance; + + const searchSettingsWithBoost = (boost: Boost) => ({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + boost, + ], + }, + }); + + beforeAll(() => { + scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation(() => true); + confirmSpy = jest.spyOn(window, 'confirm'); + }); + + afterAll(() => { + scrollToSpy.mockRestore(); + confirmSpy.mockRestore(); + }); + + describe('initializeRelevanceTuning', () => { + it('should make an API call and set state based on the normalized response', async () => { + mount(); + http.get.mockReturnValueOnce( + Promise.resolve({ + ...relevanceTuningProps, + searchSettings: { + ...relevanceTuningProps.searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + value: 5, + }, + ], + }, + }, + }) + ); + jest.spyOn(RelevanceTuningLogic.actions, 'onInitializeRelevanceTuning'); + + RelevanceTuningLogic.actions.initializeRelevanceTuning(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/details' + ); + expect(RelevanceTuningLogic.actions.onInitializeRelevanceTuning).toHaveBeenCalledWith({ + ...relevanceTuningProps, + searchSettings: { + ...relevanceTuningProps.searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + value: ['5'], + }, + ], + }, + }, + }); + }); + + it('handles errors', async () => { + mount(); + http.get.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.initializeRelevanceTuning(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('getSearchResults', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should make an API call and set state based on the response', async () => { + const searchSettingsWithNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, // This should be deleted before sent to the server + }, + ], + }, + }; + + const searchSettingsWithoutNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }; + + mount({ + query: 'foo', + searchSettings: searchSettingsWithNewBoostProp, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + jest.spyOn(RelevanceTuningLogic.actions, 'setResultsLoading'); + http.post.mockReturnValueOnce( + Promise.resolve({ + results: searchResults, + }) + ); + + RelevanceTuningLogic.actions.getSearchResults(); + jest.runAllTimers(); + await nextTick(); + + expect(RelevanceTuningLogic.actions.setResultsLoading).toHaveBeenCalledWith(true); + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings_search', + { + body: JSON.stringify(searchSettingsWithoutNewBoostProp), + query: { + query: 'foo', + }, + } + ); + expect(RelevanceTuningLogic.actions.setSearchResults).toHaveBeenCalledWith(searchResults); + }); + + it("won't send boosts or search_fields on the API call if there are none", async () => { + mount({ + query: 'foo', + searchSettings: { + searchField: {}, + boosts: {}, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + http.post.mockReturnValueOnce( + Promise.resolve({ + results: searchResults, + }) + ); + + RelevanceTuningLogic.actions.getSearchResults(); + + jest.runAllTimers(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings_search', + { + body: '{}', + query: { + query: 'foo', + }, + } + ); + }); + + it('will call clearSearchResults if there is no query', async () => { + mount({ + query: '', + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + jest.spyOn(RelevanceTuningLogic.actions, 'setResultsLoading'); + jest.spyOn(RelevanceTuningLogic.actions, 'clearSearchResults'); + + RelevanceTuningLogic.actions.getSearchResults(); + jest.runAllTimers(); + await nextTick(); + + expect(RelevanceTuningLogic.actions.clearSearchResults).toHaveBeenCalled(); + expect(RelevanceTuningLogic.actions.setSearchResults).not.toHaveBeenCalled(); + expect(RelevanceTuningLogic.actions.setResultsLoading).not.toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mount({ + query: 'foo', + }); + http.post.mockReturnValueOnce(Promise.reject('error')); + RelevanceTuningLogic.actions.getSearchResults(); + + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('setSearchSettings', () => { + it('updates search results whenever search settings are changed', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.setSearchSettings(searchSettings); + + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + }); + }); + + describe('onSearchSettingsSuccess', () => { + it('should save the response, trigger a new search, and then scroll to the top', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettingsResponse'); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.onSearchSettingsSuccess(searchSettings); + + expect(RelevanceTuningLogic.actions.setSearchSettingsResponse).toHaveBeenCalledWith( + searchSettings + ); + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + expect(scrollToSpy).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('onSearchSettingsError', () => { + it('scrolls to the top', () => { + mount(); + RelevanceTuningLogic.actions.onSearchSettingsError(); + expect(scrollToSpy).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('updateSearchSettings', () => { + it('calls an API endpoint and handles success response', async () => { + const searchSettingsWithNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, // This should be deleted before sent to the server + }, + ], + }, + }; + + const searchSettingsWithoutNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }; + mount({ + searchSettings: searchSettingsWithNewBoostProp, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsSuccess'); + http.put.mockReturnValueOnce(Promise.resolve(searchSettingsWithoutNewBoostProp)); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings', + { + body: JSON.stringify(searchSettingsWithoutNewBoostProp), + } + ); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Relevance successfully tuned. The changes will impact your results shortly.' + ); + expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( + searchSettingsWithoutNewBoostProp + ); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsError'); + http.put.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(RelevanceTuningLogic.actions.onSearchSettingsError).toHaveBeenCalled(); + }); + }); + + describe('resetSearchSettings', () => { + it('calls and API endpoint, shows a success message, and saves the response', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsSuccess'); + confirmSpy.mockImplementation(() => true); + http.post.mockReturnValueOnce(Promise.resolve(searchSettings)); + + RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/reset' + ); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Relevance has been reset to default values. The change will impact your results shortly.' + ); + expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( + searchSettings + ); + }); + + it('does nothing if the user does not confirm', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + + RelevanceTuningLogic.actions.resetSearchSettings(); + + expect(http.post).not.toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/reset' + ); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsError'); + confirmSpy.mockImplementation(() => true); + http.post.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(RelevanceTuningLogic.actions.onSearchSettingsError).toHaveBeenCalled(); + }); + }); + + describe('toggleSearchField', () => { + it('updates search weight to 1 in search fields when enabling', () => { + mount({ + searchSettings: { + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.toggleSearchField('foo', false); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + foo: { + weight: 1, + }, + }, + }); + }); + + it('removes fields from search fields when disabling', () => { + mount({ + searchSettings: { + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.toggleSearchField('bar', true); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + }); + }); + }); + + describe('updateFieldWeight', () => { + it('updates the search weight in search fields', () => { + mount({ + searchSettings, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateFieldWeight('foo', 3); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + foo: { + weight: 3, + }, + }, + }); + }); + + it('will round decimal numbers', () => { + mount({ + searchSettings, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateFieldWeight('foo', 3.9393939); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + foo: { + weight: 3.9, + }, + }, + }); + }); + }); + + describe('addBoost', () => { + it('adds a boost of given type for the given field', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 2, + type: 'value', + }, + ], + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 2, + type: 'value', + }, + { + factor: 1, + newBoost: true, + type: 'functional', + }, + ], + }, + }); + }); + + it('works even if there are no boosts yet', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: {}, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + newBoost: true, + type: 'functional', + }, + ], + }, + }); + }); + }); + + describe('deleteBoost', () => { + it('deletes the boost with the given name and index', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + { + factor: 2, + type: 'value', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => true); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }); + }); + + it('will delete they field key in boosts if this is the last boost or that field', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => true); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 0); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: {}, + }); + }); + + it('will do nothing if the user does not confirm', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => false); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 0); + + expect(RelevanceTuningLogic.actions.setSearchSettings).not.toHaveBeenCalled(); + }); + }); + + describe('updateBoostFactor', () => { + it('updates the boost factor of the target boost', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostFactor('foo', 1, 5); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 5, + type: 'functional', + }) + ); + }); + + it('will round decimal numbers', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostFactor('foo', 1, 5.293191); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 5.3, + type: 'functional', + }) + ); + }); + }); + + describe('updateBoostValue', () => { + it('will update the boost value and update search reuslts', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'b', 'c'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostValue('foo', 1, 1, 'a'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'a', 'c'], + }) + ); + }); + + it('will create a new array if no array exists yet for value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostValue('foo', 1, 0, 'a'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a'], + }) + ); + }); + }); + + describe('updateBoostCenter', () => { + it('will parse the provided provided value and set the center to that parsed value', () => { + mount({ + schema: { + foo: 'number', + }, + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'proximity', + center: 1, + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostCenter('foo', 1, '4'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'proximity', + center: 4, + }) + ); + }); + }); + + describe('addBoostValue', () => { + it('will add an empty boost value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }) + ); + }); + + it('will add two empty boost values if none exist yet', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['', ''], + }) + ); + }); + + it('will still work if the boost index is out of range', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 10); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }) + ); + }); + }); + + describe('removeBoostValue', () => { + it('will remove a boost value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'b', 'c'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.removeBoostValue('foo', 1, 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'c'], + }) + ); + }); + + it('will do nothing if boost values do not exist', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.removeBoostValue('foo', 1, 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).not.toHaveBeenCalled(); + }); + }); + + describe('updateBoostSelectOption', () => { + it('will update the boost', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'function', 'exponential'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + function: 'exponential', + }) + ); + }); + + it('can also update operation', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'operation', 'add'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + operation: 'add', + }) + ); + }); + }); + + describe('updateSearchValue', () => { + it('should update the query then update search results', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchQuery'); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.updateSearchValue('foo'); + + expect(RelevanceTuningLogic.actions.setSearchQuery).toHaveBeenCalledWith('foo'); + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + }); + }); }); describe('selectors', () => { @@ -253,24 +1140,6 @@ describe('RelevanceTuningLogic', () => { }); expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual(['bar', 'baz']); }); - - it('should return all schema fields if there is no filter applied', () => { - mount({ - filterTerm: '', - schema: { - id: 'string', - foo: 'string', - bar: 'string', - baz: 'string', - }, - }); - expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual([ - 'id', - 'foo', - 'bar', - 'baz', - ]); - }); }); describe('filteredSchemaFieldsWithConflicts', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index d4ec5e37f6ce5..cd3d8b5686cc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -6,10 +6,28 @@ */ import { kea, MakeLogicType } from 'kea'; +import { omit, cloneDeep, isEmpty } from 'lodash'; +import { setSuccessMessage, flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { Schema, SchemaConflicts } from '../../../shared/types'; -import { SearchSettings } from './types'; +import { EngineLogic } from '../engine'; +import { Result } from '../result/types'; + +import { + UPDATE_SUCCESS_MESSAGE, + RESET_CONFIRMATION_MESSAGE, + DELETE_SUCCESS_MESSAGE, + DELETE_CONFIRMATION_MESSAGE, +} from './constants'; +import { BaseBoost, Boost, BoostType, SearchSettings } from './types'; +import { + filterIfTerm, + parseBoostCenter, + removeBoostStateProps, + normalizeBoostValues, +} from './utils'; interface RelevanceTuningProps { searchSettings: SearchSettings; @@ -22,15 +40,60 @@ interface RelevanceTuningActions { setSearchSettings(searchSettings: SearchSettings): { searchSettings: SearchSettings }; setFilterValue(value: string): string; setSearchQuery(value: string): string; - setSearchResults(searchResults: object[]): object[]; + setSearchResults(searchResults: Result[]): Result[]; setResultsLoading(resultsLoading: boolean): boolean; clearSearchResults(): void; resetSearchSettingsState(): void; dismissSchemaConflictCallout(): void; + initializeRelevanceTuning(): void; + getSearchResults(): void; + setSearchSettingsResponse(searchSettings: SearchSettings): { searchSettings: SearchSettings }; + onSearchSettingsSuccess(searchSettings: SearchSettings): { searchSettings: SearchSettings }; + onSearchSettingsError(): void; + updateSearchSettings(): void; + resetSearchSettings(): void; + toggleSearchField(name: string, disableField: boolean): { name: string; disableField: boolean }; + updateFieldWeight(name: string, weight: number): { name: string; weight: number }; + addBoost(name: string, type: BoostType): { name: string; type: BoostType }; + deleteBoost(name: string, index: number): { name: string; index: number }; + updateBoostFactor( + name: string, + index: number, + factor: number + ): { name: string; index: number; factor: number }; + updateBoostValue( + name: string, + boostIndex: number, + valueIndex: number, + value: string + ): { name: string; boostIndex: number; valueIndex: number; value: string }; + updateBoostCenter( + name: string, + boostIndex: number, + value: string | number + ): { name: string; boostIndex: number; value: string | number }; + addBoostValue(name: string, boostIndex: number): { name: string; boostIndex: number }; + removeBoostValue( + name: string, + boostIndex: number, + valueIndex: number + ): { name: string; boostIndex: number; valueIndex: number }; + updateBoostSelectOption( + name: string, + boostIndex: number, + optionType: keyof BaseBoost, + value: string + ): { + name: string; + boostIndex: number; + optionType: keyof BaseBoost; + value: string; + }; + updateSearchValue(query: string): string; } interface RelevanceTuningValues { - searchSettings: Partial; + searchSettings: SearchSettings; schema: Schema; schemaFields: string[]; schemaFieldsWithConflicts: string[]; @@ -43,15 +106,10 @@ interface RelevanceTuningValues { query: string; unsavedChanges: boolean; dataLoading: boolean; - searchResults: object[] | null; + searchResults: Result[] | null; resultsLoading: boolean; } -// If the user hasn't entered a filter, then we can skip filtering the array entirely -const filterIfTerm = (array: string[], filterTerm: string): string[] => { - return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm)); -}; - export const RelevanceTuningLogic = kea< MakeLogicType >({ @@ -66,13 +124,47 @@ export const RelevanceTuningLogic = kea< clearSearchResults: true, resetSearchSettingsState: true, dismissSchemaConflictCallout: true, + initializeRelevanceTuning: true, + getSearchResults: true, + setSearchSettingsResponse: (searchSettings) => ({ + searchSettings, + }), + onSearchSettingsSuccess: (searchSettings) => ({ searchSettings }), + onSearchSettingsError: () => true, + updateSearchSettings: true, + resetSearchSettings: true, + toggleSearchField: (name, disableField) => ({ name, disableField }), + updateFieldWeight: (name, weight) => ({ name, weight }), + addBoost: (name, type) => ({ name, type }), + deleteBoost: (name, index) => ({ name, index }), + updateBoostFactor: (name, index, factor) => ({ name, index, factor }), + updateBoostValue: (name, boostIndex, valueIndex, value) => ({ + name, + boostIndex, + valueIndex, + value, + }), + updateBoostCenter: (name, boostIndex, value) => ({ name, boostIndex, value }), + addBoostValue: (name, boostIndex) => ({ name, boostIndex }), + removeBoostValue: (name, boostIndex, valueIndex) => ({ name, boostIndex, valueIndex }), + updateBoostSelectOption: (name, boostIndex, optionType, value) => ({ + name, + boostIndex, + optionType, + value, + }), + updateSearchValue: (query) => query, }), reducers: () => ({ searchSettings: [ - {}, + { + search_fields: {}, + boosts: {}, + }, { onInitializeRelevanceTuning: (_, { searchSettings }) => searchSettings, setSearchSettings: (_, { searchSettings }) => searchSettings, + setSearchSettingsResponse: (_, { searchSettings }) => searchSettings, }, ], schema: [ @@ -109,6 +201,7 @@ export const RelevanceTuningLogic = kea< false, { setSearchSettings: () => true, + setSearchSettingsResponse: () => false, }, ], @@ -155,4 +248,268 @@ export const RelevanceTuningLogic = kea< (schema: Schema): boolean => Object.keys(schema).length >= 2, ], }), + listeners: ({ actions, values }) => ({ + initializeRelevanceTuning: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings/details`; + + try { + const response = await http.get(url); + actions.onInitializeRelevanceTuning({ + ...response, + searchSettings: { + ...response.searchSettings, + boosts: normalizeBoostValues(response.searchSettings.boosts), + }, + }); + } catch (e) { + flashAPIErrors(e); + } + }, + getSearchResults: async (_, breakpoint) => { + await breakpoint(250); + + const query = values.query; + if (!query) return actions.clearSearchResults(); + + const { engineName } = EngineLogic.values; + const { http } = HttpLogic.values; + const { search_fields: searchFields, boosts } = removeBoostStateProps(values.searchSettings); + const url = `/api/app_search/engines/${engineName}/search_settings_search`; + + actions.setResultsLoading(true); + + try { + const response = await http.post(url, { + query: { + query, + }, + body: JSON.stringify({ + boosts: isEmpty(boosts) ? undefined : boosts, + search_fields: isEmpty(searchFields) ? undefined : searchFields, + }), + }); + + actions.setSearchResults(response.results); + } catch (e) { + flashAPIErrors(e); + } + }, + setSearchSettings: () => { + actions.getSearchResults(); + }, + onSearchSettingsSuccess: ({ searchSettings }) => { + actions.setSearchSettingsResponse(searchSettings); + actions.getSearchResults(); + window.scrollTo(0, 0); + }, + onSearchSettingsError: () => { + window.scrollTo(0, 0); + }, + updateSearchSettings: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings`; + + try { + const response = await http.put(url, { + body: JSON.stringify(removeBoostStateProps(values.searchSettings)), + }); + setSuccessMessage(UPDATE_SUCCESS_MESSAGE); + actions.onSearchSettingsSuccess(response); + } catch (e) { + flashAPIErrors(e); + actions.onSearchSettingsError(); + } + }, + resetSearchSettings: async () => { + if (window.confirm(RESET_CONFIRMATION_MESSAGE)) { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings/reset`; + + try { + const response = await http.post(url); + setSuccessMessage(DELETE_SUCCESS_MESSAGE); + actions.onSearchSettingsSuccess(response); + } catch (e) { + flashAPIErrors(e); + actions.onSearchSettingsError(); + } + } + }, + toggleSearchField: ({ name, disableField }) => { + const { searchSettings } = values; + + const searchFields = disableField + ? omit(searchSettings.search_fields, name) + : { ...searchSettings.search_fields, [name]: { weight: 1 } }; + + actions.setSearchSettings({ + ...searchSettings, + search_fields: searchFields, + }); + }, + updateFieldWeight: ({ name, weight }) => { + const { searchSettings } = values; + const { search_fields: searchFields } = searchSettings; + + actions.setSearchSettings({ + ...searchSettings, + search_fields: { + ...searchFields, + [name]: { + ...searchFields[name], + weight: Math.round(weight * 10) / 10, + }, + }, + }); + }, + addBoost: ({ name, type }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const emptyBoost = { type, factor: 1, newBoost: true }; + let boostArray; + + if (Array.isArray(boosts[name])) { + boostArray = boosts[name].slice(); + boostArray.push(emptyBoost); + } else { + boostArray = [emptyBoost]; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: boostArray, + }, + }); + }, + deleteBoost: ({ name, index }) => { + if (window.confirm(DELETE_CONFIRMATION_MESSAGE)) { + const { searchSettings } = values; + const { boosts } = searchSettings; + const boostsRemoved = boosts[name].slice(); + boostsRemoved.splice(index, 1); + const updatedBoosts = { ...boosts }; + + if (boostsRemoved.length > 0) { + updatedBoosts[name] = boostsRemoved; + } else { + delete updatedBoosts[name]; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: updatedBoosts, + }); + } + }, + updateBoostFactor: ({ name, index, factor }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + updatedBoosts[index].factor = Math.round(factor * 10) / 10; + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostValue: ({ name, boostIndex, valueIndex, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts: Boost[] = cloneDeep(boosts[name]); + const existingValue = updatedBoosts[boostIndex].value; + if (existingValue === undefined) { + updatedBoosts[boostIndex].value = [value]; + } else { + existingValue[valueIndex] = value; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostCenter: ({ name, boostIndex, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const fieldType = values.schema[name]; + updatedBoosts[boostIndex].center = parseBoostCenter(fieldType, value); + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + addBoostValue: ({ name, boostIndex }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const updatedBoost = updatedBoosts[boostIndex]; + if (updatedBoost) { + updatedBoost.value = Array.isArray(updatedBoost.value) ? updatedBoost.value : ['']; + updatedBoost.value.push(''); + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + removeBoostValue: ({ name, boostIndex, valueIndex }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const boostValue = updatedBoosts[boostIndex].value; + + if (boostValue === undefined) return; + + boostValue.splice(valueIndex, 1); + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostSelectOption: ({ name, boostIndex, optionType, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + updatedBoosts[boostIndex][optionType] = value; + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateSearchValue: (query) => { + actions.setSearchQuery(query); + actions.getSearchResults(); + }, + }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index 25187df89d64c..a1ed9797b9f5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -7,17 +7,31 @@ export type BoostType = 'value' | 'functional' | 'proximity'; -export interface Boost { - type: BoostType; +export interface BaseBoost { operation?: string; function?: string; +} + +// A boost that comes from the server, before we normalize it has a much looser schema +export interface RawBoost extends BaseBoost { + type: BoostType; newBoost?: boolean; center?: string | number; - value?: string | number | string[] | number[]; + value?: string | number | boolean | object | Array; factor: number; } +// We normalize raw boosts to make them safer and easier to work with +export interface Boost extends RawBoost { + value?: string[]; +} export interface SearchSettings { boosts: Record; - search_fields: object; + search_fields: Record< + string, + { + weight: number; + } + >; + result_fields?: object; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts new file mode 100644 index 0000000000000..a6598bf991c13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { BoostType } from './types'; +import { + filterIfTerm, + normalizeBoostValues, + removeBoostStateProps, + parseBoostCenter, +} from './utils'; + +describe('filterIfTerm', () => { + it('will filter a list of strings to a list of strings containing the specified string', () => { + expect(filterIfTerm(['jalepeno', 'no', 'not', 'panorama', 'truck'], 'no')).toEqual([ + 'jalepeno', + 'no', + 'not', + 'panorama', + ]); + }); + + it('will not filter at all if an empty string is provided', () => { + expect(filterIfTerm(['jalepeno', 'no', 'not', 'panorama', 'truck'], '')).toEqual([ + 'jalepeno', + 'no', + 'not', + 'panorama', + 'truck', + ]); + }); +}); + +describe('removeBoostStateProps', () => { + it('will remove the newBoost flag from boosts within the provided searchSettings object', () => { + const searchSettings = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, + }, + ], + }, + search_fields: { + foo: { + weight: 1, + }, + }, + }; + expect(removeBoostStateProps(searchSettings)).toEqual({ + ...searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }); + }); +}); + +describe('parseBoostCenter', () => { + it('should parse a boost center', () => { + expect(parseBoostCenter('text', 5)).toEqual(5); + expect(parseBoostCenter('text', '4')).toEqual('4'); + expect(parseBoostCenter('number', 5)).toEqual(5); + expect(parseBoostCenter('number', '5')).toEqual(5); + }); +}); + +describe('normalizeBoostValues', () => { + const boosts = { + foo: [ + { + type: 'value' as BoostType, + factor: 9.5, + value: 1, + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: '1', + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: [1], + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: ['1'], + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: [ + '1', + 1, + '2', + 2, + true, + { + b: 'a', + }, + {}, + ], + }, + ], + bar: [ + { + type: 'proximity' as BoostType, + factor: 9.5, + }, + ], + sp_def: [ + { + type: 'functional' as BoostType, + factor: 5, + }, + ], + }; + + it('converts all value types to string for consistency', () => { + expect(normalizeBoostValues(boosts)).toEqual({ + bar: [{ factor: 9.5, type: 'proximity' }], + foo: [ + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { + factor: 9.5, + type: 'value', + value: ['1', '1', '2', '2', 'true', '[object Object]', '[object Object]'], + }, + ], + sp_def: [{ type: 'functional', factor: 5 }], + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts new file mode 100644 index 0000000000000..e2fd0f0bbd656 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep, omit } from 'lodash'; + +import { NUMBER } from '../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../shared/types'; + +import { RawBoost, Boost, SearchSettings, BoostType } from './types'; + +// If the user hasn't entered a filter, then we can skip filtering the array entirely +export const filterIfTerm = (array: string[], filterTerm: string): string[] => { + return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm)); +}; + +export const removeBoostStateProps = (searchSettings: SearchSettings) => { + const updatedSettings = cloneDeep(searchSettings); + const { boosts } = updatedSettings; + const keys = Object.keys(boosts); + keys.forEach((key) => boosts[key].forEach((boost) => delete boost.newBoost)); + + return updatedSettings; +}; + +export const parseBoostCenter = (fieldType: SchemaTypes, value: string | number) => { + // Leave non-numeric fields alone + if (fieldType === NUMBER) { + const floatValue = parseFloat(value as string); + return isNaN(floatValue) ? value : floatValue; + } + return value; +}; + +const toArray = (v: T | T[]): T[] => (Array.isArray(v) ? v : [v]); +const toString = (v1: T) => String(v1); + +const normalizeBoostValue = (boost: RawBoost): Boost => { + if (!boost.hasOwnProperty('value')) { + // Can't simply do `return boost` here as TS can't infer the correct type + return omit(boost, 'value'); + } + + return { + ...boost, + type: boost.type as BoostType, + value: toArray(boost.value).map(toString), + }; +}; + +// Data may have been set to invalid types directly via the public App Search API. Since these are invalid, we don't want to deal +// with them as valid types in the UI. For that reason, we stringify all values here, as the data comes in. +// Additionally, values can be in single values or in arrays. +export const normalizeBoostValues = (boosts: Record): Record => + Object.entries(boosts).reduce((newBoosts, [fieldName, boostList]) => { + return { + ...newBoosts, + [fieldName]: boostList.map(normalizeBoostValue), + }; + }, {}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx index 1b580a528fe03..d12311cf16d3d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx @@ -37,7 +37,7 @@ export interface HeaderProps { leftColumn?: JSX.Element; rightColumn?: JSX.Element; rightColumnGrow?: EuiFlexItemProps['grow']; - tabs?: EuiTabProps[]; + tabs?: Array & { name?: JSX.Element | string }>; tabsClassName?: string; 'data-test-subj'?: string; } @@ -73,7 +73,7 @@ export const Header: React.FC = ({ {tabs.map((props) => ( - + {props.name} ))} diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx index 069b34e13c9a4..88b6933fe4774 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx @@ -43,7 +43,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } ), availableAsIntegrationLink: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index da0e402700539..bcb450d5ec94e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -18,7 +18,10 @@ export type StaticPage = | 'data_streams'; export type DynamicPage = - | 'integration_details' + | 'integration_details_overview' + | 'integration_details_policies' + | 'integration_details_settings' + | 'integration_details_custom' | 'integration_policy_edit' | 'policy_details' | 'add_integration_from_policy' @@ -43,6 +46,10 @@ export const PAGE_ROUTING_PATHS = { integrations_all: '/integrations', integrations_installed: '/integrations/installed', integration_details: '/integrations/detail/:pkgkey/:panel?', + integration_details_overview: '/integrations/detail/:pkgkey/overview', + integration_details_policies: '/integrations/detail/:pkgkey/policies', + integration_details_settings: '/integrations/detail/:pkgkey/settings', + integration_details_custom: '/integrations/detail/:pkgkey/custom', integration_policy_edit: '/integrations/edit-integration/:packagePolicyId', policies: '/policies', policies_list: '/policies', @@ -70,8 +77,10 @@ export const pagePathGetters: { integrations: () => '/integrations', integrations_all: () => '/integrations', integrations_installed: () => '/integrations/installed', - integration_details: ({ pkgkey, panel }) => - `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + integration_details_overview: ({ pkgkey }) => `/integrations/detail/${pkgkey}/overview`, + integration_details_policies: ({ pkgkey }) => `/integrations/detail/${pkgkey}/policies`, + integration_details_settings: ({ pkgkey }) => `/integrations/detail/${pkgkey}/settings`, + integration_details_custom: ({ pkgkey }) => `/integrations/detail/${pkgkey}/custom`, integration_policy_edit: ({ packagePolicyId }) => `/integrations/edit-integration/${packagePolicyId}`, policies: () => '/policies', diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index aa869cd076e7d..22dfe2e8be517 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -18,7 +18,7 @@ const BASE_BREADCRUMB: ChromeBreadcrumb = { }; const breadcrumbGetters: { - [key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; + [key in Page]?: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; } = { base: () => [BASE_BREADCRUMB], overview: () => [ @@ -65,7 +65,7 @@ const breadcrumbGetters: { }), }, ], - integration_details: ({ pkgTitle }) => [ + integration_details_overview: ({ pkgTitle }) => [ BASE_BREADCRUMB, { href: pagePathGetters.integrations(), @@ -84,7 +84,7 @@ const breadcrumbGetters: { }), }, { - href: pagePathGetters.integration_details({ pkgkey, panel: 'policies' }), + href: pagePathGetters.integration_details_policies({ pkgkey }), text: pkgTitle, }, { text: policyName }, @@ -142,7 +142,7 @@ const breadcrumbGetters: { }), }, { - href: pagePathGetters.integration_details({ pkgkey }), + href: pagePathGetters.integration_details_overview({ pkgkey }), text: pkgTitle, }, { @@ -221,10 +221,11 @@ const breadcrumbGetters: { export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { const { chrome, http } = useStartServices(); - const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map((breadcrumb) => ({ - ...breadcrumb, - href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, - })); + const breadcrumbs: ChromeBreadcrumb[] = + breadcrumbGetters[page]?.(values).map((breadcrumb) => ({ + ...breadcrumb, + href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, + })) || []; const docTitle: string[] = [...breadcrumbs] .reverse() .map((breadcrumb) => breadcrumb.text as string); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index a8a11c583535f..a076012100421 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -217,7 +217,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { } return from === 'policy' ? getHref('policy_details', { policyId: agentPolicyId || policyId }) - : getHref('integration_details', { pkgkey }); + : getHref('integration_details_overview', { pkgkey }); }, [agentPolicyId, policyId, from, getHref, pkgkey, routeState]); const cancelClickHandler: ReactEventHandler = useCallback( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 94133ecdef33a..b0f2232cf5067 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -246,9 +246,8 @@ export const EditPackagePolicyForm = memo<{ const cancelUrl = useMemo((): string => { if (packageInfo && policyId) { return from === 'package-edit' - ? getHref('integration_details', { + ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), - panel: 'policies', }) : getHref('policy_details', { policyId }); } @@ -258,9 +257,8 @@ export const EditPackagePolicyForm = memo<{ const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { return from === 'package-edit' - ? getPath('integration_details', { + ? getPath('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), - panel: 'policies', }) : getPath('policy_details', { policyId }); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx index 9c1a2d2fad998..cac8ff7d5e7a2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx @@ -43,7 +43,7 @@ export function PackageCard({ title={title || ''} description={description} icon={} - href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} + href={getHref('integration_details_overview', { pkgkey: `${name}-${urlVersion}` })} betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined} betaBadgeTooltipContent={ release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx index e5e7f9f81fd1e..d3fccb6001733 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx @@ -90,9 +90,8 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar } else { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); if (fromUpdate) { - const settingsPath = getPath('integration_details', { + const settingsPath = getPath('integration_details_settings', { pkgkey: `${name}-${version}`, - panel: 'settings', }); history.push(settingsPath); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx new file mode 100644 index 0000000000000..b2495b607af59 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EuiIcon, EuiPanel } from '@elastic/eui'; +import { usePackageIconType, UsePackageIconType } from '../../../../../hooks'; +import { Loading } from '../../../../../components'; + +const PanelWrapper = styled.div` + // NOTE: changes to the width here will impact navigation tabs page layout under integration package details + width: ${(props) => + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; + height: 1px; + z-index: 1; +`; + +const Panel = styled(EuiPanel)` + padding: ${(props) => props.theme.eui.spacerSizes.xl}; + margin-bottom: -100%; + svg, + img { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + } + .euiFlexItem { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + justify-content: center; + } +`; + +export function IconPanel({ + packageName, + version, + icons, +}: Pick) { + const iconType = usePackageIconType({ packageName, version, icons }); + + return ( + + + + + + ); +} + +export function LoadingIconPanel() { + return ( + + + + + + ); +} diff --git a/x-pack/plugins/code/server/config.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx similarity index 56% rename from x-pack/plugins/code/server/config.ts rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx index 2cc3e78c0b962..8424fecad08cd 100644 --- a/x-pack/plugins/code/server/config.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx @@ -4,11 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { schema } from '@kbn/config-schema'; - -const createCodeConfigSchema = () => { - return schema.any({ defaultValue: {} }); -}; - -export const CodeConfigSchema = createCodeConfigSchema(); +export { UpdateIcon } from './update_icon'; +export { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; +export { IconPanel, LoadingIconPanel } from './icon_panel'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx similarity index 90% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx index 27d0a19aba5c9..eeb74526046e2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useGetPackageStats } from '../../../../hooks'; +import { useGetPackageStats } from '../../../../../hooks'; /** * Displays a count of Agent Policies that are using the given integration diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx new file mode 100644 index 0000000000000..d7ad6667b6db0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIconTip, EuiIconProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => ( + +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx deleted file mode 100644 index e6c5a12789497..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ /dev/null @@ -1,114 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; -import styled from 'styled-components'; -import { Redirect } from 'react-router-dom'; -import { DetailParams } from '.'; -import { DetailViewPanelName, PackageInfo } from '../../../../types'; -import { AssetsFacetGroup } from '../../components/assets_facet_group'; -import { CenterColumn, LeftColumn, RightColumn } from './layout'; -import { OverviewPanel } from './overview_panel'; -import { PackagePoliciesPanel } from './package_policies_panel'; -import { SettingsPanel } from './settings_panel'; -import { useUIExtension } from '../../../../hooks/use_ui_extension'; -import { ExtensionWrapper } from '../../../../components/extension_wrapper'; -import { useLink } from '../../../../hooks'; -import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; - -type ContentProps = PackageInfo & Pick; - -const LeftSideColumn = styled(LeftColumn)` - /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ - &&& { - margin-top: 77px; - } -`; - -// fixes IE11 problem with nested flex items -const ContentFlexGroup = styled(EuiFlexGroup)` - flex: 0 0 auto !important; -`; - -export function Content(props: ContentProps) { - const { panel } = props; - const showRightColumn = useMemo(() => { - const fullWidthContentPages: DetailViewPanelName[] = ['policies', 'custom']; - return !fullWidthContentPages.includes(panel!); - }, [panel]); - - return ( - - - - - - {showRightColumn && ( - - - - )} - - ); -} - -interface ContentPanelProps { - packageInfo: PackageInfo; - panel: DetailViewPanelName; -} -export const ContentPanel = memo(({ panel, packageInfo }) => { - const { name, version, assets, title, removable, latestVersion } = packageInfo; - const pkgkey = pkgKeyFromPackageInfo(packageInfo); - - const CustomView = useUIExtension(name, 'package-detail-custom'); - const { getPath } = useLink(); - - switch (panel) { - case 'settings': - return ( - - ); - case 'policies': - return ; - case 'custom': - return CustomView ? ( - - - - ) : ( - - ); - case 'overview': - default: - return ; - } -}); - -type RightColumnContentProps = PackageInfo & Pick; -function RightColumnContent(props: RightColumnContentProps) { - const { assets, panel } = props; - switch (panel) { - case 'overview': - return assets ? ( - - - - - - ) : null; - default: - return ; - } -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx deleted file mode 100644 index c6b578dd5364f..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx +++ /dev/null @@ -1,98 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiButtonEmpty, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { Fragment, useCallback, useLayoutEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; - -const BottomFade = styled.div` - width: 100%; - background: ${(props) => - `linear-gradient(${props.theme.eui.euiColorEmptyShade}00 0%, ${props.theme.eui.euiColorEmptyShade} 100%)`}; - margin-top: -${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px; - height: ${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px; - position: absolute; -`; -const ContentCollapseContainer = styled.div` - position: relative; -`; -const CollapseButtonContainer = styled.div` - display: inline-block; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - position: absolute; - left: 50%; - transform: translateX(-50%); - top: ${(props) => parseInt(props.theme.eui.euiButtonHeight, 10) / 2}px; -`; -const CollapseButtonTop = styled(EuiButtonEmpty)` - float: right; -`; - -const CollapseButton = ({ - open, - toggleCollapse, -}: { - open: boolean; - toggleCollapse: () => void; -}) => { - return ( -
- - - - - {open ? 'Collapse' : 'Read more'} - - -
- ); -}; - -export const ContentCollapse = ({ children }: { children: React.ReactNode }) => { - const [open, setOpen] = useState(false); - const [height, setHeight] = useState('auto'); - const [collapsible, setCollapsible] = useState(true); - const contentEl = useRef(null); - const collapsedHeight = 360; - - // if content is too small, don't collapse - useLayoutEffect( - () => - contentEl.current && contentEl.current.clientHeight < collapsedHeight - ? setCollapsible(false) - : setHeight(collapsedHeight), - [] - ); - - const clickOpen = useCallback(() => { - setOpen(!open); - }, [open]); - - return ( - - {collapsible ? ( - -
- {open && ( - - Collapse - - )} - {children} -
- {!open && } - -
- ) : ( -
{children}
- )} -
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx new file mode 100644 index 0000000000000..f005c1e24dee7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { memo, useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUIExtension } from '../../../../../hooks/use_ui_extension'; +import { useLink } from '../../../../../hooks'; +import { PackageInfo } from '../../../../../types'; +import { pkgKeyFromPackageInfo } from '../../../../../services/pkg_key_from_package_info'; +import { ExtensionWrapper } from '../../../../../components/extension_wrapper'; + +interface Props { + packageInfo: PackageInfo; +} + +export const CustomViewPage: React.FC = memo(({ packageInfo }) => { + const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom'); + const { getPath } = useLink(); + const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]); + + return CustomView ? ( + + + + + + + + + ) : ( + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts new file mode 100644 index 0000000000000..bc480b1daa355 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { CustomViewPage } from './custom'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index b60d3b5eb1f2d..32e39d7c4d6ee 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -28,7 +28,7 @@ import { act, cleanup } from '@testing-library/react'; describe('when on integration detail', () => { const pkgkey = 'nginx-0.3.7'; - const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey }); + const detailPageUrlPath = pagePathGetters.integration_details_overview({ pkgkey }); let testRenderer: TestRenderer; let renderResult: ReturnType; let mockedApi: MockedApi; @@ -100,7 +100,7 @@ describe('when on integration detail', () => { it('should redirect if custom url is accessed', () => { act(() => { testRenderer.history.push( - pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' }) ); }); expect(testRenderer.history.location.pathname).toEqual(detailPageUrlPath); @@ -148,7 +148,7 @@ describe('when on integration detail', () => { it('should display custom content when tab is clicked', async () => { act(() => { testRenderer.history.push( - pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' }) ); }); await lazyComponentWasRendered; @@ -173,14 +173,14 @@ describe('when on integration detail', () => { onCancelNavigateTo: [ 'fleet', { - path: '#/integrations/detail/nginx-0.3.7', + path: '#/integrations/detail/nginx-0.3.7/overview', }, ], - onCancelUrl: '#/integrations/detail/nginx-0.3.7', + onCancelUrl: '#/integrations/detail/nginx-0.3.7/overview', onSaveNavigateTo: [ 'fleet', { - path: '#/integrations/detail/nginx-0.3.7', + path: '#/integrations/detail/nginx-0.3.7/overview', }, ], }); @@ -188,7 +188,7 @@ describe('when on integration detail', () => { }); describe('and on the Policies Tab', () => { - const policiesTabURLPath = pagePathGetters.integration_details({ pkgkey, panel: 'policies' }); + const policiesTabURLPath = pagePathGetters.integration_details_policies({ pkgkey }); beforeEach(() => { testRenderer.history.push(policiesTabURLPath); render(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index fabcd2ab917a4..3cb57b63e707d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -4,72 +4,50 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useEffect, useState, useMemo, useCallback, ReactEventHandler } from 'react'; -import { useHistory, useLocation, useParams } from 'react-router-dom'; +import React, { ReactEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; +import { Redirect, Route, Switch, useHistory, useLocation, useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiText, - EuiSpacer, EuiBetaBadge, EuiButton, + EuiButtonEmpty, EuiDescriptionList, - EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useUIExtension } from '../../../../hooks/use_ui_extension'; +import { PAGE_ROUTING_PATHS, PLUGIN_ID } from '../../../../constants'; +import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks'; +import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; import { CreatePackagePolicyRouteState, DetailViewPanelName, - entries, InstallStatus, PackageInfo, } from '../../../../types'; -import { Loading, Error } from '../../../../components'; -import { - useGetPackageInfoByKey, - useBreadcrumbs, - useLink, - useCapabilities, -} from '../../../../hooks'; +import { Error, Loading } from '../../../../components'; +import { useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout, WithHeaderLayoutProps } from '../../../../layouts'; +import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge'; import { useSetPackageInstallStatus } from '../../hooks'; -import { IconPanel, LoadingIconPanel } from '../../components/icon_panel'; -import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge'; -import { UpdateIcon } from '../../components/icons'; -import { Content } from './content'; +import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components'; +import { OverviewPage } from './overview'; +import { PackagePoliciesPage } from './policies'; +import { SettingsPage } from './settings'; +import { CustomViewPage } from './custom'; import './index.scss'; -import { useUIExtension } from '../../../../hooks/use_ui_extension'; -import { PLUGIN_ID } from '../../../../../../../common/constants'; -import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; -import { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; - -export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; export interface DetailParams { pkgkey: string; panel?: DetailViewPanelName; } -const PanelDisplayNames: Record = { - overview: i18n.translate('xpack.fleet.epm.packageDetailsNav.overviewLinkText', { - defaultMessage: 'Overview', - }), - policies: i18n.translate('xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText', { - defaultMessage: 'Policies', - }), - settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', { - defaultMessage: 'Settings', - }), - custom: i18n.translate('xpack.fleet.epm.packageDetailsNav.packageCustomLinkText', { - defaultMessage: 'Advanced', - }), -}; - const Divider = styled.div` width: 0; height: 100%; @@ -82,12 +60,12 @@ const FlexItemWithMinWidth = styled(EuiFlexItem)` `; function Breadcrumbs({ packageTitle }: { packageTitle: string }) { - useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); + useBreadcrumbs('integration_details_overview', { pkgTitle: packageTitle }); return null; } export function Detail() { - const { pkgkey, panel = DEFAULT_PANEL } = useParams(); + const { pkgkey, panel } = useParams(); const { getHref, getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const history = useHistory(); @@ -247,7 +225,7 @@ export function Detail() { { isDivider: true }, { label: i18n.translate('xpack.fleet.epm.usedByLabel', { - defaultMessage: 'Agent Policies', + defaultMessage: 'Agent policies', }), 'data-test-subj': 'agentPolicyCount', content: , @@ -306,37 +284,79 @@ export function Detail() { ] ); - const tabs = useMemo(() => { + const headerTabs = useMemo(() => { if (!packageInfo) { return []; } + const packageInfoKey = pkgKeyFromPackageInfo(packageInfo); - return (entries(PanelDisplayNames) - .filter(([panelId]) => { - // Don't show `Policies` tab if package is not installed - if (panelId === 'policies' && packageInstallStatus !== InstallStatus.installed) { - return false; - } + const tabs: WithHeaderLayoutProps['tabs'] = [ + { + id: 'overview', + name: ( + + ), + isSelected: panel === 'overview', + 'data-test-subj': `tab-overview`, + href: getHref('integration_details_overview', { + pkgkey: packageInfoKey, + }), + }, + ]; - // Don't show `custom` tab if a custom component is not registered - if (panelId === 'custom' && !showCustomTab) { - return false; - } + if (packageInstallStatus === InstallStatus.installed) { + tabs.push({ + id: 'policies', + name: ( + + ), + isSelected: panel === 'policies', + 'data-test-subj': `tab-policies`, + href: getHref('integration_details_policies', { + pkgkey: packageInfoKey, + }), + }); + } + + tabs.push({ + id: 'settings', + name: ( + + ), + isSelected: panel === 'settings', + 'data-test-subj': `tab-settings`, + href: getHref('integration_details_settings', { + pkgkey: packageInfoKey, + }), + }); + + if (showCustomTab) { + tabs.push({ + id: 'custom', + name: ( + + ), + isSelected: panel === 'custom', + 'data-test-subj': `tab-custom`, + href: getHref('integration_details_custom', { + pkgkey: packageInfoKey, + }), + }); + } - return true; - }) - .map(([panelId, display]) => { - return { - id: panelId, - name: display, - isSelected: panelId === panel, - 'data-test-subj': `tab-${panelId}`, - href: getHref('integration_details', { - pkgkey: pkgKeyFromPackageInfo(packageInfo || {}), - panel: panelId, - }), - }; - }) as unknown) as WithHeaderLayoutProps['tabs']; + return tabs; }, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]); return ( @@ -344,7 +364,7 @@ export function Detail() { leftColumn={headerLeftContent} rightColumn={headerRightContent} rightColumnGrow={false} - tabs={tabs} + tabs={headerTabs} tabsClassName="fleet__epm__shiftNavTabs" > {packageInfo ? : null} @@ -361,7 +381,21 @@ export function Detail() { ) : isLoading || !packageInfo ? ( ) : ( - + + + + + + + + + + + + + + + )} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx deleted file mode 100644 index f751a4c057b71..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexItem } from '@elastic/eui'; -import React, { FunctionComponent, ReactNode } from 'react'; -import { FlexItemGrowSize } from '@elastic/eui/src/components/flex/flex_item'; - -interface ColumnProps { - children?: ReactNode; - className?: string; - columnGrow?: FlexItemGrowSize; -} - -export const LeftColumn: FunctionComponent = ({ - columnGrow = 2, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; - -export const CenterColumn: FunctionComponent = ({ - columnGrow = 9, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; - -export const RightColumn: FunctionComponent = ({ - columnGrow = 3, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx new file mode 100644 index 0000000000000..3b9daaeb0216e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx @@ -0,0 +1,159 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTextColor, + EuiDescriptionList, + EuiNotificationBadge, +} from '@elastic/eui'; +import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; +import { + PackageInfo, + PackageSpecCategory, + AssetTypeToParts, + KibanaAssetType, + entries, +} from '../../../../../types'; +import { useGetCategories } from '../../../../../hooks'; +import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants'; + +interface Props { + packageInfo: PackageInfo; +} + +export const Details: React.FC = memo(({ packageInfo }) => { + const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories(); + const packageCategories: string[] = useMemo(() => { + if (!isLoadingCategories && categoriesData && categoriesData.response) { + return categoriesData.response + .filter((category) => packageInfo.categories?.includes(category.id as PackageSpecCategory)) + .map((category) => category.title); + } + return []; + }, [categoriesData, isLoadingCategories, packageInfo.categories]); + + const listItems = useMemo(() => { + // Base details: version and categories + const items: EuiDescriptionListProps['listItems'] = [ + { + title: ( + + + + ), + description: packageInfo.version, + }, + { + title: ( + + + + ), + description: packageCategories.join(', '), + }, + ]; + + // Asset details and counts + entries(packageInfo.assets).forEach(([service, typeToParts]) => { + // Filter out assets we are not going to display + // (currently we only display Kibana and Elasticsearch assets) + const filteredTypes: AssetTypeToParts = entries(typeToParts).reduce( + (acc: any, [asset, value]) => { + if (DisplayedAssets[service].includes(asset)) acc[asset] = value; + return acc; + }, + {} + ); + if (Object.entries(filteredTypes).length) { + items.push({ + title: ( + + + + ), + description: ( + + {entries(filteredTypes).map(([_type, parts]) => { + const type = _type as KibanaAssetType; + return ( + + + {AssetTitleMap[type]} + + {parts.length} + + + + ); + })} + + ), + }); + } + }); + + // Feature (data stream type) details + const dataStreamTypes = [ + ...new Set(packageInfo.data_streams?.map((dataStream) => dataStream.type) || []), + ]; + if (dataStreamTypes.length) { + items.push({ + title: ( + + + + ), + description: dataStreamTypes.join(', '), + }); + } + + // License details + if (packageInfo.license) { + items.push({ + title: ( + + + + ), + description: packageInfo.license, + }); + } + + return items; + }, [ + packageCategories, + packageInfo.assets, + packageInfo.data_streams, + packageInfo.license, + packageInfo.version, + ]); + + return ( + + + +

+ +

+
+
+ + + +
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts new file mode 100644 index 0000000000000..70a5453aeb42f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { OverviewPage } from './overview'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/markdown_renderers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/markdown_renderers.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/markdown_renderers.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/markdown_renderers.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx new file mode 100644 index 0000000000000..4e45ecd2f70cb --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx @@ -0,0 +1,57 @@ +/* + * 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 } from 'react'; +import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { PackageInfo } from '../../../../../types'; +import { Screenshots } from './screenshots'; +import { Readme } from './readme'; +import { Details } from './details'; + +interface Props { + packageInfo: PackageInfo; +} + +const LeftColumn = styled(EuiFlexItem)` + /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ + &&& { + margin-top: 77px; + } +`; + +export const OverviewPage: React.FC = memo(({ packageInfo }: Props) => { + return ( + + + + {packageInfo.readme ? ( + + ) : null} + + + + {packageInfo.screenshots && packageInfo.screenshots.length ? ( + + + + ) : null} + +
+ + + + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx similarity index 82% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx index 55050894d8702..92022f5e97a60 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx @@ -8,10 +8,9 @@ import { EuiLoadingContent, EuiText } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; -import { useLinks } from '../../hooks'; -import { ContentCollapse } from './content_collapse'; +import { useLinks } from '../../../hooks'; +import { sendGetFileByPath } from '../../../../../hooks'; import { markdownRenderers } from './markdown_renderers'; -import { sendGetFileByPath } from '../../../../hooks'; export function Readme({ readmePath, @@ -43,13 +42,11 @@ export function Readme({ return ( {markdown !== undefined ? ( - - - + ) : ( {/* simulates a long page of text loading */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx new file mode 100644 index 0000000000000..33b2c3efe2fba --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useMemo, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiPagination } from '@elastic/eui'; +import { ScreenshotItem } from '../../../../../types'; +import { useLinks } from '../../../hooks'; + +interface ScreenshotProps { + images: ScreenshotItem[]; + packageName: string; + version: string; +} + +export const Screenshots: React.FC = memo(({ images, packageName, version }) => { + const { toPackageImage } = useLinks(); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const maxImageIndex = useMemo(() => images.length - 1, [images.length]); + const currentImageUrl = useMemo( + () => toPackageImage(images[currentImageIndex], packageName, version), + [currentImageIndex, images, packageName, toPackageImage, version] + ); + + return ( + + {/* Title with carousel navigation */} + + + + +

+ +

+
+
+ + setCurrentImageIndex(activePage)} + compressed + /> + +
+
+ + {/* Current screenshot */} + + {currentImageUrl ? ( + + ) : ( + + )} + +
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx deleted file mode 100644 index 750f8ad3f80b4..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; -import { PackageInfo } from '../../../../types'; -import { Readme } from './readme'; -import { Screenshots } from './screenshots'; - -export function OverviewPanel(props: PackageInfo) { - const { screenshots, readme, name, version } = props; - return ( - - {readme && } - - {screenshots && } - - ); -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts new file mode 100644 index 0000000000000..d92dfc9236329 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { PackagePoliciesPage } from './package_policies'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx similarity index 86% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx index b8e5388dc1535..53cd642a1ef78 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx @@ -12,21 +12,22 @@ import { EuiBasicTable, EuiLink, EuiTableFieldDataColumnType, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; -import { useGetPackageInstallStatus } from '../../hooks'; -import { InstallStatus } from '../../../../types'; -import { useLink } from '../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; -import { useUrlPagination } from '../../../../hooks'; +import { InstallStatus } from '../../../../../types'; +import { useLink, useUrlPagination } from '../../../../../hooks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { LinkAndRevision, LinkAndRevisionProps } from '../../../../../components'; +import { LinkedAgentCount } from '../../../../../components/linked_agent_count'; +import { useGetPackageInstallStatus } from '../../../hooks'; import { PackagePolicyAndAgentPolicy, usePackagePoliciesWithAgentPolicy, } from './use_package_policies_with_agent_policy'; -import { LinkAndRevision, LinkAndRevisionProps } from '../../../../components'; import { Persona } from './persona'; -import { LinkedAgentCount } from '../../../../components/linked_agent_count'; const IntegrationDetailsLink = memo<{ packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy']; @@ -52,6 +53,7 @@ const AgentPolicyDetailLink = memo<{ children: ReactNode; }>(({ agentPolicyId, revision, children }) => { const { getHref } = useLink(); + return ( { +export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -197,18 +199,25 @@ export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProp // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) { - return ; + return ( + + ); } return ( - + + + + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx similarity index 99% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx index 373140f6fef6d..02e36df57009e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx @@ -4,9 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React, { CSSProperties, memo, useCallback } from 'react'; +import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiAvatarProps } from '@elastic/eui/src/components/avatar/avatar'; const MIN_WIDTH: CSSProperties = { minWidth: 0 }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts similarity index 87% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index 4f014d93c5d5b..ff8343314b4ac 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -6,19 +6,19 @@ */ import { useEffect, useMemo, useState } from 'react'; -import { PackagePolicy } from '../../../../../../../common/types/models'; import { + PackagePolicy, GetAgentPoliciesResponse, GetAgentPoliciesResponseItem, -} from '../../../../../../../common/types/rest_spec'; -import { useGetPackagePolicies } from '../../../../hooks/use_request'; + GetPackagePoliciesResponse, +} from '../../../../../types'; +import { agentPolicyRouteService } from '../../../../../services'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { useGetPackagePolicies } from '../../../../../hooks'; import { SendConditionalRequestConfig, useConditionalRequest, -} from '../../../../hooks/use_request/use_request'; -import { agentPolicyRouteService } from '../../../../../../../common/services'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; -import { GetPackagePoliciesResponse } from '../../../../../../../common/types/rest_spec'; +} from '../../../../../hooks/use_request/use_request'; export interface PackagePolicyEnriched extends PackagePolicy { _agentPolicy: GetAgentPoliciesResponseItem | undefined; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx deleted file mode 100644 index d1e6cf07f57ae..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx +++ /dev/null @@ -1,92 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { ScreenshotItem } from '../../../../types'; -import { useLinks } from '../../hooks'; - -interface ScreenshotProps { - images: ScreenshotItem[]; - packageName: string; - version: string; -} - -const getHorizontalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2; -const getVerticalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75; -const getPadding = (styledProps: any) => - styledProps.hascaption - ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding( - styledProps - )}px ${getVerticalPadding(styledProps)}px` - : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`; -const ScreenshotsContainer = styled(EuiFlexGroup)` - background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%), - ${(styledProps) => styledProps.theme.eui.euiColorPrimary}; - padding: ${(styledProps) => getPadding(styledProps)}; - flex: 0 0 auto; - border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius}; -`; - -// fixes ie11 problems with nested flex items -const NestedEuiFlexItem = styled(EuiFlexItem)` - flex: 0 0 auto !important; -`; - -export function Screenshots(props: ScreenshotProps) { - const { toPackageImage } = useLinks(); - const { images, packageName, version } = props; - - // for now, just get first image - const image = images[0]; - const hasCaption = image.title ? true : false; - const screenshotUrl = toPackageImage(image, packageName, version); - - return ( - - -

- -

-
- - - {hasCaption && ( - - - {image.title} - - - - )} - {screenshotUrl && ( - - {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, - set image to same width. Will need to update if size changes. - */} - - - )} - -
- ); -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_install.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_uninstall.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_uninstall.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts new file mode 100644 index 0000000000000..33c6a11f40672 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { SettingsPage } from './settings'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx similarity index 96% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx index ae85f59424fbd..ca37095f5db19 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx @@ -8,9 +8,9 @@ import { EuiButton } from '@elastic/eui'; import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PackageInfo, InstallStatus } from '../../../../types'; -import { useCapabilities } from '../../../../hooks'; -import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../hooks'; +import { PackageInfo, InstallStatus } from '../../../../../types'; +import { useCapabilities } from '../../../../../hooks'; +import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../../hooks'; import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx new file mode 100644 index 0000000000000..2c3559a651307 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx @@ -0,0 +1,238 @@ +/* + * 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 } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import { InstallStatus, PackageInfo } from '../../../../../types'; +import { useGetPackagePolicies } from '../../../../../hooks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { useGetPackageInstallStatus } from '../../../hooks'; +import { UpdateIcon } from '../components'; +import { InstallationButton } from './installation_button'; + +const SettingsTitleCell = styled.td` + padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${(props) => props.theme.eui.spacerSizes.s}; +`; + +const NoteLabel = () => ( + +); +const UpdatesAvailableMsg = () => ( + + + + +); + +interface Props { + packageInfo: PackageInfo; +} + +export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { + const { name, title, removable, latestVersion, version } = packageInfo; + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { data: packagePoliciesData } = useGetPackagePolicies({ + perPage: 0, + page: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`, + }); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); + const packageHasUsages = !!packagePoliciesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; + return ( + + + + + +

+ +

+
+ + {installedVersion !== null && ( +
+ +

+ +

+
+ + + + + + + + + + + + + + + + +
+ + {installedVersion} + + {updateAvailable && } +
+ + {latestVersion} + +
+ {updateAvailable && ( +

+ +

+ )} +
+ )} + {!hideInstallOptions && !isUpdating && ( +
+ + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( +
+ +

+ +

+
+ +

+ +

+
+ ) : ( +
+ +

+ +

+
+ +

+ +

+
+ )} + + +

+ +

+
+
+ {packageHasUsages && removable === true && ( +

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

+ )} + {removable === false && ( +

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

+ )} +
+ )} +
+
+
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx deleted file mode 100644 index b9835f41a0d79..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx +++ /dev/null @@ -1,230 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; -import styled from 'styled-components'; -import { InstallStatus, PackageInfo } from '../../../../types'; -import { useGetPackagePolicies } from '../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../constants'; -import { useGetPackageInstallStatus } from '../../hooks'; -import { InstallationButton } from './installation_button'; -import { UpdateIcon } from '../../components/icons'; - -const SettingsTitleCell = styled.td` - padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; - padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; -`; - -const UpdatesAvailableMsgContainer = styled.span` - padding-left: ${(props) => props.theme.eui.spacerSizes.s}; -`; - -const NoteLabel = () => ( - -); -const UpdatesAvailableMsg = () => ( - - - - -); - -export const SettingsPanel = ( - props: Pick -) => { - const { name, title, removable, latestVersion, version } = props; - const getPackageInstallStatus = useGetPackageInstallStatus(); - const { data: packagePoliciesData } = useGetPackagePolicies({ - perPage: 0, - page: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${props.name}`, - }); - const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); - const packageHasUsages = !!packagePoliciesData?.total; - const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; - const isViewingOldPackage = version < latestVersion; - // hide install/remove options if the user has version of the package is installed - // and this package is out of date or if they do have a version installed but it's not this one - const hideInstallOptions = - (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || - (installationStatus === InstallStatus.installed && installedVersion !== version); - - const isUpdating = installationStatus === InstallStatus.installing && installedVersion; - return ( - - -

- -

-
- - {installedVersion !== null && ( -
- -

- -

-
- - - - - - - - - - - - - - - - -
- - {installedVersion} - - {updateAvailable && } -
- - {latestVersion} - -
- {updateAvailable && ( -

- -

- )} -
- )} - {!hideInstallOptions && !isUpdating && ( -
- - {installationStatus === InstallStatus.notInstalled || - installationStatus === InstallStatus.installing ? ( -
- -

- -

-
- -

- -

-
- ) : ( -
- -

- -

-
- -

- -

-
- )} - - -

- -

-
-
- {packageHasUsages && removable === true && ( -

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

- )} - {removable === false && ( -

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

- )} -
- )} -
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 5ccbfc953428e..89aa5ad1add35 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -49,6 +49,7 @@ export { CreatePackagePolicyResponse, UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, + GetPackagePoliciesResponse, // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents @@ -122,6 +123,7 @@ export { InstallationStatus, Installable, RegistryRelease, + PackageSpecCategory, } from '../../../../common'; export * from './intra_app_route_state'; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index ef0c34ee56393..6b35f74b3febc 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -79,11 +79,10 @@ export const createPackagePolicyHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; - let newData = { ...request.body }; try { - newData = await packagePolicyService.runExternalCallbacks( + const newData = await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', - newData, + { ...request.body }, context, request ); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 9800ddf95f7b2..31e9a63175d18 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -36,7 +36,11 @@ import { FleetServerPolicy, AGENT_POLICY_INDEX, } from '../../common'; -import { AgentPolicyNameExistsError, AgentPolicyDeletionError } from '../errors'; +import { + AgentPolicyNameExistsError, + AgentPolicyDeletionError, + IngestManagerError, +} from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; @@ -382,6 +386,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (oldAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); + } + return await this._update( soClient, esClient, @@ -409,6 +417,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (oldAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot remove integrations of managed policy ${id}`); + } + return await this._update( soClient, esClient, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 4b04014b20969..8d1ac90f3ec15 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -25,6 +25,7 @@ import { doesAgentPolicyAlreadyIncludePackage, } from '../../common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; +import { IngestManagerError, ingestErrorToResponseOptions } from '../errors'; import { NewPackagePolicy, UpdatePackagePolicy, @@ -63,15 +64,20 @@ class PackagePolicyService { const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id); if (!parentAgentPolicy) { throw new Error('Agent policy not found'); - } else { - if ( - (parentAgentPolicy.package_policies as PackagePolicy[]).find( - (siblingPackagePolicy) => siblingPackagePolicy.name === packagePolicy.name - ) - ) { - throw new Error('There is already a package with the same name on this agent policy'); - } } + if (parentAgentPolicy.is_managed) { + throw new IngestManagerError( + `Cannot add integrations to managed policy ${parentAgentPolicy.id}` + ); + } + if ( + (parentAgentPolicy.package_policies as PackagePolicy[]).find( + (siblingPackagePolicy) => siblingPackagePolicy.name === packagePolicy.name + ) + ) { + throw new Error('There is already a package with the same name on this agent policy'); + } + // Add ids to stream const packagePolicyId = options?.id || uuid.v4(); let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) => @@ -285,6 +291,9 @@ class PackagePolicyService { if (!parentAgentPolicy) { throw new Error('Agent policy not found'); } else { + if (parentAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); + } if ( (parentAgentPolicy.package_policies as PackagePolicy[]).find( (siblingPackagePolicy) => @@ -295,7 +304,7 @@ class PackagePolicyService { } } - let inputs = await restOfPackagePolicy.inputs.map((input) => + let inputs = restOfPackagePolicy.inputs.map((input) => assignStreamIdToInput(oldPackagePolicy.id, input) ); @@ -363,10 +372,11 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); - } catch (e) { + } catch (error) { result.push({ id, success: false, + ...ingestErrorToResponseOptions(error), }); } } diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 09e319b9935d3..ade43638deb68 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import React, { useCallback, useState, useEffect, useContext } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -21,7 +21,7 @@ import { SavedViewCreateModal } from './create_modal'; import { SavedViewUpdateModal } from './update_modal'; import { SavedViewManageViewsFlyout } from './manage_views_flyout'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { SavedView } from '../../containers/saved_view/saved_view'; +import { useSavedViewContext } from '../../containers/saved_view/saved_view'; import { SavedViewListModal } from './view_list_modal'; interface Props { @@ -47,7 +47,7 @@ export function SavedViewsToolbarControls(props: Props) { updatedView, currentView, setCurrentView, - } = useContext(SavedView.Context); + } = useSavedViewContext(); const [modalOpen, setModalOpen] = useState(false); const [viewListModalOpen, setViewListModalOpen] = useState(false); const [isInvalid, setIsInvalid] = useState(false); diff --git a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx index e867cf800f4b4..4c4835cbe4cdb 100644 --- a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx +++ b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx @@ -6,9 +6,14 @@ */ import createContainer from 'constate'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; import { useCallback, useMemo, useState, useEffect, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { SimpleSavedObject, SavedObjectAttributes } from 'kibana/public'; +import { useUrlState } from '../../utils/use_url_state'; import { useFindSavedObject } from '../../hooks/use_find_saved_object'; import { useCreateSavedObject } from '../../hooks/use_create_saved_object'; import { useDeleteSavedObject } from '../../hooks/use_delete_saved_object'; @@ -39,6 +44,14 @@ interface Props { shouldLoadDefault: boolean; } +const savedViewUrlStateRT = rt.type({ + viewId: rt.string, +}); +type SavedViewUrlState = rt.TypeOf; +const DEFAULT_SAVED_VIEW_STATE: SavedViewUrlState = { + viewId: '0', +}; + export const useSavedView = (props: Props) => { const { source, @@ -52,6 +65,13 @@ export const useSavedView = (props: Props) => { const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject< SavedViewSavedObject >(viewType); + const [urlState, setUrlState] = useUrlState({ + defaultState: DEFAULT_SAVED_VIEW_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'savedView', + }); + const [shouldLoadDefault] = useState(props.shouldLoadDefault); const [currentView, setCurrentView] = useState | null>(null); const [loadingDefaultView, setLoadingDefaultView] = useState(null); @@ -212,25 +232,35 @@ export const useSavedView = (props: Props) => { }); }, [setCurrentView, defaultViewId, defaultViewState]); - useEffect(() => { - if (loadingDefaultView || currentView || !shouldLoadDefault) { - return; - } - + const loadDefaultViewIfSet = useCallback(() => { if (defaultViewId !== '0') { loadDefaultView(); } else { setDefault(); setLoadingDefaultView(false); } - }, [ - loadDefaultView, - shouldLoadDefault, - setDefault, - loadingDefaultView, - currentView, - defaultViewId, - ]); + }, [defaultViewId, loadDefaultView, setDefault, setLoadingDefaultView]); + + useEffect(() => { + if (loadingDefaultView || currentView || !shouldLoadDefault) { + return; + } + + loadDefaultViewIfSet(); + }, [loadDefaultViewIfSet, loadingDefaultView, currentView, shouldLoadDefault]); + + useEffect(() => { + if (currentView && urlState.viewId !== currentView.id && data) + setUrlState({ viewId: currentView.id }); + }, [urlState, setUrlState, currentView, defaultViewId, data]); + + useEffect(() => { + if (!currentView && !loading && data) { + const viewToSet = views.find((v) => v.id === urlState.viewId); + if (viewToSet) setCurrentView(viewToSet); + else loadDefaultViewIfSet(); + } + }, [loading, currentView, data, views, setCurrentView, loadDefaultViewIfSet, urlState.viewId]); return { views, @@ -260,3 +290,11 @@ export const useSavedView = (props: Props) => { export const SavedView = createContainer(useSavedView); export const [SavedViewProvider, useSavedViewContext] = SavedView; + +const encodeUrlState = (state: SavedViewUrlState) => { + return savedViewUrlStateRT.encode(state); +}; +const decodeUrlState = (value: unknown) => { + const state = pipe(savedViewUrlStateRT.decode(value), fold(constant(undefined), identity)); + return state; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 8fd32bda7fbc8..240cb778275b1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -36,7 +36,7 @@ import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; -import { SavedView } from '../../containers/saved_view/saved_view'; +import { SavedViewProvider } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; @@ -195,7 +195,7 @@ const PageContent = (props: { const { options } = useContext(MetricsExplorerOptionsContainer.Context); return ( - - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index a3a47276bb521..e313cf0762af2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -89,11 +89,11 @@ export const NodeContextPopover = ({ - +

{node.name}

-
+ @@ -194,3 +194,12 @@ const OverlayPanel = euiStyled(EuiPanel).attrs({ paddingSize: 'none' })` max-width: 100%; } `; + +const OverlayTitle = euiStyled(EuiFlexItem)` + overflow: hidden; + & h4 { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 7123c022538e9..6b980d33c2559 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -23,7 +23,7 @@ import { useTrackPageview } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Layout } from './components/layout'; import { useLinkProps } from '../../../hooks/use_link_props'; -import { SavedView } from '../../../containers/saved_view/saved_view'; +import { SavedViewProvider } from '../../../containers/saved_view/saved_view'; import { DEFAULT_WAFFLE_VIEW_STATE } from './hooks/use_waffle_view_state'; import { useWaffleOptionsContext } from './hooks/use_waffle_options'; @@ -64,13 +64,13 @@ export const SnapshotPage = () => { ) : metricIndicesExist ? ( <> - - + ) : hasFailedLoadingSource ? ( diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts index 850fab6937f4b..7c92a81e1a896 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts @@ -79,7 +79,7 @@ export async function getLatestLogEntriesCategoriesDatasetsStats( return { categorization_status: latestHitSource.categorization_status, categorized_doc_count: latestHitSource.categorized_doc_count, - dataset: bucket.key.dataset ?? '', + dataset: bucket.key?.dataset ?? '', dead_category_count: latestHitSource.dead_category_count, failed_category_count: latestHitSource.failed_category_count, frequent_category_count: latestHitSource.frequent_category_count, diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts index c7b2590e4be98..2cbb0ef60cd16 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts @@ -98,9 +98,12 @@ export const logEntryCategorizerStatsHitRT = rt.type({ export type LogEntryCategorizerStatsHit = rt.TypeOf; -const compositeDatasetKeyRT = rt.type({ - dataset: rt.union([rt.string, rt.null]), -}); +const compositeDatasetKeyRT = rt.union([ + rt.type({ + dataset: rt.union([rt.string, rt.null]), + }), + rt.undefined, +]); export type CompositeDatasetKey = rt.TypeOf; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx index 0dd54118d0305..a9362060b2dd0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx @@ -90,7 +90,7 @@ export function TimeScaling({ iconSide="right" data-test-subj="indexPattern-time-scaling-popover" onClick={() => { - setPopoverOpen(true); + setPopoverOpen(!popoverOpen); }} > {i18n.translate('xpack.lens.indexPattern.timeScale.advancedSettings', { diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts index 8f88f626160ce..cfca262ec09c1 100644 --- a/x-pack/plugins/logstash/public/plugin.ts +++ b/x-pack/plugins/logstash/public/plugin.ts @@ -6,11 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { Subscription } from 'rxjs'; +import { Subscription, Subject, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import { once } from 'lodash'; -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { Capabilities, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { HomePublicPluginSetup, FeatureCatalogueCategory, @@ -30,6 +30,7 @@ interface SetupDeps { export class LogstashPlugin implements Plugin { private licenseSubscription?: Subscription; + private capabilities$ = new Subject(); public setup(core: CoreSetup, plugins: SetupDeps) { const logstashLicense$ = plugins.licensing.license$.pipe( @@ -51,35 +52,43 @@ export class LogstashPlugin implements Plugin { }, }); - this.licenseSubscription = logstashLicense$.subscribe((license: any) => { - if (license.enableLinks) { - managementApp.enable(); - } else { - managementApp.disable(); - } + this.licenseSubscription = combineLatest([logstashLicense$, this.capabilities$]).subscribe( + ([license, capabilities]) => { + const shouldShow = license.enableLinks && capabilities.management.ingest.pipelines === true; + if (shouldShow) { + managementApp.enable(); + } else { + managementApp.disable(); + } - if (plugins.home && license.enableLinks) { - // Ensure that we don't register the feature more than once - once(() => { - plugins.home!.featureCatalogue.register({ - id: 'management_logstash', - title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { - defaultMessage: 'Logstash Pipelines', - }), - description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { - defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', - }), - icon: 'pipelineApp', - path: '/app/management/ingest/pipelines', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, + if (plugins.home && shouldShow) { + // Ensure that we don't register the feature more than once + once(() => { + plugins.home!.featureCatalogue.register({ + id: 'management_logstash', + title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { + defaultMessage: 'Logstash Pipelines', + }), + description: i18n.translate( + 'xpack.logstash.homeFeature.logstashPipelinesDescription', + { + defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', + } + ), + icon: 'pipelineApp', + path: '/app/management/ingest/pipelines', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); }); - }); + } } - }); + ); } - public start(core: CoreStart) {} + public start(core: CoreStart) { + this.capabilities$.next(core.application.capabilities); + } public stop() { if (this.licenseSubscription) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index a017de8d43a71..b079fc154713e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -150,7 +150,7 @@ export const ExplorationQueryBar: FC = ({ = ({ closePopover={() => setErrorMessage(undefined)} input={ = ({ closePopover={() => setErrorMessage(undefined)} input={ { let testProps = { defaultModel: eventsDefaultModel, end: to, - id: 'test-stateful-events-viewer', + id: TimelineId.test, start: from, scopeId: SourcererScopeName.timeline, }; @@ -155,7 +155,7 @@ describe('EventsViewer', () => { indexName: 'auditbeat-7.10.1-2020.12.18-000001', }, tabType: 'query', - timelineId: 'test-stateful-events-viewer', + timelineId: TimelineId.test, }, type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', }); @@ -199,17 +199,22 @@ describe('EventsViewer', () => { defaultHeaders.forEach((header) => { test(`it renders the ${header.id} default EventsViewer column header`, () => { + testProps = { + ...testProps, + // Update with a new id, to force columns back to default. + id: TimelineId.test2, + }; const wrapper = mount( ); - defaultHeaders.forEach((h) => + defaultHeaders.forEach((h) => { expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( true - ) - ); + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 77573dbab0a53..254309aee906b 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -117,7 +117,7 @@ interface Props { filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; - id: string; + id: TimelineId; indexNames: string[]; indexPattern: IIndexPattern; isLive: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index c2fbfdb666e04..5004c23f9111c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -16,6 +16,7 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; +import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useTimelineEvents } from '../../../timelines/containers'; @@ -36,7 +37,7 @@ const testProps = { defaultModel: eventsDefaultModel, end: to, indexNames: [], - id: 'test-stateful-events-viewer', + id: TimelineId.test, scopeId: SourcererScopeName.default, start: from, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 526bc312172b0..2b5420674b89c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -12,6 +12,7 @@ import styled from 'styled-components'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; +import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Filter } from '../../../../../../../src/plugins/data/public'; @@ -34,7 +35,7 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export interface OwnProps { defaultModel: SubsetTimelineModel; end: string; - id: string; + id: TimelineId; scopeId: SourcererScopeName; start: string; headerFilterGroup?: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index 13da6ac904031..fe6f82e632f73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -32,10 +32,7 @@ export const FleetTrustedAppsCard = memo(( const trustedAppsListUrlPath = getTrustedAppsListPath(); const trustedAppRouteState = useMemo(() => { - const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details({ - pkgkey, - panel: 'custom', - })}`; + const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 8df0c92ca83e5..b5864a0a83cf2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -19,6 +19,7 @@ const initialState: DataState = { data: null, }, resolverComponentInstanceID: undefined, + indices: [], }; /* eslint-disable complexity */ export const dataReducer: Reducer = (state = initialState, action) => { @@ -35,6 +36,7 @@ export const dataReducer: Reducer = (state = initialS }, resolverComponentInstanceID: action.payload.resolverComponentInstanceID, locationSearch: action.payload.locationSearch, + indices: action.payload.indices, }; const panelViewAndParameters = selectors.panelViewAndParameters(nextState); return { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index c372c98c6e060..b864bb254a5fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -664,4 +664,85 @@ describe('data state', () => { `); }); }); + describe('when the resolver tree response is complete, still use non-default indices', () => { + beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID: 'a', + firstChildID: 'b', + secondChildID: 'c', + }); + const { schema, dataSource } = endpointSourceSchema(); + actions = [ + { + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema, + parameters: { + databaseDocumentID: '', + indices: ['someNonDefaultIndex'], + filters: {}, + }, + }, + }, + ]; + }); + it('should have an empty array for tree parameter indices, and a non empty array for event indices', () => { + const treeParameterIndices = selectors.treeParameterIndices(state()); + expect(treeParameterIndices.length).toBe(0); + const eventIndices = selectors.eventIndices(state()); + expect(eventIndices.length).toBe(1); + }); + }); + describe('when the resolver tree response is pending use the same indices the user is currently looking at data from', () => { + beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID: 'a', + firstChildID: 'b', + secondChildID: 'c', + }); + const { schema, dataSource } = endpointSourceSchema(); + actions = [ + { + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema, + parameters: { + databaseDocumentID: '', + indices: ['defaultIndex'], + filters: {}, + }, + }, + }, + { + type: 'appReceivedNewExternalProperties', + payload: { + databaseDocumentID: '', + resolverComponentInstanceID: '', + locationSearch: '', + indices: ['someNonDefaultIndex', 'someOtherIndex'], + shouldUpdate: false, + filters: {}, + }, + }, + { + type: 'appRequestedResolverData', + payload: { + databaseDocumentID: '', + indices: ['someNonDefaultIndex', 'someOtherIndex'], + filters: {}, + }, + }, + ]; + }); + it('should have an empty array for tree parameter indices, and the same set of indices as the last tree response', () => { + const treeParameterIndices = selectors.treeParameterIndices(state()); + expect(treeParameterIndices.length).toBe(0); + const eventIndices = selectors.eventIndices(state()); + expect(eventIndices.length).toBe(1); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index a39aa4f0cd983..fb6fb6073d7cf 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -63,6 +63,13 @@ export function resolverComponentInstanceID(state: DataState): string { return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; } +/** + * The indices resolver should use, passed in as external props. + */ +const currentIndices = (state: DataState): string[] => { + return state.indices; +}; + /** * The last NewResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that * we're currently interested in. @@ -71,6 +78,12 @@ const resolverTreeResponse = (state: DataState): NewResolverTree | undefined => return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined; }; +const lastResponseIndices = (state: DataState): string[] | undefined => { + return state.tree?.lastResponse?.successful + ? state.tree?.lastResponse?.parameters?.indices + : undefined; +}; + /** * If we received a NewResolverTree, return the schema associated with that tree, otherwise return undefined. * As of writing, this is only used for the info popover in the graph_controls panel @@ -336,10 +349,22 @@ export const timeRangeFilters = createSelector( /** * The indices to use for the requests with the backend. */ -export const treeParamterIndices = createSelector(treeParametersToFetch, (parameters) => { +export const treeParameterIndices = createSelector(treeParametersToFetch, (parameters) => { return parameters?.indices ?? []; }); +/** + * Panel requests should not use indices derived from the tree parameter selector, as this is only defined briefly while the resolver_tree_fetcher middleware is running. + * Instead, panel requests should use the indices used by the last good request, falling back to the indices passed as external props. + */ +export const eventIndices = createSelector( + lastResponseIndices, + currentIndices, + function eventIndices(lastIndices, current): string[] { + return lastIndices ?? current ?? []; + } +); + export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( tree, originID, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts index 3b8389182e990..33772dddd676e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts @@ -32,7 +32,7 @@ export function CurrentRelatedEventFetcher( const state = api.getState(); const newParams = selectors.panelViewAndParameters(state); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); const oldParams = last; last = newParams; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts index 696e7f921673b..074fdf7535790 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts @@ -38,7 +38,7 @@ export function NodeDataFetcher( * This gets the visible nodes that we haven't already requested or received data for */ const newIDsToRequest: Set = selectors.newIDsToRequest(state)(Number.POSITIVE_INFINITY); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); if (newIDsToRequest.size <= 0) { return; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index fbce03caf64d8..19a11e07a9d87 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -27,7 +27,7 @@ export function RelatedEventsFetcher( const newParams = selectors.panelViewAndParameters(state); const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); const oldParams = last; const timeRangeFilters = selectors.timeRangeFilters(state); diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index a845de57bbdc6..4c088a8be4ed9 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -80,9 +80,14 @@ export const treeRequestParametersToAbort = composeSelectors( */ export const treeParameterIndices = composeSelectors( dataStateSelector, - dataSelectors.treeParamterIndices + dataSelectors.treeParameterIndices ); +/** + * An array of indices to use for resolver panel requests. + */ +export const eventIndices = composeSelectors(dataStateSelector, dataSelectors.eventIndices); + export const resolverComponentInstanceID = composeSelectors( dataStateSelector, dataSelectors.resolverComponentInstanceID diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index d3ddc51429ccd..e6a004938a267 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -376,6 +376,8 @@ export interface DataState { */ readonly resolverComponentInstanceID?: string; + readonly indices: string[]; + /** * The `search` part of the URL. */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 3783f5591c43e..f57ce42e7e079 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -18,6 +18,7 @@ import { kibanaObservable, createSecuritySolutionStorageMock, } from '../../../common/mock'; +import { TimelineId } from '../../../../common/types/timeline'; import { createStore, State } from '../../../common/store'; import * as timelineActions from '../../store/timeline/actions'; @@ -43,7 +44,7 @@ describe('Flyout', () => { const { storage } = createSecuritySolutionStorageMock(); const props = { onAppLeave: jest.fn(), - timelineId: 'test', + timelineId: TimelineId.test, }; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index f7518c2c34f66..bd7c7fbd1941f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -26,7 +26,7 @@ const Visible = styled.div<{ show?: boolean }>` Visible.displayName = 'Visible'; interface OwnProps { - timelineId: string; + timelineId: TimelineId; onAppLeave: (handler: AppLeaveHandler) => void; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx index e16cec78cf13b..4ccc7ef5b5bc5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx @@ -9,13 +9,14 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; +import { TimelineId } from '../../../../../common/types/timeline'; import { Pane } from '.'; describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + ); expect(EmptyComponent.find('Pane')).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index a4d85bd76b105..e63ffedf3da7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -11,12 +11,13 @@ import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { StatefulTimeline } from '../../timeline'; +import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { - timelineId: string; + timelineId: TimelineId; } const EuiFlyoutContainer = styled.div` diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index c0e1a54faa8dd..1286208bff9e6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -15,7 +15,6 @@ import { } from '../../../common/containers/use_full_screen'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { TimelineId } from '../../../../common/types/timeline'; - import { GraphOverlay } from '.'; jest.mock('../../../common/hooks/use_selector', () => ({ @@ -28,6 +27,10 @@ jest.mock('../../../common/containers/use_full_screen', () => ({ useTimelineFullScreen: jest.fn(), })); +jest.mock('../../../resolver/view/use_resolver_query_params_cleaner'); +jest.mock('../../../resolver/view/use_state_syncing_actions'); +jest.mock('../../../resolver/view/use_sync_selected_node'); + describe('GraphOverlay', () => { beforeEach(() => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ @@ -42,12 +45,11 @@ describe('GraphOverlay', () => { describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { const isEventViewer = true; - const timelineId = 'used-as-an-events-viewer'; test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -69,7 +71,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 1b3a0c21ef683..9c9c56461609d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -30,6 +30,8 @@ import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { Resolver } from '../../../resolver/view'; import { @@ -38,8 +40,6 @@ import { endSelector, } from '../../../common/components/super_date_picker/selectors'; import * as i18n from './translations'; -import { useUiSetting$ } from '../../../common/lib/kibana'; -import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; const OverlayContainer = styled.div` ${({ $restrictWidth }: { $restrictWidth: boolean }) => @@ -61,14 +61,14 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` interface OwnProps { isEventViewer: boolean; - timelineId: string; + timelineId: TimelineId; } interface NavigationProps { fullScreen: boolean; globalFullScreen: boolean; onCloseOverlay: () => void; - timelineId: string; + timelineId: TimelineId; timelineFullScreen: boolean; toggleFullScreen: () => void; } @@ -169,16 +169,14 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } globalFullScreen, ]); - const { signalIndexName } = useSignalIndex(); - const [siemDefaultIndices] = useUiSetting$(DEFAULT_INDEX_KEY); - const indices: string[] | null = useMemo(() => { - if (signalIndexName === null) { - return null; - } else { - return [...siemDefaultIndices, signalIndexName]; - } - }, [signalIndexName, siemDefaultIndices]); + let sourcereScope = SourcererScopeName.default; + if ([TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage].includes(timelineId)) { + sourcereScope = SourcererScopeName.detections; + } else if (timelineId === TimelineId.active) { + sourcereScope = SourcererScopeName.timeline; + } + const { selectedPatterns } = useSourcererScope(sourcereScope); return ( = ({ isEventViewer, timelineId } - {graphEventId !== undefined && indices !== null ? ( + {graphEventId !== undefined ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx index db4867e1abfe7..1678a92c4cdaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -9,10 +9,11 @@ import React, { useMemo } from 'react'; import { timelineSelectors } from '../../../store/timeline'; import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineId } from '../../../../../common/types/timeline'; import { GraphOverlay } from '../../graph_overlay'; interface GraphTabContentProps { - timelineId: string; + timelineId: TimelineId; } const GraphTabContentComponent: React.FC = ({ timelineId }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 219d32f147b60..e7422e32805a9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -12,7 +12,7 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper'; import '../../../common/mock/match_media'; import { mockBrowserFields, mockDocValueFields } from '../../../common/containers/source/mock'; - +import { TimelineId } from '../../../../common/types/timeline'; import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common/mock'; import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; @@ -55,7 +55,7 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { - timelineId: 'timeline-test', + timelineId: TimelineId.test, }; beforeEach(() => { @@ -91,7 +91,7 @@ describe('StatefulTimeline', () => { ); expect( wrapper - .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) + .find(`[data-timeline-id="test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) .first() .exists() ).toEqual(true); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 9cb95daba685b..c37fc93e33b08 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -18,7 +18,7 @@ import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; -import { TimelineType, TimelineTabs } from '../../../../common/types/timeline'; +import { TimelineType, TimelineTabs, TimelineId } from '../../../../common/types/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { activeTimeline } from '../../containers/active_timeline_context'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; @@ -35,7 +35,7 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { - timelineId: string; + timelineId: TimelineId; } const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 9f6bfcf7e320c..ca70e4ae64686 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -9,7 +9,7 @@ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui'; import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { TimelineTabs } from '../../../../../common/types/timeline'; +import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline'; import { useShallowEqualSelector, @@ -42,7 +42,7 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { - timelineId: string; + timelineId: TimelineId; graphEventId?: string; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 8af0df3063026..56e2f9c7c7304 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -47,6 +47,8 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + quarantine_result: true, + quarantine_message: 'this file is bad', something_else: 'nope', }, }, @@ -79,6 +81,8 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + quarantine_result: true, + quarantine_message: 'this file is bad', }, }, host: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 4e32410cdb6ae..a18604fb92a40 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -320,6 +320,8 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + quarantine_result: true, + quarantine_message: true, }, }, host: { diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index 40213b3743d62..ad5a2e11409ec 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -201,10 +201,10 @@ export class TaskManagerRunner implements TaskRunner { }); const stopTaskTimer = startTaskTimer(); - const apmTrans = apm.startTransaction( - `taskManager run ${this.instance.taskType}`, - 'taskManager' - ); + const apmTrans = apm.startTransaction(`taskManager run`, 'taskManager'); + apmTrans?.addLabels({ + taskType: this.taskType, + }); try { this.task = this.definition.createTaskRunner(modifiedContext); const result = await this.task.run(); @@ -232,10 +232,11 @@ export class TaskManagerRunner implements TaskRunner { public async markTaskAsRunning(): Promise { performance.mark('markTaskAsRunning_start'); - const apmTrans = apm.startTransaction( - `taskManager markTaskAsRunning ${this.instance.taskType}`, - 'taskManager' - ); + const apmTrans = apm.startTransaction(`taskManager markTaskAsRunning`, 'taskManager'); + + apmTrans?.addLabels({ + taskType: this.taskType, + }); const now = new Date(); try { diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 8c66db9c418ea..44431795a34ba 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -37,6 +37,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./package_policy/create')); loadTestFile(require.resolve('./package_policy/update')); loadTestFile(require.resolve('./package_policy/get')); + loadTestFile(require.resolve('./package_policy/delete')); // Agent policies loadTestFile(require.resolve('./agent_policy/index')); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 8e339bc78b087..c9c871e280f16 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -39,6 +39,52 @@ export default function ({ getService }: FtrProviderContext) { .send({ agentPolicyId }); }); + it('should fail for managed agent policies', async function () { + if (server.enabled) { + // get a managed policy + const { + body: { item: managedPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Managed policy from ${Date.now()}`, + namespace: 'default', + is_managed: true, + }); + + // try to add an integration to the managed policy + const { body } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: managedPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + + expect(body.statusCode).to.be(400); + expect(body.message).to.contain('Cannot add integrations to managed policy'); + + // delete policy we just made + await supertest.post(`/api/fleet/agent_policies/delete`).set('kbn-xsrf', 'xxxx').send({ + agentPolicyId: managedPolicy.id, + }); + } else { + warnAndSkipTest(this, log); + } + }); + it('should work with valid values', async function () { if (server.enabled) { await supertest diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts new file mode 100644 index 0000000000000..e64ba8580d145 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -0,0 +1,127 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + + // use function () {} and not () => {} here + // because `this` has to point to the Mocha context + // see https://mochajs.org/#arrow-functions + + describe('Package Policy - delete', async function () { + skipIfNoDockerRegistry(providerContext); + let agentPolicy: any; + let packagePolicy: any; + + before(async function () { + let agentPolicyResponse = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }); + + // if one already exists, re-use that + if (agentPolicyResponse.body.statusCode === 409) { + const errorRegex = /^agent policy \'(?[\w,\-]+)\' already exists/i; + const result = errorRegex.exec(agentPolicyResponse.body.message); + if (result?.groups?.id) { + agentPolicyResponse = await supertest + .put(`/api/fleet/agent_policies/${result.groups.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }); + } + } + agentPolicy = agentPolicyResponse.body.item; + + const { body: packagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: agentPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }); + packagePolicy = packagePolicyResponse.item; + }); + + after(async function () { + await supertest + .post(`/api/fleet/agent_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId: agentPolicy.id }); + + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }); + }); + + it('should fail on managed agent policies', async function () { + // update existing policy to managed + await supertest + .put(`/api/fleet/agent_policies/${agentPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: agentPolicy.name, + namespace: agentPolicy.namespace, + is_managed: true, + }) + .expect(200); + + // try to delete + const { body: results } = await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }) + .expect(200); + + // delete always succeeds (returns 200) with Array<{success: boolean}> + expect(Array.isArray(results)); + expect(results.length).to.be(1); + expect(results[0].success).to.be(false); + expect(results[0].body.message).to.contain('Cannot remove integrations of managed policy'); + + // revert existing policy to unmanaged + await supertest + .put(`/api/fleet/agent_policies/${agentPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: agentPolicy.name, + namespace: agentPolicy.namespace, + is_managed: false, + }) + .expect(200); + }); + + it('should work for unmanaged policies', async function () { + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index e0dc1a5d96b4b..9a70c6ad004dd 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -21,6 +21,7 @@ export default function (providerContext: FtrProviderContext) { describe('Package Policy - update', async function () { skipIfNoDockerRegistry(providerContext); let agentPolicyId: string; + let managedAgentPolicyId: string; let packagePolicyId: string; let packagePolicyId2: string; @@ -35,8 +36,30 @@ export default function (providerContext: FtrProviderContext) { name: 'Test policy', namespace: 'default', }); + agentPolicyId = agentPolicyResponse.item.id; + const { body: managedAgentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test managed policy', + namespace: 'default', + is_managed: true, + }); + + // if one already exists, re-use that + const managedExists = managedAgentPolicyResponse.statusCode === 409; + if (managedExists) { + const errorRegex = /^agent policy \'(?[\w,\-]+)\' already exists/i; + const result = errorRegex.exec(managedAgentPolicyResponse.message); + if (result?.groups?.id) { + managedAgentPolicyId = result.groups.id; + } + } else { + managedAgentPolicyId = managedAgentPolicyResponse.item.id; + } + const { body: packagePolicyResponse } = await supertest .post(`/api/fleet/package_policies`) .set('kbn-xsrf', 'xxxx') @@ -83,6 +106,29 @@ export default function (providerContext: FtrProviderContext) { .send({ agentPolicyId }); }); + it('should fail on managed agent policies', async function () { + const { body } = await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'updated_namespace', + policy_id: managedAgentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + + expect(body.message).to.contain('Cannot update integrations of managed policy'); + }); + it('should work with valid values', async function () { await supertest .put(`/api/fleet/package_policies/${packagePolicyId}`) diff --git a/x-pack/test/functional/apps/dashboard/sync_colors.ts b/x-pack/test/functional/apps/dashboard/sync_colors.ts index 5dd390dd7976f..7e54f966870c3 100644 --- a/x-pack/test/functional/apps/dashboard/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/sync_colors.ts @@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'geo.src', }); - await PageObjects.lens.save('vis1', true, true); + await PageObjects.lens.save('vis1', false, true); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardAddPanel.clickCreateNewLink(); await dashboardAddPanel.clickVisType('lens'); @@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.addFilter('geo.src', 'is not', 'CN'); - await PageObjects.lens.save('vis2', true, true); + await PageObjects.lens.save('vis2', false, true); await PageObjects.header.waitUntilLoadingHasFinished(); const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0)); const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1)); diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts new file mode 100644 index 0000000000000..ad393e620746f --- /dev/null +++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts @@ -0,0 +1,243 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'dashboard', + 'visualize', + 'lens', + 'timeToVisualize', + 'common', + 'header', + ]); + const find = getService('find'); + const listingTable = getService('listingTable'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const testSubjects = getService('testSubjects'); + const security = getService('security'); + + describe('lens add-to-dashboards tests', () => { + it('should allow new lens vizs be added to a new dashboard', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.lens.save('New Lens from Modal', false, false, 'new'); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow existing lens vizs be added to a new dashboard', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + await PageObjects.lens.save('Artistpreviouslyknownaslens Copy', true, false, 'new'); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new lens vizs be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.dashboard.saveDashboard('My Very Cool Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Cool Dashboard', 1); + + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.lens.save( + 'New Lens from Modal', + false, + false, + 'existing', + 'My Very Cool Dashboard' + ); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + + it('should allow existing lens vizs be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.dashboard.saveDashboard('My Wonderful Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Wonderful Dashboard', 1); + + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + await PageObjects.lens.save( + 'Artistpreviouslyknownaslens Copy', + true, + false, + 'existing', + 'My Wonderful Dashboard' + ); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + + describe('Capabilities', function capabilitiesTests() { + describe('dashboard no-access privileges', () => { + before(async () => { + await PageObjects.common.navigateToApp('visualize'); + await security.testUser.setRoles(['test_logstash_reader', 'global_visualize_all'], true); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not display dashboard flow prompt', async () => { + await PageObjects.common.navigateToApp('visualize'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoLandingPage(); + + const hasPrompt = await testSubjects.exists('visualize-dashboard-flow-prompt'); + expect(hasPrompt).to.eql(false); + }); + + it('should not display add-to-dashboard options', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const hasOptions = await testSubjects.exists('add-to-dashboard-options'); + expect(hasOptions).to.eql(false); + }); + }); + + describe('dashboard read-only privileges', () => { + before(async () => { + await security.testUser.setRoles( + ['test_logstash_reader', 'global_visualize_all', 'global_dashboard_read'], + true + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not display dashboard flow prompt', async () => { + await PageObjects.common.navigateToApp('visualize'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoLandingPage(); + + const hasPrompt = await testSubjects.exists('visualize-dashboard-flow-prompt'); + expect(hasPrompt).to.eql(false); + }); + + it('should not display add-to-dashboard options', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const hasOptions = await testSubjects.exists('add-to-dashboard-options'); + expect(hasOptions).to.eql(false); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 10b1f4d30145f..31b7b665fb2f0 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./add_to_dashboard')); loadTestFile(require.resolve('./table')); loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); diff --git a/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js b/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js new file mode 100644 index 0000000000000..9bbf6b1afab66 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js @@ -0,0 +1,122 @@ +/* + * 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 expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'maps', + 'timeToVisualize', + 'visualize', + ]); + + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const security = getService('security'); + + describe('maps add-to-dashboard save flow', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should allow new map be added to a new dashboard', async () => { + await PageObjects.maps.openNewMap(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('map 1', { addToDashboard: 'new' }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow existing maps be added to a new dashboard', async () => { + await PageObjects.maps.loadSavedMap('document example'); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('document example copy', { + addToDashboard: 'new', + saveAsNew: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new map be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + + await PageObjects.dashboard.saveDashboard('My Very Cool Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Cool Dashboard', 1); + + await PageObjects.maps.openNewMap(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('My New Map 2', { + addToDashboard: 'existing', + dashboardId: 'My Very Cool Dashboard', + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + + it('should allow existing maps be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + + await PageObjects.dashboard.saveDashboard('My Wonderful Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Wonderful Dashboard', 1); + + await PageObjects.maps.loadSavedMap('document example'); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('document example copy 2', { + addToDashboard: 'existing', + dashboardId: 'My Wonderful Dashboard', + saveAsNew: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 9fd4c9db703db..552f830e2a379 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -7,6 +7,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { + loadTestFile(require.resolve('./add_to_dashboard')); loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./embeddable_library')); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index add6979c2dde1..dcb730f77725d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -17,7 +17,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const find = getService('find'); const comboBox = getService('comboBox'); const browser = getService('browser'); - const PageObjects = getPageObjects(['header', 'timePicker', 'common', 'visualize', 'dashboard']); + + const PageObjects = getPageObjects([ + 'header', + 'timePicker', + 'common', + 'visualize', + 'dashboard', + 'timeToVisualize', + ]); return logWrapper('lensPage', log, { /** @@ -341,16 +349,16 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont title: string, saveAsNew?: boolean, redirectToOrigin?: boolean, - addToDashboard?: boolean, + addToDashboard?: 'new' | 'existing' | null, dashboardId?: string ) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('lnsApp_saveButton'); - await PageObjects.visualize.setSaveModalValues(title, { + await PageObjects.timeToVisualize.setSaveModalValues(title, { saveAsNew, redirectToOrigin, - addToDashboard, + addToDashboard: addToDashboard ? addToDashboard : null, dashboardId, }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts index 15e1c37befd9f..791fed9424966 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts @@ -18,7 +18,7 @@ export function FleetIntegrations({ getService, getPageObjects }: FtrProviderCon return { async navigateToIntegrationDetails(pkgkey: string) { await pageObjects.common.navigateToApp(PLUGIN_ID, { - hash: pagePathGetters.integration_details({ pkgkey }), + hash: pagePathGetters.integration_details_overview({ pkgkey }), }); }, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 4cbec2da21807..6209503e75610 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -68,6 +68,7 @@ { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/osquery/tsconfig.json" }, { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/runtime_fields/tsconfig.json" }, { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 00286ac47da6e..5589c62010db1 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -30,6 +30,7 @@ "plugins/maps_legacy_licensing/**/*", "plugins/ml/**/*", "plugins/observability/**/*", + "plugins/osquery/**/*", "plugins/reporting/**/*", "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", @@ -133,6 +134,7 @@ { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, { "path": "./plugins/observability/tsconfig.json" }, + { "path": "./plugins/osquery/tsconfig.json" }, { "path": "./plugins/painless_lab/tsconfig.json" }, { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index c9f3186ffcba2..319025b3aab77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6488,7 +6488,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.23", "@types/react@^16.9.36": +"@types/react@*", "@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== @@ -21069,10 +21069,10 @@ moment-timezone@^0.5.27: resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75" integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw== -monaco-editor@^0.17.0: - version "0.17.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" - integrity sha512-JAc0mtW7NeO+0SwPRcdkfDbWLgkqL9WfP1NbpP9wNASsW6oWqgZqNIWt4teymGjZIXTElx3dnQmUYHmVrJ7HxA== +monaco-editor@*, monaco-editor@^0.22.3: + version "0.22.3" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.22.3.tgz#69b42451d3116c6c08d9b8e052007ff891fd85d7" + integrity sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ== monitor-event-loop-delay@^1.0.0: version "1.0.0" @@ -24247,12 +24247,12 @@ react-moment-proptypes@^1.7.0: dependencies: moment ">=1.6.0" -react-monaco-editor@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.27.0.tgz#2dbf47b8fd4d8e4763934051f07291d9b128bb89" - integrity sha512-Im40xO4DuFlQ6kVcSBHC+p70fD/5aErUy1uyLT9RZ4nlehn6BOPpwmcw/2IN/LfMvy8X4WmLuuvrNftBZLH+vA== +react-monaco-editor@^0.41.2: + version "0.41.2" + resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.41.2.tgz#7ec9cadc101d73003a908fca61c50011f237d2b5" + integrity sha512-0nNqkkSLtUQDHtcCASv3ccYukD+P2uvFzcFZGh6iWg9RZF3Rj9/+jqsTNo2cl4avkX8JVGC/qnZr/g7hxXTBTQ== dependencies: - "@types/react" "^16.8.23" + monaco-editor "*" prop-types "^15.7.2" react-motion@^0.4.8: