diff --git a/.eslintrc.js b/.eslintrc.js index 6657ba3cb1f01..4e46336ec70ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -908,6 +908,7 @@ module.exports = { }, { files: [ + 'x-pack/plugins/aiops/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/apm/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/exploratory_view/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/infra/**/*.{js,mjs,ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index be401c26dc0d2..89f769c0f12ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -67,6 +67,7 @@ packages/kbn-ci-stats-performance-metrics @elastic/kibana-operations packages/kbn-ci-stats-reporter @elastic/kibana-operations packages/kbn-ci-stats-shipper-cli @elastic/kibana-operations packages/kbn-cli-dev-mode @elastic/kibana-operations +packages/cloud @elastic/kibana-core x-pack/plugins/cloud_integrations/cloud_chat @elastic/kibana-core x-pack/plugins/cloud_integrations/cloud_chat_provider @elastic/kibana-core x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/platform-onboarding diff --git a/.github/workflows/create-deploy-tag.yml b/.github/workflows/create-deploy-tag.yml index abe2e131165ec..85e226d384cc2 100644 --- a/.github/workflows/create-deploy-tag.yml +++ b/.github/workflows/create-deploy-tag.yml @@ -102,7 +102,7 @@ jobs: "", "", " (use Elastic Cloud Staging VPN)", - "", + "", "" ] - name: Post Slack failure message diff --git a/.i18nrc.json b/.i18nrc.json index b5e17c18d3542..4657840019f6c 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -15,6 +15,7 @@ "customIntegrations": "src/plugins/custom_integrations", "customIntegrationsPackage": "packages/kbn-custom-integrations", "dashboard": "src/plugins/dashboard", + "cloud": "packages/cloud", "domDragDrop": "packages/kbn-dom-drag-drop", "controls": "src/plugins/controls", "data": "src/plugins/data", diff --git a/config/serverless.yml b/config/serverless.yml index 33eaec2b22b6a..8319d4c0ecee9 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -7,6 +7,7 @@ xpack.fleet.internal.disableILMPolicies: true xpack.fleet.internal.disableProxies: true xpack.fleet.internal.activeAgentsSoftLimit: 25000 xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions: true +xpack.fleet.internal.retrySetupOnBoot: true # Cloud links xpack.cloud.base_url: 'https://cloud.elastic.co' diff --git a/dev_docs/tutorials/performance/running_performance_journey_in_cloud.mdx b/dev_docs/tutorials/performance/running_performance_journey_in_cloud.mdx index 3f8b373afad39..6ab160ac93328 100644 --- a/dev_docs/tutorials/performance/running_performance_journey_in_cloud.mdx +++ b/dev_docs/tutorials/performance/running_performance_journey_in_cloud.mdx @@ -11,7 +11,7 @@ tags: ['kibana', 'onboarding', 'setup', 'performance', 'development', 'telemetry As a way to better understand user experience with Kibana in cloud, we support running performance journeys against Cloud deployments. The process takes a few steps: -- Create a cloud deployment +- Create a cloud deployment (8.11.0+ is supported) - Re-configure deployment with APM enabled and reporting metrics to the monitoring cluster - Create a user with `superuser` role to run tests with - Checkout the branch that matches your cloud deployment version @@ -35,7 +35,7 @@ Navigate to `Advanced Edit` page and change `Deployment Configuration` by adding ``` "user_settings_override_json": { "tracing.apm.enabled": "true", - "tracing.apm.environment": "development", + "tracing.apm.agent.environment": "development", "tracing.apm.agent.service_name": "elasticsearch", "tracing.apm.agent.server_url": "", "tracing.apm.agent.metrics_interval": "120s", @@ -50,6 +50,7 @@ Navigate to `Advanced Edit` page and change `Deployment Configuration` by adding ``` "user_settings_override_json": { + "coreApp.allowDynamicConfigOverrides": true, "elastic.apm.active": true, "elastic.apm.breakdownMetrics": false, "elastic.apm.captureBody": "all", @@ -74,8 +75,28 @@ Note: DEPLOYMENT_ID and YOUR_JOURNEY_NAME values are optional labels to find the Save changes and make sure cluster is restarted successfully. +### Use QAF to prepare the deployment +The quickest way to prepare ESS deployment is to use [QAF](https://github.com/elastic/qaf): + +- Make sure to add `~/.elastic/cloud.json` and ~/.elastic/cloud-admin.json with Cloud API (to create deployment) & Cloud Admin API (to modify it) keys +``` +{ + "api_key": { + "production": "", + "staging": "", + "qa": "" + } +} +``` +- Create deployment and modify it +``` +export EC_DEPLOYMENT_NAME=kibana-perf-8.11 +qaf elastic-cloud deployments create --stack-version 8.11.0-SNAPSHOT --environment staging --region gcp-us-central1 +qaf elastic-cloud deployments configure-for-performance-journeys +``` + ### Run the journey -Make sure you have created user with `superuser` role and the Kibana repo branch is matching your deployment version. +Make sure the Kibana repo branch is matching your deployment version. Set env variables to run FTR against your cloud deployment: ``` @@ -90,4 +111,6 @@ Run your journey with the command: node scripts/functional_test_runner.js --config x-pack/performance/journeys/$YOUR_JOURNEY_NAME.ts` ``` +APM & Telemetry labels will be updated on the fly and metrics/traces should be available in Telemetry Staging and kibana-ops-e2e-perf cluster. + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index e271eb6cce5c0..fef9ae71a085b 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -54,8 +54,9 @@ Details for each programming language library that Elastic provides are in the https://www.elastic.co/guide/en/elasticsearch/client/index.html[{es} Client documentation]. If you are running {kib} on our hosted {es} Service, -click *View deployment details* on the *Integrations* view +click *Endpoints* on the *Integrations* view to verify your {es} endpoint and Cloud ID, and create API keys for integration. +Alternatively, the *Endpoints* are also accessible through the top bar help menu. [float] === Add sample data diff --git a/package.json b/package.json index 3b7f8c030fc8f..4792c66f475ab 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,7 @@ "@kbn/chart-expressions-common": "link:src/plugins/chart_expressions/common", "@kbn/chart-icons": "link:packages/kbn-chart-icons", "@kbn/charts-plugin": "link:src/plugins/charts", + "@kbn/cloud": "link:packages/cloud", "@kbn/cloud-chat-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat", "@kbn/cloud-chat-provider-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat_provider", "@kbn/cloud-data-migration-plugin": "link:x-pack/plugins/cloud_integrations/cloud_data_migration", @@ -887,6 +888,7 @@ "email-addresses": "^5.0.0", "execa": "^5.1.1", "expiry-js": "0.1.7", + "exponential-backoff": "^3.1.1", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.1", "fflate": "^0.6.9", diff --git a/packages/cloud/README.md b/packages/cloud/README.md new file mode 100644 index 0000000000000..e387c4b9be959 --- /dev/null +++ b/packages/cloud/README.md @@ -0,0 +1,3 @@ +# @kbn/cloud + +Empty package generated by @kbn/generate diff --git a/packages/cloud/deployment_details/deployment_details.tsx b/packages/cloud/deployment_details/deployment_details.tsx new file mode 100644 index 0000000000000..278709f7b6d32 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details.tsx @@ -0,0 +1,81 @@ +/* + * 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 React from 'react'; + +import { + EuiForm, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDeploymentDetails } from './services'; +import { DeploymentDetailsEsInput } from './deployment_details_es_input'; +import { DeploymentDetailsCloudIdInput } from './deployment_details_cloudid_input'; + +const hasActiveModifierKey = (event: React.MouseEvent): boolean => { + return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey; +}; + +export const DeploymentDetails = ({ closeModal }: { closeModal?: () => void }) => { + const { cloudId, elasticsearchUrl, managementUrl, learnMoreUrl, navigateToUrl } = + useDeploymentDetails(); + const isInsideModal = !!closeModal; + + if (!cloudId) { + return null; + } + + return ( + + {/* Elastic endpoint */} + {elasticsearchUrl && } + + {/* Cloud ID */} + + + + + {managementUrl && ( + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + if (!hasActiveModifierKey(e)) { + e.preventDefault(); + navigateToUrl(managementUrl); + } + if (closeModal) { + closeModal(); + } + }} + flush="left" + > + {i18n.translate('cloud.deploymentDetails.createManageApiKeysButtonLabel', { + defaultMessage: 'Create and manage API keys', + })} + + + {!isInsideModal && ( + + + {i18n.translate('cloud.deploymentDetails.learnMoreButtonLabel', { + defaultMessage: 'Learn more', + })} + + + )} + + )} + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx new file mode 100644 index 0000000000000..a749fe4371715 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx @@ -0,0 +1,46 @@ +/* + * 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 React, { type FC } from 'react'; +import { + EuiFormRow, + EuiFieldText, + EuiCopy, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const DeploymentDetailsCloudIdInput: FC<{ cloudId: string }> = ({ cloudId }) => { + return ( + + + + + + + + {(copy) => ( + + )} + + + + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_es_input.tsx b/packages/cloud/deployment_details/deployment_details_es_input.tsx new file mode 100644 index 0000000000000..2998b5bade543 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_es_input.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React, { type FC } from 'react'; +import { + EuiFormRow, + EuiFieldText, + EuiCopy, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const DeploymentDetailsEsInput: FC<{ elasticsearchUrl: string }> = ({ + elasticsearchUrl, +}) => { + return ( + + + + + + + + {(copy) => ( + + )} + + + + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_modal.tsx b/packages/cloud/deployment_details/deployment_details_modal.tsx new file mode 100644 index 0000000000000..2f3d628c2ca47 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_modal.tsx @@ -0,0 +1,69 @@ +/* + * 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 React, { type FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { useDeploymentDetails } from './services'; +import { DeploymentDetails } from './deployment_details'; + +interface Props { + closeModal: () => void; +} + +export const DeploymentDetailsModal: FC = ({ closeModal }) => { + const { learnMoreUrl } = useDeploymentDetails(); + + return ( + { + closeModal(); + }} + style={{ width: 600 }} + data-test-subj="deploymentDetailsModal" + > + + + {i18n.translate('cloud.deploymentDetails.helpMenuLinks.endpoints', { + defaultMessage: 'Endpoints', + })} + + + + + + + + + + {i18n.translate('cloud.deploymentDetails.modal.learnMoreButtonLabel', { + defaultMessage: 'Learn more', + })} + + + + + {i18n.translate('cloud.deploymentDetails.modal.closeButtonLabel', { + defaultMessage: 'Close', + })} + + + + + + ); +}; diff --git a/packages/cloud/deployment_details/index.ts b/packages/cloud/deployment_details/index.ts new file mode 100644 index 0000000000000..2f37291eecd7c --- /dev/null +++ b/packages/cloud/deployment_details/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DeploymentDetailsKibanaProvider, DeploymentDetailsProvider } from './services'; +export { DeploymentDetails } from './deployment_details'; +export { DeploymentDetailsModal } from './deployment_details_modal'; diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx new file mode 100644 index 0000000000000..c4e8be12bb547 --- /dev/null +++ b/packages/cloud/deployment_details/services.tsx @@ -0,0 +1,123 @@ +/* + * 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 React, { FC, useContext } from 'react'; + +export interface DeploymentDetailsContextValue { + cloudId?: string; + elasticsearchUrl?: string; + managementUrl?: string; + learnMoreUrl: string; + navigateToUrl(url: string): Promise; +} + +const DeploymentDetailsContext = React.createContext(null); + +/** + * Abstract external service Provider. + */ +export const DeploymentDetailsProvider: FC = ({ + children, + ...services +}) => { + return ( + + {children} + + ); +}; + +/** + * Kibana-specific service types. + */ +export interface DeploymentDetailsKibanaDependencies { + /** CoreStart contract */ + core: { + application: { + navigateToUrl(url: string): Promise; + }; + }; + /** SharePluginStart contract */ + share: { + url: { + locators: { + get( + id: string + ): undefined | { useUrl: (params: { sectionId: string; appId: string }) => string }; + }; + }; + }; + /** CloudSetup contract */ + cloud: { + isCloudEnabled: boolean; + cloudId?: string; + elasticsearchUrl?: string; + }; + /** DocLinksStart contract */ + docLinks: { + links: { + fleet: { + apiKeysLearnMore: string; + }; + }; + }; +} + +/** + * Kibana-specific Provider that maps to known dependency types. + */ +export const DeploymentDetailsKibanaProvider: FC = ({ + children, + ...services +}) => { + const { + core: { + application: { navigateToUrl }, + }, + cloud: { isCloudEnabled, cloudId, elasticsearchUrl }, + share: { + url: { locators }, + }, + docLinks: { + links: { + fleet: { apiKeysLearnMore }, + }, + }, + } = services; + + const managementUrl = locators + .get('MANAGEMENT_APP_LOCATOR') + ?.useUrl({ sectionId: 'security', appId: 'api_keys' }); + + return ( + + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useDeploymentDetails() { + const context = useContext(DeploymentDetailsContext); + + if (!context) { + throw new Error( + 'DeploymentDetailsContext is missing. Ensure your component or React root is wrapped with or .' + ); + } + + return context; +} diff --git a/packages/cloud/jest.config.js b/packages/cloud/jest.config.js new file mode 100644 index 0000000000000..174f01cfc1be6 --- /dev/null +++ b/packages/cloud/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/cloud'], +}; diff --git a/packages/cloud/kibana.jsonc b/packages/cloud/kibana.jsonc new file mode 100644 index 0000000000000..e39a0dbe40617 --- /dev/null +++ b/packages/cloud/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/cloud", + "owner": "@elastic/kibana-core" +} diff --git a/packages/cloud/package.json b/packages/cloud/package.json new file mode 100644 index 0000000000000..8e0023dc5c7a3 --- /dev/null +++ b/packages/cloud/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/cloud", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json new file mode 100644 index 0000000000000..c4703bc51cf6c --- /dev/null +++ b/packages/cloud/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + ] +} diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx index e1e43d43ab401..5c3d5bb048737 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx @@ -67,6 +67,7 @@ const buildDefaultContentLinks = ({ defaultMessage: 'Open an issue in GitHub', }), href: docLinks.links.kibana.createGithubIssue, + iconType: 'logoGithub', }, ]; @@ -201,17 +202,40 @@ export class HeaderHelpMenu extends Component { return ( - {defaultContentLinks.map(({ href, title, iconType }, i) => { - const isLast = i === defaultContentLinks.length - 1; - return ( - - - {title} - - {!isLast && } - - ); - })} + {defaultContentLinks.map( + ({ href, title, iconType, onClick: _onClick, dataTestSubj }, i) => { + const isLast = i === defaultContentLinks.length - 1; + + if (href && _onClick) { + throw new Error( + 'Only one of `href` and `onClick` should be provided for the help menu link.' + ); + } + + const hrefProps = href ? { href, target: '_blank' } : {}; + const onClick = () => { + if (!_onClick) return; + _onClick(); + this.closeMenu(); + }; + + return ( + + + {title} + + {!isLast && } + + ); + } + )} ); } diff --git a/packages/core/chrome/core-chrome-browser/src/nav_controls.ts b/packages/core/chrome/core-chrome-browser/src/nav_controls.ts index 39b5d1b3b59b1..22c074862151b 100644 --- a/packages/core/chrome/core-chrome-browser/src/nav_controls.ts +++ b/packages/core/chrome/core-chrome-browser/src/nav_controls.ts @@ -18,8 +18,10 @@ export interface ChromeNavControl { /** @public */ export interface ChromeHelpMenuLink { title: string; - href: string; + href?: string; iconType?: string; + onClick?: () => void; + dataTestSubj?: string; } /** diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/README.md b/packages/kbn-coloring/src/shared_components/color_mapping/README.md new file mode 100644 index 0000000000000..220824ca47820 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/README.md @@ -0,0 +1,87 @@ +# Color Mapping + +This shared component can be used to define a color mapping as an association of one or multiple string values to a color definition. + +This package provides: +- a React component, called `CategoricalColorMapping` that provides a simplified UI (that in general can be hosted in a flyout), that helps the user generate a `ColorMapping.Config` object that descibes the mappings configuration +- a function `getColorFactory` that given a color mapping configuration returns a function that maps a passed category to the corresponding color +- a definition scheme for the color mapping, based on the type `ColorMapping.Config`, that provides an extensible way of describing the link between colors and rules. Collects the minimal information required apply colors based on categories. Together with the `ColorMappingInputData` can be used to get colors in a deterministic way. + + +An example of the configuration is the following: +```ts +const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'auto', + assignments: [ + { + rule: { + type: 'matchExactly', + values: ['']; + }, + color: { + type: 'categorical', + paletteId: 'eui', + colorIndex: 2, + } + } + ], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: 'neutral', + colorIndex: 2 + }, + touched: false, + }, + ], + paletteId: EUIPalette.id, + colorMode: { + type: 'categorical', + }, +}; +``` + +The function `getColorFactory` is a curry function where, given the model, a palette getter, the theme mode (dark/light) and a list of categories, returns a function that can be used to pick the right color based on a given category. + +```ts +function getColorFactory( + model: ColorMapping.Config, + getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette, + isDarkMode: boolean, + data: { + type: 'categories'; + categories: Array; + } +): (category: string | string[]) => Color +``` + + + +A `category` can be in the shape of a plain string or an array of strings. Numbers, MultiFieldKey, IP etc needs to be stringified. + + +The `CategoricalColorMapping` React component has the following props: + +```tsx +function CategoricalColorMapping(props: { + /** The initial color mapping model, usually coming from a the visualization saved object */ + model: ColorMapping.Config; + /** A map of paletteId and palette configuration */ + palettes: Map; + /** A data description of what needs to be colored */ + data: ColorMappingInputData; + /** Theme dark mode */ + isDarkMode: boolean; + /** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; + /** A function called at every change in the model */ + onModelUpdate: (model: ColorMapping.Config) => void; +}) + +``` + +the `onModelUpdate` callback is called everytime a change in the model is applied from within the component. Is not called when the `model` prop is updated. \ No newline at end of file diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx new file mode 100644 index 0000000000000..95f4ff5623ea3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx @@ -0,0 +1,132 @@ +/* + * 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 React, { FC } from 'react'; +import { EuiFlyout, EuiForm } from '@elastic/eui'; +import { ComponentStory } from '@storybook/react'; +import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping'; +import { AVAILABLE_PALETTES } from '../palettes'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping'; + +export default { + title: 'Color Mapping', + component: CategoricalColorMapping, + decorators: [ + (story: Function) => ( + {}} hideCloseButton> + {story()} + + ), + ], +}; + +const Template: ComponentStory> = (args) => ( + +); + +export const Default = Template.bind({}); + +Default.args = { + model: { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignmentMode: 'manual', + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + colorIndex: 0, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + { + type: 'categorical', + colorIndex: 1, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + { + type: 'categorical', + colorIndex: 2, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + ], + sort: 'asc', + }, + assignments: [ + { + rule: { + type: 'matchExactly', + values: ['this is', 'a multi-line combobox that is very long and that will be truncated'], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: ['b', ['double', 'value']], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: ['c'], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: [ + 'this is', + 'a multi-line wrap', + 'combo box', + 'test combo', + '3 lines', + ['double', 'value'], + ], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + ], + }, + isDarkMode: false, + data: { + type: 'categories', + categories: [ + 'a', + 'b', + 'c', + 'd', + 'this is', + 'a multi-line wrap', + 'combo box', + 'test combo', + '3 lines', + ], + }, + + palettes: AVAILABLE_PALETTES, + specialTokens: new Map(), + // eslint-disable-next-line no-console + onModelUpdate: (model) => console.log(model), +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx new file mode 100644 index 0000000000000..fe8374d7dcdcd --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx @@ -0,0 +1,115 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; +import { CategoricalColorMapping, ColorMappingInputData } from './categorical_color_mapping'; +import { AVAILABLE_PALETTES } from './palettes'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from './config/default_color_mapping'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; + +const AUTO_ASSIGN_SWITCH = '[data-test-subj="lns-colorMapping-autoAssignSwitch"]'; +const ASSIGNMENTS_LIST = '[data-test-subj="lns-colorMapping-assignmentsList"]'; +const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`; + +describe('color mapping', () => { + it('load a default color mapping', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['categoryA', 'categoryB'], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + + expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(true); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + dataInput.categories.forEach((category, index) => { + const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes(); + expect(assignment.text()).toEqual(category); + expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(true); + }); + expect(onModelUpdateFn).not.toBeCalled(); + }); + + it('switch to manual assignments', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['categoryA', 'categoryB'], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click'); + expect(onModelUpdateFn).toBeCalledTimes(1); + expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(false); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + dataInput.categories.forEach((category, index) => { + const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes(); + expect(assignment.text()).toEqual(category); + expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(false); + }); + }); + + it('handle special tokens, multi-fields keys and non-trimmed whitespaces', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + const assignment1 = component.find(ASSIGNMENT_ITEM(0)).hostNodes(); + expect(assignment1.text()).toEqual('Other'); + + const assignment2 = component.find(ASSIGNMENT_ITEM(1)).hostNodes(); + expect(assignment2.text()).toEqual(`fieldA${MULTI_FIELD_KEY_SEPARATOR}fieldB`); + + const assignment3 = component.find(ASSIGNMENT_ITEM(2)).hostNodes(); + expect(assignment3.text()).toEqual('(Empty)'); + + const assignment4 = component.find(ASSIGNMENT_ITEM(3)).hostNodes(); + expect(assignment4.text()).toEqual(' with-whitespaces '); + }); +}); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx new file mode 100644 index 0000000000000..290c549684f90 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx @@ -0,0 +1,97 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { type EnhancedStore, configureStore } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { colorMappingReducer, updateModel } from './state/color_mapping'; +import { Container } from './components/container/container'; +import { ColorMapping } from './config'; +import { uiReducer } from './state/ui'; + +/** + * A configuration object that is required to populate correctly the visible categories + * or the ranges in the CategoricalColorMapping component + */ +export type ColorMappingInputData = + | { + type: 'categories'; + /** an ORDERED array of categories rendered in the visualization */ + categories: Array; + } + | { + type: 'ranges'; + min: number; + max: number; + bins: number; + }; + +/** + * The props of the CategoricalColorMapping component + */ +export interface ColorMappingProps { + /** The initial color mapping model, usually coming from a the visualization saved object */ + model: ColorMapping.Config; + /** A map of paletteId and palette configuration */ + palettes: Map; + /** A data description of what needs to be colored */ + data: ColorMappingInputData; + /** Theme dark mode */ + isDarkMode: boolean; + /** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; + /** A function called at every change in the model */ + onModelUpdate: (model: ColorMapping.Config) => void; +} + +/** + * The React component for mapping categorical values to colors + */ +export class CategoricalColorMapping extends React.Component { + store: EnhancedStore<{ colorMapping: ColorMapping.Config }>; + unsubscribe: () => void; + constructor(props: ColorMappingProps) { + super(props); + // configure the store at mount time + this.store = configureStore({ + preloadedState: { + colorMapping: props.model, + }, + reducer: { + colorMapping: colorMappingReducer, + ui: uiReducer, + }, + }); + // subscribe to store changes to update external tools + this.unsubscribe = this.store.subscribe(() => { + this.props.onModelUpdate(this.store.getState().colorMapping); + }); + } + componentWillUnmount() { + this.unsubscribe(); + } + componentDidUpdate(prevProps: Readonly) { + if (!isEqual(prevProps.model, this.props.model)) { + this.store.dispatch(updateModel(this.props.model)); + } + } + render() { + const { palettes, data, isDarkMode, specialTokens } = this.props; + return ( + + + + ); + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts new file mode 100644 index 0000000000000..93896394daf41 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts @@ -0,0 +1,294 @@ +/* + * 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 { + DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_NEUTRAL_PALETTE_INDEX, +} from '../config/default_color_mapping'; +import { getColorFactory } from './color_handling'; +import { getPalette, AVAILABLE_PALETTES } from '../palettes'; +import { + EUIAmsterdamColorBlindPalette, + EUI_AMSTERDAM_PALETTE_COLORS, +} from '../palettes/eui_amsterdam'; +import { NeutralPalette, NEUTRAL_COLOR_DARK, NEUTRAL_COLOR_LIGHT } from '../palettes/neutral'; +import { toHex } from './color_math'; + +import { ColorMapping } from '../config'; + +describe('Color mapping - color generation', () => { + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + it('returns EUI light colors from default config', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, { + type: 'categories', + categories: ['catA', 'catB', 'catC'], + }); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('returns max number of colors defined in palette, use other color otherwise', () => { + const twoColorPalette: ColorMapping.CategoricalPalette = { + id: 'twoColors', + name: 'twoColors', + colorCount: 2, + type: 'categorical', + getColor(valueInRange, isDarkMode) { + return ['red', 'blue'][valueInRange]; + }, + }; + + const simplifiedGetPaletteGn = getPalette( + new Map([[twoColorPalette.id, twoColorPalette]]), + NeutralPalette + ); + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + paletteId: twoColorPalette.id, + }, + simplifiedGetPaletteGn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3', 'cat4'], + } + ); + expect(colorFactory('cat1')).toBe('#ff0000'); + expect(colorFactory('cat2')).toBe('#0000ff'); + // return a palette color only up to the max number of color in the palette + expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + // currently there is no difference in the two colors, but this could change in the future + // this test will catch the change + it('returns EUI dark colors from default config', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, { + type: 'categories', + categories: ['catA', 'catB', 'catC'], + }); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, { + type: 'categories', + categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '], + }); + expect(colorFactory('__other__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('__empty__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + expect(colorFactory(' with-whitespaces ')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[3]); + }); + + it('ignores configured assignments in auto mode', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignments: [ + { + color: { type: 'colorCode', colorCode: 'red' }, + rule: { type: 'matchExactly', values: ['assignmentToIgnore'] }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['catA', 'catB', 'assignmentToIgnore'], + } + ); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('assignmentToIgnore')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + }); + + it('color with auto rule are assigned in order of the configured data input', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignmentMode: 'manual', + assignments: [ + { + color: { type: 'colorCode', colorCode: 'red' }, + rule: { type: 'auto' }, + touched: false, + }, + { + color: { type: 'colorCode', colorCode: 'blue' }, + rule: { type: 'matchExactly', values: ['blueCat'] }, + touched: false, + }, + { + color: { type: 'colorCode', colorCode: 'green' }, + rule: { type: 'auto' }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['blueCat', 'redCat', 'greenCat'], + } + ); + // this matches exactly + expect(colorFactory('blueCat')).toBe('blue'); + // this matches with the first availabe "auto" rule + expect(colorFactory('redCat')).toBe('red'); + // this matches with the second availabe "auto" rule + expect(colorFactory('greenCat')).toBe('green'); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('returns sequential gradient colors from darker to lighter [desc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + // this matches exactly with the initial step selected + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + expect(toHex(colorFactory('cat3'))).toBe('#cce8e0'); + }); + + it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'asc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + // this matches exactly with the initial step selected + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + }); + + it('returns 2 colors gradient [desc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 2, + touched: false, + }, + ], + sort: 'desc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green + expect(toHex(colorFactory('cat2'))).toBe('#a4908f'); // red gray green + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink + }); + + it('returns divergent gradient [asc, darkMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + { type: 'categorical', paletteId: NeutralPalette.id, colorIndex: 0, touched: false }, + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 2, + touched: false, + }, + ], + sort: 'asc', // testing in ascending order + }, + }, + getPaletteFn, + true, // testing in dark mode + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink + expect(toHex(colorFactory('cat2'))).toBe(NEUTRAL_COLOR_DARK[0]); // NEUTRAL LIGHT GRAY + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green + expect(toHex(colorFactory('not available cat'))).toBe( + toHex(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]) + ); // check the other + }); +}); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts new file mode 100644 index 0000000000000..795f94b740e9b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts @@ -0,0 +1,164 @@ +/* + * 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 chroma from 'chroma-js'; +import { ColorMapping } from '../config'; +import { changeAlpha, combineColors, getValidColor } from './color_math'; +import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories'; +import { getPalette } from '../palettes'; +import { ColorMappingInputData } from '../categorical_color_mapping'; +import { ruleMatch } from './rule_matching'; +import { GradientColorMode } from '../config/types'; + +export function getAssignmentColor( + colorMode: ColorMapping.Config['colorMode'], + color: ColorMapping.Config['assignments'][number]['color'], + getPaletteFn: ReturnType, + isDarkMode: boolean, + index: number, + total: number +) { + switch (color.type) { + case 'colorCode': + case 'categorical': + return getColor(color, getPaletteFn, isDarkMode); + case 'gradient': { + if (colorMode.type === 'categorical') { + return 'red'; + } + const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); + return total === 0 ? 'red' : total === 1 ? colorScale(0) : colorScale(index / (total - 1)); + } + } +} + +export function getColor( + color: ColorMapping.ColorCode | ColorMapping.CategoricalColor, + getPaletteFn: ReturnType, + isDarkMode: boolean +) { + return color.type === 'colorCode' + ? color.colorCode + : getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex(); +} + +export function getColorFactory( + model: ColorMapping.Config, + getPaletteFn: ReturnType, + isDarkMode: boolean, + data: ColorMappingInputData +): (category: string | string[]) => string { + const palette = getPaletteFn(model.paletteId); + // generate on-the-fly assignments in auto-mode based on current data. + // This simplify the code by always using assignments, even if there is no real static assigmnets + const assignments = + model.assignmentMode === 'auto' + ? generateAutoAssignmentsForCategories(data, palette, model.colorMode) + : model.assignments; + + // find auto-assigned colors + const autoAssignedColors = + data.type === 'categories' + ? assignments.filter((a) => { + return ( + a.rule.type === 'auto' || (a.rule.type === 'matchExactly' && a.rule.values.length === 0) + ); + }) + : []; + + // find all categories that doesn't match with an assignment + const nonAssignedCategories = + data.type === 'categories' + ? data.categories.filter((category) => { + return !assignments.some(({ rule }) => ruleMatch(rule, category)); + }) + : []; + + return (category: string | string[]) => { + if (typeof category === 'string' || Array.isArray(category)) { + const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category); + + // return color for a non assigned category + if (nonAssignedCategoryIndex > -1) { + if (nonAssignedCategoryIndex < autoAssignedColors.length) { + const autoAssignmentIndex = assignments.findIndex( + (d) => d === autoAssignedColors[nonAssignedCategoryIndex] + ); + return getAssignmentColor( + model.colorMode, + autoAssignedColors[nonAssignedCategoryIndex].color, + getPaletteFn, + isDarkMode, + autoAssignmentIndex, + assignments.length + ); + } + // if no auto-assign color rule/color is available then use the other color + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } + + // find the assignment where the category matches the rule + const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { + return ruleMatch(rule, category); + }); + + // return the assigned color + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + model.colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); + } + // if no assign color rule/color is available then use the other color + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } else { + const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { + return ruleMatch(rule, category); + }); + + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + model.colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); + } + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } + }; +} + +export function getGradientColorScale( + colorMode: GradientColorMode, + getPaletteFn: ReturnType, + isDarkMode: boolean +): (value: number) => string { + const steps = + colorMode.steps.length === 1 + ? [ + getColor(colorMode.steps[0], getPaletteFn, isDarkMode), + combineColors( + changeAlpha(getColor(colorMode.steps[0], getPaletteFn, isDarkMode), 0.3), + isDarkMode ? 'black' : 'white' + ), + ] + : colorMode.steps.map((d) => getColor(d, getPaletteFn, isDarkMode)); + steps.sort(() => (colorMode.sort === 'asc' ? -1 : 1)); + const scale = chroma.scale(steps).mode('lab'); + return (value: number) => scale(value).hex(); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts new file mode 100644 index 0000000000000..eb9e57d52af55 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts @@ -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 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 chroma from 'chroma-js'; + +export function getValidColor(color: string): chroma.Color { + try { + return chroma(color); + } catch { + return chroma('red'); + } +} + +export function hasEnoughContrast(color: string, isDark: boolean, threshold = 4.5) { + return chroma.contrast(getValidColor(color), isDark ? 'black' : 'white') >= threshold; +} + +export function changeAlpha(color: string, alpha: number) { + const [r, g, b] = getValidColor(color).rgb(); + return `rgba(${r},${g},${b},${alpha})`; +} + +export function toHex(color: string) { + return getValidColor(color).hex().toLowerCase(); +} + +export function isSameColor(color1: string, color2: string) { + return toHex(color1) === toHex(color2); +} + +/** + * Blend a foreground (fg) color with a background (bg) color + */ +export function combineColors(fg: string, bg: string): string { + const [fgR, fgG, fgB, fgA] = getValidColor(fg).rgba(); + const [bgR, bgG, bgB, bgA] = getValidColor(bg).rgba(); + + // combine colors only if foreground has transparency + if (fgA === 1) { + return chroma.rgb(fgR, fgG, fgB).hex(); + } + + // For reference on alpha calculations: + // https://en.wikipedia.org/wiki/Alpha_compositing + const alpha = fgA + bgA * (1 - fgA); + + if (alpha === 0) { + return '#00000000'; + } + + const r = Math.round((fgR * fgA + bgR * bgA * (1 - fgA)) / alpha); + const g = Math.round((fgG * fgA + bgG * bgA * (1 - fgA)) / alpha); + const b = Math.round((fgB * fgA + bgB * bgA * (1 - fgA)) / alpha); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts new file mode 100644 index 0000000000000..7557644154a52 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts @@ -0,0 +1,46 @@ +/* + * 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 { ColorMapping } from '../config'; + +export function ruleMatch( + rule: ColorMapping.Config['assignments'][number]['rule'], + value: string | number | string[] +) { + switch (rule.type) { + case 'matchExactly': + if (Array.isArray(value)) { + return rule.values.some( + (v) => + Array.isArray(v) && v.length === value.length && v.every((part, i) => part === value[i]) + ); + } + return rule.values.includes(`${value}`); + case 'matchExactlyCI': + return rule.values.some((d) => d.toLowerCase() === `${value}`); + case 'range': + // TODO: color by value not yet possible in all charts in elastic-charts + return typeof value === 'number' ? rangeMatch(rule, value) : false; + default: + return false; + } +} + +export function rangeMatch(rule: ColorMapping.RuleRange, value: number) { + return ( + (rule.min === rule.max && rule.min === value) || + ((rule.minInclusive ? value >= rule.min : value > rule.min) && + (rule.maxInclusive ? value <= rule.max : value < rule.max)) + ); +} + +// TODO: move in some data/table related package +export const SPECIAL_TOKENS_STRING_CONVERTION = new Map([ + ['__other__', 'Other'], + ['', '(empty)'], +]); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx new file mode 100644 index 0000000000000..896f2ea392884 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx @@ -0,0 +1,150 @@ +/* + * 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 React from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + removeAssignment, + updateAssignmentColor, + updateAssignmentRule, +} from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { Range } from './range'; +import { Match } from './match'; +import { getPalette } from '../../palettes'; + +import { ColorMappingInputData } from '../../categorical_color_mapping'; +import { ColorSwatch } from '../color_picker/color_swatch'; + +export function Assignment({ + data, + assignment, + disableDelete, + index, + total, + canPickColor, + editable, + palette, + colorMode, + getPaletteFn, + isDarkMode, + specialTokens, + assignmentValuesCounter, +}: { + data: ColorMappingInputData; + index: number; + total: number; + colorMode: ColorMapping.Config['colorMode']; + assignment: ColorMapping.Config['assignments'][number]; + disableDelete: boolean; + palette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + canPickColor: boolean; + editable: boolean; + isDarkMode: boolean; + specialTokens: Map; + assignmentValuesCounter: Map; +}) { + const dispatch = useDispatch(); + + return ( + + + { + dispatch(updateAssignmentColor({ assignmentIndex: index, color })); + }} + /> + + + {assignment.rule.type === 'auto' || + assignment.rule.type === 'matchExactly' || + assignment.rule.type === 'matchExactlyCI' ? ( + ) => { + dispatch( + updateAssignmentRule({ + assignmentIndex: index, + rule: values.length === 0 ? { type: 'auto' } : { type: 'matchExactly', values }, + }) + ); + }} + assignmentValuesCounter={assignmentValuesCounter} + /> + ) : assignment.rule.type === 'range' ? ( + { + const rule: ColorMapping.RuleRange = { + type: 'range', + min, + max, + minInclusive, + maxInclusive, + }; + dispatch(updateAssignmentRule({ assignmentIndex: index, rule })); + }} + /> + ) : null} + + + dispatch(removeAssignment(index))} + aria-label={i18n.translate( + 'coloring.colorMapping.assignments.deleteAssignmentButtonLabel', + { + defaultMessage: 'Delete this assignment', + } + )} + color="danger" + css={ + !disableDelete + ? css` + color: ${euiThemeVars.euiTextSubduedColor}; + transition: ${euiThemeVars.euiAnimSpeedFast} ease-in-out; + transition-property: color; + &:hover, + &:focus { + color: ${euiThemeVars.euiColorDangerText}; + } + ` + : undefined + } + /> + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx new file mode 100644 index 0000000000000..43c5583191cf3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx @@ -0,0 +1,101 @@ +/* + * 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 React from 'react'; +import { EuiComboBox, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { ColorMapping } from '../../config'; + +export const Match: React.FC<{ + index: number; + editable: boolean; + rule: + | ColorMapping.RuleAuto + | ColorMapping.RuleMatchExactly + | ColorMapping.RuleMatchExactlyCI + | ColorMapping.RuleRegExp; + updateValue: (values: Array) => void; + options: Array; + specialTokens: Map; + assignmentValuesCounter: Map; +}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => { + const selectedOptions = + rule.type === 'auto' + ? [] + : typeof rule.values === 'string' + ? [ + { + label: rule.values, + value: rule.values, + append: + (assignmentValuesCounter.get(rule.values) ?? 0) > 1 ? ( + + ) : undefined, + }, + ] + : rule.values.map((value) => { + const ruleValues = Array.isArray(value) ? value : [value]; + return { + label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR), + value, + append: + (assignmentValuesCounter.get(value) ?? 0) > 1 ? ( + + ) : undefined, + }; + }); + + const convertedOptions = options.map((value) => { + const ruleValues = Array.isArray(value) ? value : [value]; + return { + label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR), + value, + }; + }); + + return ( + + { + updateValue( + changedOptions.reduce>((acc, option) => { + if (option.value !== undefined) { + acc.push(option.value); + } + return acc; + }, []) + ); + }} + onCreateOption={(label) => { + if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) { + updateValue([...selectedOptions, { label, value: label }].map((d) => d.value)); + } + }} + isClearable={false} + compressed + /> + + ); +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx new file mode 100644 index 0000000000000..70f2cf49609e0 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx @@ -0,0 +1,67 @@ +/* + * 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 React from 'react'; +import { EuiButtonEmpty, EuiFieldNumber, EuiFlexItem } from '@elastic/eui'; +import { ColorMapping } from '../../config'; + +export const Range: React.FC<{ + rule: ColorMapping.RuleRange; + editable: boolean; + updateValue: (min: number, max: number, minInclusive: boolean, maxInclusive: boolean) => void; +}> = ({ rule, updateValue, editable }) => { + const minValid = rule.min <= rule.max; + const maxValid = rule.max >= rule.min; + + return ( + <> + + updateValue(rule.min, rule.max, !rule.minInclusive, rule.maxInclusive)} + > + {rule.minInclusive ? 'GTE' : 'GT'} + + } + placeholder="min" + value={rule.min} + isInvalid={!minValid} + disabled={!editable} + onChange={(e) => + updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive) + } + aria-label="The min value" + /> + + + updateValue(rule.min, rule.max, rule.minInclusive, !rule.maxInclusive)} + > + {rule.maxInclusive ? 'LTE' : 'LT'} + + } + placeholder="max" + disabled={!editable} + value={rule.max} + onChange={(e) => + updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive) + } + aria-label="The max value" + /> + + + ); +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx new file mode 100644 index 0000000000000..29ede59e37f41 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { ColorSwatch } from '../color_picker/color_swatch'; +import { updateSpecialAssignmentColor } from '../../state/color_mapping'; + +export function SpecialAssignment({ + assignment, + index, + palette, + getPaletteFn, + isDarkMode, + total, +}: { + isDarkMode: boolean; + index: number; + assignment: ColorMapping.Config['specialAssignments'][number]; + palette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + total: number; +}) { + const dispatch = useDispatch(); + const canPickColor = true; + return ( + + + { + dispatch( + updateSpecialAssignmentColor({ + assignmentIndex: index, + color, + }) + ); + }} + /> + + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx new file mode 100644 index 0000000000000..e1e8a08aa6b22 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiPopoverTitle, + EuiTab, + EuiTabs, + EuiTitle, + EuiHorizontalRule, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { PaletteColors } from './palette_colors'; +import { RGBPicker } from './rgb_picker'; +import { NeutralPalette } from '../../palettes/neutral'; + +export function ColorPicker({ + palette, + getPaletteFn, + color, + close, + selectColor, + isDarkMode, + deleteStep, +}: { + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + getPaletteFn: ReturnType; + palette: ColorMapping.CategoricalPalette; + isDarkMode: boolean; + close: () => void; + selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; + deleteStep?: () => void; +}) { + const [tab, setTab] = useState( + color.type === 'categorical' && + (color.paletteId === palette.id || color.paletteId === NeutralPalette.id) + ? 'palette' + : 'custom' + ); + + return ( +
+ + + setTab('palette')} isSelected={tab === 'palette'}> + + + {i18n.translate('coloring.colorMapping.colorPicker.paletteTabLabel', { + defaultMessage: 'Colors', + })} + + + + setTab('custom')} isSelected={tab === 'custom'}> + + + {i18n.translate('coloring.colorMapping.colorPicker.customTabLabel', { + defaultMessage: 'Custom', + })} + + + + + + {tab === 'palette' ? ( + + ) : ( + + )} + {deleteStep ? ( + <> + + { + close(); + deleteStep(); + }} + style={{ paddingBottom: 8 }} + > + {i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', { + defaultMessage: 'Remove color step', + })} + + + ) : null} +
+ ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx new file mode 100644 index 0000000000000..8ddc56d2476c7 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx @@ -0,0 +1,184 @@ +/* + * 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 { + EuiColorPickerSwatch, + EuiPopover, + euiShadowSmall, + isColorDark, + useEuiTheme, +} from '@elastic/eui'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { ColorPicker } from './color_picker'; +import { getAssignmentColor } from '../../color/color_handling'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { removeGradientColorStep } from '../../state/color_mapping'; + +import { selectColorPickerVisibility } from '../../state/selectors'; +import { colorPickerVisibility, hideColorPickerVisibility } from '../../state/ui'; +import { getValidColor } from '../../color/color_math'; + +interface ColorPickerSwatchProps { + colorMode: ColorMapping.Config['colorMode']; + assignmentColor: + | ColorMapping.Config['assignments'][number]['color'] + | ColorMapping.Config['specialAssignments'][number]['color']; + getPaletteFn: ReturnType; + canPickColor: boolean; + index: number; + total: number; + palette: ColorMapping.CategoricalPalette; + onColorChange: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; + swatchShape: 'square' | 'round'; + isDarkMode: boolean; + forType: 'assignment' | 'specialAssignment' | 'gradient'; +} +export const ColorSwatch = ({ + colorMode, + assignmentColor, + getPaletteFn, + canPickColor, + index, + total, + palette, + onColorChange, + swatchShape, + isDarkMode, + forType, +}: ColorPickerSwatchProps) => { + const colorPickerState = useSelector(selectColorPickerVisibility); + const dispatch = useDispatch(); + const colorPickerVisible = + colorPickerState.index === index && + colorPickerState.type === forType && + colorPickerState.visibile; + const colorHex = getAssignmentColor( + colorMode, + assignmentColor, + getPaletteFn, + isDarkMode, + index, + total + ); + const colorIsDark = isColorDark(...getValidColor(colorHex).rgb()); + const euiTheme = useEuiTheme(); + return canPickColor && assignmentColor.type !== 'gradient' ? ( + dispatch(hideColorPickerVisibility())} + anchorPosition="upLeft" + button={ + swatchShape === 'round' ? ( + + ); +} + +function ColorStop({ + colorMode, + step, + index, + currentPalette, + getPaletteFn, + isDarkMode, +}: { + colorMode: ColorMapping.GradientColorMode; + step: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + index: number; + currentPalette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + return ( + { + dispatch( + updateGradientColorStep({ + index, + color, + }) + ); + }} + forType="gradient" + /> + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx new file mode 100644 index 0000000000000..a15bdca26ee1c --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx @@ -0,0 +1,263 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + EuiButtonGroup, + EuiColorPalettePicker, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ScaleCategoricalIcon } from './scale_categorical'; +import { ScaleSequentialIcon } from './scale_sequential'; + +import { RootState, updatePalette } from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { updateAssignmentsPalette, updateColorModePalette } from '../../config/assignments'; +import { getPalette } from '../../palettes'; + +export function PaletteSelector({ + palettes, + getPaletteFn, + isDarkMode, +}: { + getPaletteFn: ReturnType; + palettes: Map; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode); + const model = useSelector((state: RootState) => state.colorMapping); + + const { paletteId } = model; + + const switchPaletteFn = useCallback( + (selectedPaletteId: string, preserveColorChanges: boolean) => { + dispatch( + updatePalette({ + paletteId: selectedPaletteId, + assignments: updateAssignmentsPalette( + model.assignments, + model.assignmentMode, + model.colorMode, + selectedPaletteId, + getPaletteFn, + preserveColorChanges + ), + colorMode: updateColorModePalette( + model.colorMode, + selectedPaletteId, + preserveColorChanges + ), + }) + ); + }, + [getPaletteFn, model, dispatch] + ); + + const updateColorMode = useCallback( + (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => { + const updatedColorMode: ColorMapping.Config['colorMode'] = + type === 'gradient' + ? { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + } + : { type: 'categorical' }; + + const assignments = updateAssignmentsPalette( + model.assignments, + model.assignmentMode, + updatedColorMode, + paletteId, + getPaletteFn, + preserveColorChanges + ); + dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode })); + }, + [getPaletteFn, model, dispatch, paletteId] + ); + + const [preserveModalPaletteId, setPreserveModalPaletteId] = useState(null); + + const preserveChangesModal = + preserveModalPaletteId !== null ? ( + { + if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, true); + setPreserveModalPaletteId(null); + }} + onConfirm={() => { + if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, false); + setPreserveModalPaletteId(null); + }} + confirmButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.discardButton', { + defaultMessage: 'Discard changes', + })} + cancelButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.preserveButton', { + defaultMessage: 'Preserve changes', + })} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

+ {i18n.translate('coloring.colorMapping.colorChangesModal.switchPaletteDescription', { + defaultMessage: 'Switching palette will discard all your custom color changes', + })} +

+
+ ) : null; + + const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>( + null + ); + + const colorScaleModal = + colorScaleModalId !== null ? ( + { + setColorScaleModalId(null); + }} + onConfirm={() => { + if (colorScaleModalId) updateColorMode(colorScaleModalId, false); + setColorScaleModalId(null); + }} + cancelButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.goBackButtonLabel', + { + defaultMessage: 'Go back', + } + )} + confirmButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.discardButtonLabel', + { + defaultMessage: 'Discard changes', + } + )} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

+ {colorScaleModalId === 'categorical' + ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', { + defaultMessage: `Switching to a categorical mode will discard all your custom color changes`, + }) + : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', { + defaultMessage: `Switching to a sequential mode will discard all your custom color changes`, + })} +

+
+ ) : null; + + return ( + <> + {preserveChangesModal} + {colorScaleModal} + + + + d.name !== 'Neutral') + .map((palette) => ({ + 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`, + value: palette.id, + title: palette.name, + palette: Array.from({ length: palette.colorCount }, (_, i) => { + return palette.getColor(i, isDarkMode); + }), + type: 'fixed', + }))} + onChange={(selectedPaletteId) => { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && + model.colorMode.steps.some((a) => a.touched); + if (hasChanges || hasGradientChanges) { + setPreserveModalPaletteId(selectedPaletteId); + } else { + switchPaletteFn(selectedPaletteId, false); + } + }} + valueOfSelected={model.paletteId} + selectionDisplay={'palette'} + compressed={true} + /> + + + + + { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && + model.colorMode.steps.some((a) => a.touched); + + if (hasChanges || hasGradientChanges) { + setColorScaleModalId(id as 'gradient' | 'categorical'); + } else { + updateColorMode(id as 'gradient' | 'categorical', false); + } + }} + isIconOnly + /> + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx new file mode 100644 index 0000000000000..f71ed74485365 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx @@ -0,0 +1,17 @@ +/* + * 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 React from 'react'; + +export function ScaleCategoricalIcon() { + return ( + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx new file mode 100644 index 0000000000000..ec245f471f307 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx @@ -0,0 +1,20 @@ +/* + * 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 React from 'react'; + +export function ScaleSequentialIcon() { + return ( + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts new file mode 100644 index 0000000000000..97c4d17c35e4d --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts @@ -0,0 +1,65 @@ +/* + * 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 { ColorMapping } from '.'; +import { ColorMappingInputData } from '../categorical_color_mapping'; +import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; + +export function generateAutoAssignmentsForCategories( + data: ColorMappingInputData, + palette: ColorMapping.CategoricalPalette, + colorMode: ColorMapping.Config['colorMode'] +): ColorMapping.Config['assignments'] { + const isCategorical = colorMode.type === 'categorical'; + + const maxColorAssignable = data.type === 'categories' ? data.categories.length : data.bins; + + const assignableColors = isCategorical + ? Math.min(palette.colorCount, maxColorAssignable) + : Math.min(MAX_ASSIGNABLE_COLORS, maxColorAssignable); + + const autoRules: Array = + data.type === 'categories' + ? data.categories.map((c) => ({ type: 'matchExactly', values: [c] })) + : Array.from({ length: data.bins }, (d, i) => { + const step = (data.max - data.min) / data.bins; + return { + type: 'range', + min: data.max - i * step - step, + max: data.max - i * step, + minInclusive: true, + maxInclusive: false, + }; + }); + + const assignments = autoRules + .slice(0, assignableColors) + .map((rule, colorIndex) => { + if (isCategorical) { + return { + rule, + color: { + type: 'categorical', + paletteId: palette.id, + colorIndex, + }, + touched: false, + }; + } else { + return { + rule, + color: { + type: 'gradient', + }, + touched: false, + }; + } + }); + + return assignments; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts new file mode 100644 index 0000000000000..701baa1b1710b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ColorMapping } from '.'; +import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; +import { getPalette, NeutralPalette } from '../palettes'; +import { DEFAULT_NEUTRAL_PALETTE_INDEX } from './default_color_mapping'; + +export function updateAssignmentsPalette( + assignments: ColorMapping.Config['assignments'], + assignmentMode: ColorMapping.Config['assignmentMode'], + colorMode: ColorMapping.Config['colorMode'], + paletteId: string, + getPaletteFn: ReturnType, + preserveColorChanges: boolean +): ColorMapping.Config['assignments'] { + const palette = getPaletteFn(paletteId); + const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; + return assignmentMode === 'auto' + ? [] + : assignments.map(({ rule, color, touched }, index) => { + if (preserveColorChanges && touched) { + return { rule, color, touched }; + } else { + const newColor: ColorMapping.Config['assignments'][number]['color'] = + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: index < maxColors ? paletteId : NeutralPalette.id, + colorIndex: index < maxColors ? index : 0, + } + : { type: 'gradient' }; + return { + rule, + color: newColor, + touched: false, + }; + } + }); +} + +export function updateColorModePalette( + colorMode: ColorMapping.Config['colorMode'], + paletteId: string, + preserveColorChanges: boolean +): ColorMapping.Config['colorMode'] { + return colorMode.type === 'categorical' + ? colorMode + : { + type: 'gradient', + steps: colorMode.steps.map((step, stepIndex) => { + return preserveColorChanges + ? step + : { type: 'categorical', paletteId, colorIndex: stepIndex, touched: false }; + }), + sort: colorMode.sort, + }; +} + +export function getUnusedColorForNewAssignment( + palette: ColorMapping.CategoricalPalette, + colorMode: ColorMapping.Config['colorMode'], + assignments: ColorMapping.Config['assignments'] +): ColorMapping.Config['assignments'][number]['color'] { + if (colorMode.type === 'categorical') { + // TODO: change the type of color assignment depending on palette + // compute the next unused color index in the palette. + const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; + const colorIndices = new Set(Array.from({ length: maxColors }, (d, i) => i)); + assignments.forEach(({ color }) => { + if (color.type === 'categorical' && color.paletteId === palette.id) { + colorIndices.delete(color.colorIndex); + } + }); + const paletteForNextUnusedColorIndex = colorIndices.size > 0 ? palette.id : NeutralPalette.id; + const nextUnusedColorIndex = + colorIndices.size > 0 ? [...colorIndices][0] : DEFAULT_NEUTRAL_PALETTE_INDEX; + return { + type: 'categorical', + paletteId: paletteForNextUnusedColorIndex, + colorIndex: nextUnusedColorIndex, + }; + } else { + return { type: 'gradient' }; + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts new file mode 100644 index 0000000000000..e4005770b2883 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts @@ -0,0 +1,79 @@ +/* + * 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 { ColorMapping } from '.'; +import { AVAILABLE_PALETTES, getPalette } from '../palettes'; +import { EUIAmsterdamColorBlindPalette } from '../palettes/eui_amsterdam'; +import { NeutralPalette } from '../palettes/neutral'; +import { getColor, getGradientColorScale } from '../color/color_handling'; + +export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1; + +/** + * The default color mapping used in Kibana, starts with the EUI color palette + */ +export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'auto', + assignments: [], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + touched: false, + }, + ], + paletteId: EUIAmsterdamColorBlindPalette.id, + colorMode: { + type: 'categorical', + }, +}; + +export function getPaletteColors( + isDarkMode: boolean, + colorMappings?: ColorMapping.Config +): string[] { + const colorMappingModel = colorMappings ?? { ...DEFAULT_COLOR_MAPPING_CONFIG }; + const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(colorMappingModel.paletteId); + return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); +} + +export function getColorsFromMapping( + isDarkMode: boolean, + colorMappings?: ColorMapping.Config +): string[] { + const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } = + colorMappings ?? { + ...DEFAULT_COLOR_MAPPING_CONFIG, + }; + + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + if (colorMode.type === 'gradient') { + const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); + return Array.from({ length: 6 }, (d, i) => colorScale(i / 6)); + } else { + const palette = getPaletteFn(paletteId); + if (assignmentMode === 'auto') { + return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); + } else { + return [ + ...assignments.map((a) => { + return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode); + }), + ...specialAssignments.map((a) => { + return getColor(a.color, getPaletteFn, isDarkMode); + }), + ].filter((color) => color !== ''); + } + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts new file mode 100644 index 0000000000000..e75687596789e --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * as ColorMapping from './types'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts new file mode 100644 index 0000000000000..59cb18435112d --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts @@ -0,0 +1,153 @@ +/* + * 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. + */ + +/** + * A color specified as a CSS color datatype (rgb/a,hex,keywords,lab,lch etc) + */ +export interface ColorCode { + type: 'colorCode'; + colorCode: string; +} + +/** + * An index specified categorical color, coming from paletteId + */ +export interface CategoricalColor { + type: 'categorical'; + paletteId: string; + colorIndex: number; +} + +/** + * Specify that the Color in an Assignment needs to be taken from a gradient defined in the `Config.colorMode` + */ +export interface GradientColor { + type: 'gradient'; +} + +/** + * A special rule that match automatically, in order, all the categories that are not matching a specified rule + */ +export interface RuleAuto { + /* tag */ + type: 'auto'; +} +/** + * A rule that match exactly, case sensitive, with the provided strings + */ +export interface RuleMatchExactly { + /* tag */ + type: 'matchExactly'; + values: Array; +} + +/** + * A Match rule to match the values case insensitive + * @ignore not used yet + */ +export interface RuleMatchExactlyCI { + /* tag */ + type: 'matchExactlyCI'; + values: string[]; +} + +/** + * A range rule, not used yet, but can be used for numerical data assignments + */ +export interface RuleRange { + /* tag */ + type: 'range'; + /** + * The min value of the range + */ + min: number; + /** + * The max value of the range + */ + max: number; + /** + * `true` if the range is left-closed (the `min` value is considered within the range), false otherwise (only values that are + * greater than the `min` are considered within the range) + */ + minInclusive: boolean; + /** + * `true` if the range is right-closed (the `max` value is considered within the range), false otherwise (only values less than + * the `max` are considered within the range) + */ + maxInclusive: boolean; +} +/** + * Regex rule. + * @ignore not used yet + */ +export interface RuleRegExp { + /* tag */ + type: 'regex'; + /** + * TODO: not sure how we can store a regexp + */ + values: string; +} + +/** + * A specific catch-everything-else rule + */ +export interface RuleOthers { + /* tag */ + type: 'other'; +} + +/** + * An assignment is the connection link between a rule and a color + */ +export interface Assignment { + /** + * Describe the rule used to assign the color. + */ + rule: R; + /** + * The color definition + */ + color: C; + + /** + * Specify if the color was changed from the original one + * TODO: rename + */ + touched: boolean; +} + +export interface CategoricalColorMode { + type: 'categorical'; +} +export interface GradientColorMode { + type: 'gradient'; + steps: Array<(CategoricalColor | ColorCode) & { touched: boolean }>; + sort: 'asc' | 'desc'; +} + +export interface Config { + paletteId: string; + colorMode: CategoricalColorMode | GradientColorMode; + assignmentMode: 'auto' | 'manual'; + assignments: Array< + Assignment< + RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp, + CategoricalColor | ColorCode | GradientColor + > + >; + specialAssignments: Array>; +} + +export interface CategoricalPalette { + id: string; + name: string; + type: 'categorical'; + colorCount: number; + getColor: (valueInRange: number, isDarkMode: boolean) => string; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts new file mode 100644 index 0000000000000..1b49a2c6a8bf3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { CategoricalColorMapping, type ColorMappingProps } from './categorical_color_mapping'; +export type { ColorMappingInputData } from './categorical_color_mapping'; +export type { ColorMapping } from './config'; +export * from './palettes'; +export * from './color/color_handling'; +export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching'; +export { + DEFAULT_COLOR_MAPPING_CONFIG, + getPaletteColors, + getColorsFromMapping, +} from './config/default_color_mapping'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts new file mode 100644 index 0000000000000..d93440c5ac5e4 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ColorMapping } from '../config'; + +export const ELASTIC_BRAND_PALETTE_COLORS = [ + '#20377d', + '#7de2d1', + '#ff957d', + '#f04e98', + '#0077cc', + '#fec514', +]; + +export const ElasticBrandPalette: ColorMapping.CategoricalPalette = { + id: 'elastic_brand_2023', + name: 'Elastic Brand', + colorCount: ELASTIC_BRAND_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return ELASTIC_BRAND_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts new file mode 100644 index 0000000000000..ec48793e12819 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ColorMapping } from '../config'; + +export const EUI_AMSTERDAM_PALETTE_COLORS = [ + '#54b399', + '#6092c0', + '#d36086', + '#9170b8', + '#ca8eae', + '#d6bf57', + '#b9a888', + '#da8b45', + '#aa6556', + '#e7664c', +]; + +export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = { + id: 'eui_amsterdam_color_blind', + name: 'Default', + colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts new file mode 100644 index 0000000000000..340bbd32f0279 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts @@ -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 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 { ColorMapping } from '../config'; +import { ElasticBrandPalette } from './elastic_brand'; +import { EUIAmsterdamColorBlindPalette } from './eui_amsterdam'; +import { KibanaV7LegacyPalette } from './kibana_legacy'; +import { NeutralPalette } from './neutral'; + +export const AVAILABLE_PALETTES = new Map([ + [EUIAmsterdamColorBlindPalette.id, EUIAmsterdamColorBlindPalette], + [ElasticBrandPalette.id, ElasticBrandPalette], + [KibanaV7LegacyPalette.id, KibanaV7LegacyPalette], + [NeutralPalette.id, NeutralPalette], +]); + +/** + * This function should be instanciated once at the root of the component with the available palettes and + * a choosed default one and shared across components to keep a single point of truth of the available palettes and the default + * one. + */ +export function getPalette( + palettes: Map, + defaultPalette: ColorMapping.CategoricalPalette +): (paletteId: string) => ColorMapping.CategoricalPalette { + return (paletteId) => palettes.get(paletteId) ?? defaultPalette; +} + +export * from './eui_amsterdam'; +export * from './elastic_brand'; +export * from './kibana_legacy'; +export * from './neutral'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts new file mode 100644 index 0000000000000..9b576e0b05c66 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts @@ -0,0 +1,29 @@ +/* + * 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 { ColorMapping } from '../config'; + +export const KIBANA_V7_LEGACY_PALETTE_COLORS = [ + '#00a69b', + '#57c17b', + '#6f87d8', + '#663db8', + '#bc52bc', + '#9e3533', + '#daa05d', +]; + +export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = { + id: 'kibana_v7_legacy', + name: 'Kibana Legacy', + colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.ts new file mode 100644 index 0000000000000..5d3d92790843b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.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 { ColorMapping } from '../config'; + +const schemeGreys = ['#f2f4fb', '#d4d9e5', '#98a2b3', '#696f7d', '#353642']; +export const NEUTRAL_COLOR_LIGHT = schemeGreys.slice(); +export const NEUTRAL_COLOR_DARK = schemeGreys.slice().reverse(); + +export const NeutralPalette: ColorMapping.CategoricalPalette = { + id: 'neutral', + name: 'Neutral', + colorCount: NEUTRAL_COLOR_LIGHT.length, + type: 'categorical', + getColor(valueInRange, isDarkMode) { + return isDarkMode ? NEUTRAL_COLOR_DARK[valueInRange] : NEUTRAL_COLOR_LIGHT[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts new file mode 100644 index 0000000000000..27588aff2b389 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts @@ -0,0 +1,225 @@ +/* + * 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 { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { ColorMapping } from '../config'; + +export interface RootState { + colorMapping: ColorMapping.Config; + ui: { + colorPicker: { + index: number; + visibile: boolean; + type: 'gradient' | 'assignment' | 'specialAssignment'; + }; + }; +} + +const initialState: RootState['colorMapping'] = { + assignmentMode: 'auto', + assignments: [], + specialAssignments: [], + paletteId: 'eui', + colorMode: { type: 'categorical' }, +}; + +export const colorMappingSlice = createSlice({ + name: 'colorMapping', + initialState, + reducers: { + updateModel: (state, action: PayloadAction) => { + state.assignmentMode = action.payload.assignmentMode; + state.assignments = [...action.payload.assignments]; + state.specialAssignments = [...action.payload.specialAssignments]; + state.paletteId = action.payload.paletteId; + state.colorMode = { ...action.payload.colorMode }; + }, + updatePalette: ( + state, + action: PayloadAction<{ + assignments: ColorMapping.Config['assignments']; + paletteId: ColorMapping.Config['paletteId']; + colorMode: ColorMapping.Config['colorMode']; + }> + ) => { + state.paletteId = action.payload.paletteId; + state.assignments = [...action.payload.assignments]; + state.colorMode = { ...action.payload.colorMode }; + }, + assignStatically: (state, action: PayloadAction) => { + state.assignmentMode = 'manual'; + state.assignments = [...action.payload]; + }, + assignAutomatically: (state) => { + state.assignmentMode = 'auto'; + state.assignments = []; + }, + + addNewAssignment: ( + state, + action: PayloadAction + ) => { + state.assignments.push({ ...action.payload }); + }, + updateAssignment: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + assignment: ColorMapping.Config['assignments'][number]; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...action.payload.assignment, + touched: true, + }; + }, + updateAssignmentRule: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + rule: ColorMapping.Config['assignments'][number]['rule']; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...state.assignments[action.payload.assignmentIndex], + rule: action.payload.rule, + }; + }, + updateAssignmentColor: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + color: ColorMapping.Config['assignments'][number]['color']; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...state.assignments[action.payload.assignmentIndex], + color: action.payload.color, + touched: true, + }; + }, + + updateSpecialAssignmentColor: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + color: ColorMapping.Config['specialAssignments'][number]['color']; + }> + ) => { + state.specialAssignments[action.payload.assignmentIndex] = { + ...state.specialAssignments[action.payload.assignmentIndex], + color: action.payload.color, + touched: true, + }; + }, + removeAssignment: (state, action: PayloadAction) => { + state.assignments.splice(action.payload, 1); + }, + changeColorMode: (state, action: PayloadAction) => { + state.colorMode = { ...action.payload }; + }, + updateGradientColorStep: ( + state, + action: PayloadAction<{ + index: number; + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + }> + ) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + steps: state.colorMode.steps.map((step, index) => { + return index === action.payload.index + ? { ...action.payload.color, touched: true } + : { ...step, touched: false }; + }), + }; + }, + removeGradientColorStep: (state, action: PayloadAction) => { + if (state.colorMode.type !== 'gradient') { + return; + } + const steps = [...state.colorMode.steps]; + steps.splice(action.payload, 1); + + // this maintain the correct sort direciton depending on which step + // gets removed from the array when only 2 steps are left. + const sort = + state.colorMode.steps.length === 2 + ? state.colorMode.sort === 'desc' + ? action.payload === 0 + ? 'asc' + : 'desc' + : action.payload === 0 + ? 'desc' + : 'asc' + : state.colorMode.sort; + + state.colorMode = { + ...state.colorMode, + steps: [...steps], + sort, + }; + }, + addGradientColorStep: ( + state, + action: PayloadAction<{ + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + at: number; + }> + ) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + steps: [ + ...state.colorMode.steps.slice(0, action.payload.at), + { ...action.payload.color, touched: false }, + ...state.colorMode.steps.slice(action.payload.at), + ], + }; + }, + + changeGradientSortOrder: (state, action: PayloadAction<'asc' | 'desc'>) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + sort: action.payload, + }; + }, + }, +}); +// Action creators are generated for each case reducer function +export const { + updatePalette, + assignStatically, + assignAutomatically, + addNewAssignment, + updateAssignment, + updateAssignmentColor, + updateSpecialAssignmentColor, + updateAssignmentRule, + removeAssignment, + changeColorMode, + updateGradientColorStep, + removeGradientColorStep, + addGradientColorStep, + changeGradientSortOrder, + updateModel, +} = colorMappingSlice.actions; + +export const colorMappingReducer = colorMappingSlice.reducer; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts new file mode 100644 index 0000000000000..69bd57d2d852e --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts @@ -0,0 +1,26 @@ +/* + * 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 { getPalette } from '../palettes'; +import { RootState } from './color_mapping'; + +export function selectPalette(getPaletteFn: ReturnType) { + return (state: RootState) => getPaletteFn(state.colorMapping.paletteId); +} +export function selectColorMode(state: RootState) { + return state.colorMapping.colorMode; +} +export function selectSpecialAssignments(state: RootState) { + return state.colorMapping.specialAssignments; +} +export function selectIsAutoAssignmentMode(state: RootState) { + return state.colorMapping.assignmentMode === 'auto'; +} +export function selectColorPickerVisibility(state: RootState) { + return state.ui.colorPicker; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts new file mode 100644 index 0000000000000..632fb31e9dcc5 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { RootState } from './color_mapping'; + +const initialState: RootState['ui'] = { + colorPicker: { + index: 0, + visibile: false, + type: 'assignment', + }, +}; + +export const uiSlice = createSlice({ + name: 'colorMapping', + initialState, + reducers: { + colorPickerVisibility: ( + state, + action: PayloadAction<{ + index: number; + type: RootState['ui']['colorPicker']['type']; + visible: boolean; + }> + ) => { + state.colorPicker.visibile = action.payload.visible; + state.colorPicker.index = action.payload.index; + state.colorPicker.type = action.payload.type; + }, + switchColorPickerVisibility: (state) => { + state.colorPicker.visibile = !state.colorPicker.visibile; + }, + showColorPickerVisibility: (state) => { + state.colorPicker.visibile = true; + }, + hideColorPickerVisibility: (state) => { + state.colorPicker.visibile = false; + }, + }, +}); + +export const { + colorPickerVisibility, + switchColorPickerVisibility, + showColorPickerVisibility, + hideColorPickerVisibility, +} = uiSlice.actions; + +export const uiReducer = uiSlice.reducer; diff --git a/packages/kbn-coloring/src/shared_components/index.ts b/packages/kbn-coloring/src/shared_components/index.ts index 546224092e576..242df23b19e53 100644 --- a/packages/kbn-coloring/src/shared_components/index.ts +++ b/packages/kbn-coloring/src/shared_components/index.ts @@ -21,3 +21,5 @@ export const CustomizablePaletteLazy = React.lazy(() => import('./coloring')); * a predefined fallback and error boundary. */ export const CustomizablePalette = withSuspense(CustomizablePaletteLazy); + +export * from './color_mapping'; diff --git a/packages/kbn-coloring/tsconfig.json b/packages/kbn-coloring/tsconfig.json index 54c068f8bd3b6..315e59225601c 100644 --- a/packages/kbn-coloring/tsconfig.json +++ b/packages/kbn-coloring/tsconfig.json @@ -10,7 +10,6 @@ ] }, "include": [ - "**/*.scss", "**/*.ts", "**/*.tsx" ], @@ -21,6 +20,8 @@ "@kbn/utility-types", "@kbn/shared-ux-utility", "@kbn/test-jest-helpers", + "@kbn/data-plugin", + "@kbn/ui-theme", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-journeys/journey/journey.ts b/packages/kbn-journeys/journey/journey.ts index 7952b8ee7408d..bf3de796265f9 100644 --- a/packages/kbn-journeys/journey/journey.ts +++ b/packages/kbn-journeys/journey/journey.ts @@ -12,14 +12,9 @@ import { Page } from 'playwright'; import callsites from 'callsites'; import { ToolingLog } from '@kbn/tooling-log'; import { FtrConfigProvider } from '@kbn/test'; -import { - FtrProviderContext, - KibanaServer, - Es, - RetryService, -} from '@kbn/ftr-common-functional-services'; - -import { Auth } from '../services/auth'; +import { FtrProviderContext } from '../services/ftr_context_provider'; +import { Es, KibanaServer, Retry, Auth } from '../services'; + import { InputDelays } from '../services/input_delays'; import { KibanaUrl } from '../services/kibana_url'; @@ -37,7 +32,7 @@ export interface BaseStepCtx { kbnUrl: KibanaUrl; kibanaServer: KibanaServer; es: Es; - retry: RetryService; + retry: Retry; auth: Auth; } @@ -141,7 +136,7 @@ export class Journey { getService('kibanaServer'), getService('es'), getService('retry'), - new Auth(getService('config'), getService('log'), getService('kibanaServer')), + getService('auth'), this.config ).initMochaSuite(this.#steps); } diff --git a/packages/kbn-journeys/journey/journey_ftr_config.ts b/packages/kbn-journeys/journey/journey_ftr_config.ts index 1abc141c7bbae..b60f6fa191978 100644 --- a/packages/kbn-journeys/journey/journey_ftr_config.ts +++ b/packages/kbn-journeys/journey/journey_ftr_config.ts @@ -11,7 +11,7 @@ import Path from 'path'; import { v4 as uuidV4 } from 'uuid'; import { REPO_ROOT } from '@kbn/repo-info'; import type { FtrConfigProviderContext, FtrConfigProvider } from '@kbn/test'; -import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; +import { services } from '../services'; import { AnyStep } from './journey'; import { JourneyConfig } from './journey_config'; @@ -66,7 +66,7 @@ export function makeFtrConfigProvider( bail: true, }, - services: commonFunctionalServices, + services, pageObjects: {}, servicesRequiredForTestAnalysis: ['performance', 'journeyConfig'], diff --git a/packages/kbn-journeys/journey/journey_ftr_harness.ts b/packages/kbn-journeys/journey/journey_ftr_harness.ts index 9df84821de032..0f649e7a1de27 100644 --- a/packages/kbn-journeys/journey/journey_ftr_harness.ts +++ b/packages/kbn-journeys/journey/journey_ftr_harness.ts @@ -9,20 +9,19 @@ import Url from 'url'; import { inspect, format } from 'util'; import { setTimeout } from 'timers/promises'; - import * as Rx from 'rxjs'; import apmNode from 'elastic-apm-node'; import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession, Request } from 'playwright'; import { asyncMap, asyncForEach } from '@kbn/std'; import { ToolingLog } from '@kbn/tooling-log'; import { Config } from '@kbn/test'; -import { EsArchiver, KibanaServer, Es, RetryService } from '@kbn/ftr-common-functional-services'; import { ELASTIC_HTTP_VERSION_HEADER, X_ELASTIC_INTERNAL_ORIGIN_REQUEST, } from '@kbn/core-http-common'; -import { Auth } from '../services/auth'; +import { AxiosError } from 'axios'; +import { Auth, Es, EsArchiver, KibanaServer, Retry } from '../services'; import { getInputDelays } from '../services/input_delays'; import { KibanaUrl } from '../services/kibana_url'; @@ -40,7 +39,7 @@ export class JourneyFtrHarness { private readonly esArchiver: EsArchiver, private readonly kibanaServer: KibanaServer, private readonly es: Es, - private readonly retry: RetryService, + private readonly retry: Retry, private readonly auth: Auth, private readonly journeyConfig: JourneyConfig ) { @@ -63,15 +62,24 @@ export class JourneyFtrHarness { private async updateTelemetryAndAPMLabels(labels: { [k: string]: string }) { this.log.info(`Updating telemetry & APM labels: ${JSON.stringify(labels)}`); - await this.kibanaServer.request({ - path: '/internal/core/_settings', - method: 'PUT', - headers: { - [ELASTIC_HTTP_VERSION_HEADER]: '1', - [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'ftr', - }, - body: { telemetry: { labels } }, - }); + try { + await this.kibanaServer.request({ + path: '/internal/core/_settings', + method: 'PUT', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'ftr', + }, + body: { telemetry: { labels } }, + }); + } catch (error) { + const statusCode = (error as AxiosError).response?.status; + if (statusCode === 404) { + throw new Error( + `Failed to update labels, supported Kibana version is 8.11.0+ and must be started with "coreApp.allowDynamicConfigOverrides:true"` + ); + } else throw error; + } } private async setupApm() { @@ -385,7 +393,7 @@ export class JourneyFtrHarness { } const isServerlessProject = !!this.config.get('serverless'); - const kibanaPage = getNewPageObject(isServerlessProject, page, this.log); + const kibanaPage = getNewPageObject(isServerlessProject, page, this.log, this.retry); this.#_ctx = this.journeyConfig.getExtendedStepCtx({ kibanaPage, diff --git a/packages/kbn-journeys/services/auth.ts b/packages/kbn-journeys/services/auth.ts index 2a2a9719bd49b..1bc015d9f81cc 100644 --- a/packages/kbn-journeys/services/auth.ts +++ b/packages/kbn-journeys/services/auth.ts @@ -10,9 +10,7 @@ import Url from 'url'; import { format } from 'util'; import axios, { AxiosResponse } from 'axios'; -import { ToolingLog } from '@kbn/tooling-log'; -import { Config } from '@kbn/test'; -import { KibanaServer } from '@kbn/ftr-common-functional-services'; +import { FtrService } from './ftr_context_provider'; export interface Credentials { username: string; @@ -22,12 +20,10 @@ export interface Credentials { function extractCookieValue(authResponse: AxiosResponse) { return authResponse.headers['set-cookie']?.[0].toString().split(';')[0].split('sid=')[1] ?? ''; } -export class Auth { - constructor( - private readonly config: Config, - private readonly log: ToolingLog, - private readonly kibanaServer: KibanaServer - ) {} +export class AuthService extends FtrService { + private readonly config = this.ctx.getService('config'); + private readonly log = this.ctx.getService('log'); + private readonly kibanaServer = this.ctx.getService('kibanaServer'); public async login(credentials?: Credentials) { const baseUrl = new URL( diff --git a/packages/kbn-journeys/services/es.ts b/packages/kbn-journeys/services/es.ts new file mode 100644 index 0000000000000..16ca3079a2ab0 --- /dev/null +++ b/packages/kbn-journeys/services/es.ts @@ -0,0 +1,20 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; + +import { createEsClientForFtrConfig, ProvidedType } from '@kbn/test'; +import { FtrProviderContext } from './ftr_context_provider'; + +export function EsProvider({ getService }: FtrProviderContext): Client { + const config = getService('config'); + + return createEsClientForFtrConfig(config); +} + +export type Es = ProvidedType; diff --git a/packages/kbn-journeys/services/ftr_context_provider.ts b/packages/kbn-journeys/services/ftr_context_provider.ts new file mode 100644 index 0000000000000..7dd5038ef3f19 --- /dev/null +++ b/packages/kbn-journeys/services/ftr_context_provider.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 { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; +import { services } from '.'; + +export type FtrProviderContext = GenericFtrProviderContext; +export class FtrService extends GenericFtrService {} diff --git a/packages/kbn-journeys/services/index.ts b/packages/kbn-journeys/services/index.ts new file mode 100644 index 0000000000000..93611e5d5a3f8 --- /dev/null +++ b/packages/kbn-journeys/services/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { commonFunctionalServices, RetryService } from '@kbn/ftr-common-functional-services'; +import { EsArchiverProvider } from '@kbn/ftr-common-functional-services/services/es_archiver'; +import { KibanaServerProvider } from '@kbn/ftr-common-functional-services/services/kibana_server'; +import { ProvidedType } from '@kbn/test'; +import { EsProvider } from './es'; +import { AuthService } from './auth'; + +export const services = { + es: EsProvider, + kibanaServer: commonFunctionalServices.kibanaServer, + esArchiver: commonFunctionalServices.esArchiver, + retry: commonFunctionalServices.retry, + auth: AuthService, +}; + +export type EsArchiver = ProvidedType; +export type KibanaServer = ProvidedType; +export type Es = ProvidedType; +export type Auth = AuthService; +export type Retry = RetryService; diff --git a/packages/kbn-journeys/services/page/index.ts b/packages/kbn-journeys/services/page/index.ts index 6a809eb7480f6..6e0aaafcfdc27 100644 --- a/packages/kbn-journeys/services/page/index.ts +++ b/packages/kbn-journeys/services/page/index.ts @@ -8,9 +8,10 @@ import { ToolingLog } from '@kbn/tooling-log'; import { Page } from 'playwright'; +import { Retry } from '..'; import { KibanaPage } from './kibana_page'; import { ProjectPage } from './project_page'; -export function getNewPageObject(isServerless: boolean, page: Page, log: ToolingLog) { - return isServerless ? new ProjectPage(page, log) : new KibanaPage(page, log); +export function getNewPageObject(isServerless: boolean, page: Page, log: ToolingLog, retry: Retry) { + return isServerless ? new ProjectPage(page, log, retry) : new KibanaPage(page, log, retry); } diff --git a/packages/kbn-journeys/services/page/kibana_page.ts b/packages/kbn-journeys/services/page/kibana_page.ts index 72e595601473a..170e009d0cd29 100644 --- a/packages/kbn-journeys/services/page/kibana_page.ts +++ b/packages/kbn-journeys/services/page/kibana_page.ts @@ -9,6 +9,7 @@ import { subj } from '@kbn/test-subj-selector'; import { ToolingLog } from '@kbn/tooling-log'; import { Page } from 'playwright'; +import { Retry } from '..'; interface WaitForRenderArgs { expectedItemsCount: number; @@ -19,10 +20,12 @@ interface WaitForRenderArgs { export class KibanaPage { readonly page: Page; readonly log: ToolingLog; + readonly retry: Retry; - constructor(page: Page, log: ToolingLog) { + constructor(page: Page, log: ToolingLog, retry: Retry) { this.page = page; this.log = log; + this.retry = retry; } async waitForHeader() { @@ -36,25 +39,32 @@ export class KibanaPage { } async waitForRender({ expectedItemsCount, itemLocator, checkAttribute }: WaitForRenderArgs) { - try { - await this.page.waitForFunction( - function renderCompleted(args: WaitForRenderArgs) { - const renderingItems = Array.from(document.querySelectorAll(args.itemLocator)); - const allItemsLoaded = renderingItems.length === args.expectedItemsCount; - return allItemsLoaded - ? renderingItems.every((e) => e.getAttribute(args.checkAttribute) === 'true') - : false; - }, - { expectedItemsCount, itemLocator, checkAttribute } - ); - } catch (err) { - const loaded = await this.page.$$(itemLocator); - const rendered = await this.page.$$(`${itemLocator}[${checkAttribute}="true"]`); - this.log.error( - `'waitForRendering' failed: loaded - ${loaded.length}, rendered - ${rendered.length}, expected count - ${expectedItemsCount}` - ); - throw err; - } + // we can't use `page.waitForFunction` because of CSP while testing on Cloud + await this.retry.waitFor( + `rendering of ${expectedItemsCount} elements with selector ${itemLocator} is completed`, + async () => { + const renderingItems = await this.page.$$(itemLocator); + if (renderingItems.length === expectedItemsCount) { + // all components are loaded, checking if all are rendered + const renderStatuses = await Promise.all( + renderingItems.map(async (item) => { + return (await item.getAttribute(checkAttribute)) === 'true'; + }) + ); + const rendered = renderStatuses.filter((isRendered) => isRendered === true); + this.log.debug( + `waitForRender: ${rendered.length} out of ${expectedItemsCount} are rendered...` + ); + return rendered.length === expectedItemsCount; + } else { + // not all components are loaded yet + this.log.debug( + `waitForRender: ${renderingItems.length} out of ${expectedItemsCount} are loaded...` + ); + return false; + } + } + ); } async waitForVisualizations(count: number) { diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 78bba1df4220a..ec34d257eadc2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -50,13 +50,13 @@ pageLoadAssetSize: expressionLegacyMetricVis: 23121 expressionMetric: 22238 expressionMetricVis: 23121 - expressionPartitionVis: 26338 + expressionPartitionVis: 28000 expressionRepeatImage: 22341 expressionRevealImage: 25675 expressions: 140958 expressionShape: 34008 expressionTagcloud: 27505 - expressionXY: 39500 + expressionXY: 45000 features: 21723 fieldFormats: 65209 files: 22673 diff --git a/src/plugins/chart_expressions/common/color_categories.ts b/src/plugins/chart_expressions/common/color_categories.ts new file mode 100644 index 0000000000000..0bb8811f2701a --- /dev/null +++ b/src/plugins/chart_expressions/common/color_categories.ts @@ -0,0 +1,41 @@ +/* + * 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 { DatatableRow } from '@kbn/expressions-plugin/common'; +import { isMultiFieldKey } from '@kbn/data-plugin/common'; + +/** + * Get the stringified version of all the categories that needs to be colored in the chart. + * Multifield keys will return as array of string and simple fields (numeric, string) will be returned as a plain unformatted string. + */ +export function getColorCategories( + rows: DatatableRow[], + accessor?: string +): Array { + return accessor + ? rows.reduce<{ keys: Set; categories: Array }>( + (acc, r) => { + const value = r[accessor]; + if (value === undefined) { + return acc; + } + // The categories needs to be stringified in their unformatted version. + // We can't distinguish between a number and a string from a text input and the match should + // work with both numeric field values and string values. + const key = (isMultiFieldKey(value) ? [...value.keys] : [value]).map(String); + const stringifiedKeys = key.join(','); + if (!acc.keys.has(stringifiedKeys)) { + acc.keys.add(stringifiedKeys); + acc.categories.push(key.length === 1 ? key[0] : key); + } + return acc; + }, + { keys: new Set(), categories: [] } + ).categories + : []; +} diff --git a/src/plugins/chart_expressions/common/index.ts b/src/plugins/chart_expressions/common/index.ts index 0983b1ed28d4d..acc3b4d8c88cd 100644 --- a/src/plugins/chart_expressions/common/index.ts +++ b/src/plugins/chart_expressions/common/index.ts @@ -13,3 +13,4 @@ export { isOnAggBasedEditor, } from './utils'; export type { Simplify, MakeOverridesSerializable } from './types'; +export { getColorCategories } from './color_categories'; diff --git a/src/plugins/chart_expressions/common/tsconfig.json b/src/plugins/chart_expressions/common/tsconfig.json index f65660474561b..7ac76523fcb6c 100644 --- a/src/plugins/chart_expressions/common/tsconfig.json +++ b/src/plugins/chart_expressions/common/tsconfig.json @@ -15,5 +15,7 @@ ], "kbn_references": [ "@kbn/core-execution-context-common", + "@kbn/expressions-plugin", + "@kbn/data-plugin", ] } diff --git a/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc b/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc index 41a9c965a66da..a49ca80a2fcd2 100644 --- a/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc @@ -12,7 +12,8 @@ "fieldFormats", "charts", "visualizations", - "presentationUtil" + "presentationUtil", + "data" ], "optionalPlugins": [ "usageCollection" diff --git a/src/plugins/chart_expressions/expression_metric/kibana.jsonc b/src/plugins/chart_expressions/expression_metric/kibana.jsonc index 087583e6fff6f..a53def7de36ee 100644 --- a/src/plugins/chart_expressions/expression_metric/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_metric/kibana.jsonc @@ -12,7 +12,8 @@ "fieldFormats", "charts", "visualizations", - "presentationUtil" + "presentationUtil", + "data" ], "optionalPlugins": [ "usageCollection" diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap index 604368d7ab130..a3cd4f976757b 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap @@ -71,6 +71,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap index 293f86c6bf9ec..edcb2c8fd76e4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -61,6 +61,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { @@ -203,6 +204,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap index f6817eca439cf..17c372547ad79 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap @@ -71,6 +71,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap index 7c74291190a2d..cb1d724053dfe 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "type": "vis_dimension", }, + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts index b312de7bf1583..c74669439b2c3 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts @@ -122,6 +122,10 @@ export const strings = { i18n.translate('expressionPartitionVis.reusable.function.dimension.splitrow', { defaultMessage: 'Row split', }), + getColorMappingHelp: () => + i18n.translate('expressionPartitionVis.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), }; export const errors = { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts index fc863cf73c68c..4a9dff714c8da 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -110,6 +110,10 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -146,6 +150,7 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index 0cf6522456c62..30e8388f1255e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -141,6 +141,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -173,6 +177,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts index 2a5d0a6af7a8a..e0804dd9b0e92 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -115,6 +115,10 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -152,6 +156,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts index e4176cf6015c1..6e23513851b1e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -114,6 +114,10 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -147,6 +151,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 239253f54491d..00667ace39576 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -61,6 +61,7 @@ interface VisCommonParams { maxLegendLines: number; legendSize?: LegendSize; ariaLabel?: string; + colorMapping?: string; // JSON stringified object of the color mapping } interface VisCommonConfig extends VisCommonParams { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts new file mode 100644 index 0000000000000..ec11fc7605de1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts @@ -0,0 +1,77 @@ +/* + * 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 { NodeColorAccessor, PATH_KEY } from '@elastic/charts'; +import { lightenColor } from '@kbn/charts-plugin/public'; +import { MultiFieldKey } from '@kbn/data-plugin/common'; +import { getColorFactory } from '@kbn/coloring'; +import { isMultiFieldKey } from '@kbn/data-plugin/common'; +import { ChartTypes } from '../../../common/types'; + +export function getCategoryKeys(category: string | MultiFieldKey): string | string[] { + return isMultiFieldKey(category) ? category.keys.map(String) : `${category}`; +} + +/** + * Get the color of a specific slice/section in Pie,donut,waffle and treemap. + * These chart type shares the same color assignment mechanism. + */ +const getPieFillColor = + ( + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType + ): NodeColorAccessor => + (_key, _sortIndex, node) => { + const path = node[PATH_KEY]; + // the category used to color the pie/donut is at the third level of the path + // first two are: small multiple and pie whole center. + const category = getCategoryKeys(path[2].value); + const color = getColorFn(category); + // increase the lightness of the color on each layer. + return lightenColor(color, layerIndex + 1, numOfLayers); + }; + +/** + * Get the color of a section in a Mosaic chart. + * This chart has a slight variation in the way color are applied. Mosaic can represent up to 2 layers, + * described in lens as the horizontal and vertical axes. + * With a single layer the color is simply applied per each category, with 2 layer, the color is applied only + * to the category that describe a row, not by column. + */ +const getMosaicFillColor = + ( + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType + ): NodeColorAccessor => + (_key, _sortIndex, node) => { + // Special case for 2 layer mosaic where the color is per rows and the columns are not colored + if (numOfLayers === 2 && layerIndex === 0) { + // transparent color will fallback to the kibana/context background + return 'rgba(0,0,0,0)'; + } + const path = node[PATH_KEY]; + + // the category used to color the pie/donut is at the third level of the `path` when using a single layer mosaic + // and are at fourth level of `path` when using 2 layer mosaic + // first two are: small multiple and pie whole center. + const category = getCategoryKeys(numOfLayers === 2 ? path[3].value : path[2].value); + return getColorFn(category); + }; + +export const getPartitionFillColor = ( + chartType: ChartTypes, + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType +): NodeColorAccessor => { + return chartType === ChartTypes.MOSAIC + ? getMosaicFillColor(layerIndex, numOfLayers, getColorFn) + : getPieFillColor(layerIndex, numOfLayers, getColorFn); +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts index e4f3e1687e4ad..6f40097809e18 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts @@ -7,16 +7,25 @@ */ import { Datum, PartitionLayer } from '@elastic/charts'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { + PaletteRegistry, + getColorFactory, + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, +} from '@kbn/coloring'; import { i18n } from '@kbn/i18n'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { Datatable, DatatableRow } from '@kbn/expressions-plugin/public'; + +import { getColorCategories } from '@kbn/chart-expressions-common'; import { getDistinctSeries } from '..'; import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; import { sortPredicateByType, sortPredicateSaveSourceOrder } from './sort_predicate'; import { byDataColorPaletteMap, getColor } from './get_color'; import { getNodeLabel } from './get_node_labels'; +import { getPartitionFillColor } from '../colors/color_mapping_accessors'; // This is particularly useful in case of a text based languages where // it's no possible to use a missingBucketLabel @@ -62,6 +71,15 @@ export const getLayers = ( const distinctSeries = getDistinctSeries(rows, columns); + // return a fn only if color mapping is available in visParams + const getColorFromMappingFn = getColorFromMappingFactory( + chartType, + columns, + rows, + isDarkMode, + visParams + ); + return columns.map((col, layerIndex) => { return { groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? emptySliceLabel : col.name), @@ -75,26 +93,74 @@ export const getLayers = ( ? sortPredicateSaveSourceOrder() : sortPredicateForType, shape: { - fillColor: (key, sortIndex, node) => - getColor( - chartType, - key, - node, - layerIndex, - isSplitChart, - overwriteColors, - distinctSeries, - { columnsLength: columns.length, rowsLength: rows.length }, - visParams, - palettes, - byDataPalette, - syncColors, - isDarkMode, - formatter, - col, - formatters - ), + // this applies color mapping only if visParams.colorMapping is available + fillColor: getColorFromMappingFn + ? getPartitionFillColor(chartType, layerIndex, columns.length, getColorFromMappingFn) + : (key, sortIndex, node) => + getColor( + chartType, + key, + node, + layerIndex, + isSplitChart, + overwriteColors, + distinctSeries, + { columnsLength: columns.length, rowsLength: rows.length }, + visParams, + palettes, + byDataPalette, + syncColors, + isDarkMode, + formatter, + col, + formatters + ), }, }; }); }; + +/** + * If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key) + * and returns a color specified in the provided mapping + */ +function getColorFromMappingFactory( + chartType: ChartTypes, + columns: Array>, + rows: DatatableRow[], + isDarkMode: boolean, + visParams: PartitionVisParams +): undefined | ((category: string | string[]) => string) { + const { colorMapping, dimensions } = visParams; + + if (!colorMapping) { + // return undefined, we will use the legacy color mapping instead + return undefined; + } + // if pie/donut/treemap with no buckets use the default color mode + if ( + (chartType === ChartTypes.DONUT || + chartType === ChartTypes.PIE || + chartType === ChartTypes.TREEMAP) && + (!dimensions.buckets || dimensions.buckets?.length === 0) + ) { + return undefined; + } + // the mosaic configures the main categories in the second column, instead of the first + // as it happens in all the other partition types. + // Independentely from the bucket aggregation used, the categories will always be casted + // as string to make it nicely working with a text input field, avoiding a field + const categories = + chartType === ChartTypes.MOSAIC && columns.length === 2 + ? getColorCategories(rows, columns[1]?.id) + : getColorCategories(rows, columns[0]?.id); + return getColorFactory( + JSON.parse(colorMapping), + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories, + } + ); +} diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap index d03cebd680290..15f335df82684 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap @@ -66,6 +66,7 @@ Object { "bucket": Object { "accessor": 1, }, + "colorMapping": undefined, "isPreview": false, "maxFontSize": 72, "metric": Object { @@ -126,6 +127,7 @@ Object { }, "type": "vis_dimension", }, + "colorMapping": undefined, "isPreview": false, "maxFontSize": 72, "metric": Object { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts index ec69431cd1735..75148e570331c 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts @@ -51,6 +51,9 @@ const strings = { isPreview: i18n.translate('expressionTagcloud.functions.tagcloud.args.isPreviewHelpText', { defaultMessage: 'Set isPreview to true to avoid showing out of room warnings', }), + colorMapping: i18n.translate('expressionTagcloud.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), }, dimension: { tags: i18n.translate('expressionTagcloud.functions.tagcloud.dimension.tags', { @@ -146,6 +149,10 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { default: false, required: false, }, + colorMapping: { + types: ['string'], + help: argHelp.colorMapping, + }, }, fn(input, args, handlers) { validateAccessor(args.metric, input.columns); @@ -167,6 +174,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, isPreview: Boolean(args.isPreview), + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 985da788c6ffc..c59e70a5c028d 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -27,6 +27,7 @@ interface TagCloudCommonParams { metric: ExpressionValueVisDimension | string; bucket?: ExpressionValueVisDimension | string; palette: PaletteOutput; + colorMapping?: string; // JSON stringified object of the color mapping } export interface TagCloudVisConfig extends TagCloudCommonParams { diff --git a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc index 6c6ce82d321ed..b6bf410e2786f 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc @@ -8,6 +8,7 @@ "server": true, "browser": true, "requiredPlugins": [ + "data", "expressions", "visualizations", "charts", diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx index 3f9c86778e82d..86c4bc009d931 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx @@ -105,6 +105,7 @@ describe('TagCloudChart', function () { renderComplete: jest.fn(), syncColors: false, visType: 'tagcloud', + isDarkMode: false, }; wrapperPropsWithColumnNames = { @@ -135,6 +136,7 @@ describe('TagCloudChart', function () { renderComplete: jest.fn(), syncColors: false, visType: 'tagcloud', + isDarkMode: false, }; }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index adfc3df81f97f..e3532bb17f97e 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -13,11 +13,19 @@ import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; import { IconChartTagcloud } from '@kbn/chart-icons'; import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; -import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring'; -import { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public'; -import { getOverridesFor } from '@kbn/chart-expressions-common'; +import { + PaletteRegistry, + PaletteOutput, + getColorFactory, + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, +} from '@kbn/coloring'; +import { IInterpreterRenderHandlers, DatatableRow } from '@kbn/expressions-plugin/public'; +import { getColorCategories, getOverridesFor } from '@kbn/chart-expressions-common'; import type { AllowedSettingsOverrides, AllowedChartOverrides } from '@kbn/charts-plugin/common'; import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { isMultiFieldKey } from '@kbn/data-plugin/common'; import { getFormatService } from '../format_service'; import { TagcloudRendererConfig } from '../../common/types'; import { ScaleOptions, Orientation } from '../../common/constants'; @@ -31,6 +39,7 @@ export type TagCloudChartProps = TagcloudRendererConfig & { renderComplete: IInterpreterRenderHandlers['done']; palettesRegistry: PaletteRegistry; overrides?: AllowedSettingsOverrides & AllowedChartOverrides; + isDarkMode: boolean; }; const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) => @@ -84,9 +93,10 @@ export const TagCloudChart = ({ renderComplete, syncColors, overrides, + isDarkMode, }: TagCloudChartProps) => { const [warning, setWarning] = useState(false); - const { bucket, metric, scale, palette, showLabel, orientation } = visParams; + const { bucket, metric, scale, palette, showLabel, orientation, colorMapping } = visParams; const bucketFormatter = useMemo(() => { return bucket @@ -96,23 +106,35 @@ export const TagCloudChart = ({ const tagCloudData = useMemo(() => { const bucketColumn = bucket ? getColumnByAccessor(bucket, visData.columns)! : null; - const tagColumn = bucket ? bucketColumn!.id : null; + const tagColumn = bucket ? bucketColumn!.id : undefined; const metricColumn = getColumnByAccessor(metric, visData.columns)!.id; const metrics = visData.rows.map((row) => row[metricColumn]); - const values = bucket && tagColumn !== null ? visData.rows.map((row) => row[tagColumn]) : []; + const values = + bucket && tagColumn !== undefined ? visData.rows.map((row) => row[tagColumn]) : []; const maxValue = Math.max(...metrics); const minValue = Math.min(...metrics); + const colorFromMappingFn = getColorFromMappingFactory( + tagColumn, + visData.rows, + isDarkMode, + colorMapping + ); + return visData.rows.map((row) => { - const tag = tagColumn === null ? 'all' : row[tagColumn]; + const tag = tagColumn === undefined ? 'all' : row[tagColumn]; + + const category = isMultiFieldKey(tag) ? tag.keys.map(String) : `${tag}`; return { text: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag, weight: tag === 'all' || visData.rows.length <= 1 ? 1 : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0, - color: getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)', + color: colorFromMappingFn + ? colorFromMappingFn(category) + : getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)', }; }); }, [ @@ -124,6 +146,8 @@ export const TagCloudChart = ({ syncColors, visData.columns, visData.rows, + colorMapping, + isDarkMode, ]); useEffect(() => { @@ -278,3 +302,28 @@ export const TagCloudChart = ({ // eslint-disable-next-line import/no-default-export export { TagCloudChart as default }; + +/** + * If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key) + * and returns a color specified in the provided mapping + */ +function getColorFromMappingFactory( + tagColumn: string | undefined, + rows: DatatableRow[], + isDarkMode: boolean, + colorMapping?: string +): undefined | ((category: string | string[]) => string) { + if (!colorMapping) { + // return undefined, we will use the legacy color mapping instead + return undefined; + } + return getColorFactory( + JSON.parse(colorMapping), + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories: getColorCategories(rows, tagColumn), + } + ); +} diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx index b3ab496447754..101c40b6b384d 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx @@ -67,6 +67,12 @@ export const tagcloudRenderer: ( }; const palettesRegistry = await plugins.charts.palettes.getPalettes(); + let isDarkMode = false; + plugins.charts.theme.darkModeEnabled$ + .subscribe((val) => { + isDarkMode = val.darkMode; + }) + .unsubscribe(); render( @@ -87,6 +93,7 @@ export const tagcloudRenderer: ( fireEvent={handlers.event} syncColors={config.syncColors} overrides={config.overrides} + isDarkMode={isDarkMode} /> )} diff --git a/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json b/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json index 55e81302586b8..b737dfb445f09 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json +++ b/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json @@ -27,6 +27,7 @@ "@kbn/analytics", "@kbn/chart-expressions-common", "@kbn/chart-icons", + "@kbn/data-plugin", ], "exclude": [ "target/**/*", diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 10f6d5d748b23..b9e2bd6dbac67 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -94,4 +94,8 @@ export const commonDataLayerArgs: Omit< help: strings.getPaletteHelp(), default: '{palette}', }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 94d788106acb3..03df575b3c653 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -52,6 +52,7 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult layerType: LayerTypes.DATA, table: normalizedTable, showLines: args.showLines, + colorMapping: args.colorMapping, ...accessors, }; }; diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index d9fc015c2844c..2446a27e718ce 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -209,6 +209,10 @@ export const strings = { i18n.translate('expressionXY.dataLayer.palette.help', { defaultMessage: 'Palette', }), + getColorMappingHelp: () => + i18n.translate('expressionXY.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), getTableHelp: () => i18n.translate('expressionXY.layers.table.help', { defaultMessage: 'Table', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 55fd63786570b..a81128f6e74a7 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -136,6 +136,7 @@ export interface DataLayerArgs { isStacked: boolean; isHorizontal: boolean; palette: PaletteOutput; + colorMapping?: string; // JSON stringified object of the color mapping decorations?: DataDecorationConfigResult[]; curveType?: XYCurveType; } @@ -163,6 +164,7 @@ export interface ExtendedDataLayerArgs { isStacked: boolean; isHorizontal: boolean; palette: PaletteOutput; + colorMapping?: string; // palette will always be set on the expression decorations?: DataDecorationConfigResult[]; curveType?: XYCurveType; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index fe76259b65889..9bc59b677ed78 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -1099,6 +1099,7 @@ exports[`XYChart component it renders area 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -2107,6 +2108,7 @@ exports[`XYChart component it renders bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -3115,6 +3117,7 @@ exports[`XYChart component it renders horizontal bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -4123,6 +4126,7 @@ exports[`XYChart component it renders line 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -5131,6 +5135,7 @@ exports[`XYChart component it renders stacked area 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -6139,6 +6144,7 @@ exports[`XYChart component it renders stacked bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -7147,6 +7153,7 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -8381,6 +8388,7 @@ exports[`XYChart component split chart should render split chart if both, splitR }, } } + isDarkMode={false} layers={ Array [ Object { @@ -9622,6 +9630,7 @@ exports[`XYChart component split chart should render split chart if splitColumnA }, } } + isDarkMode={false} layers={ Array [ Object { @@ -10861,6 +10870,7 @@ exports[`XYChart component split chart should render split chart if splitRowAcce }, } } + isDarkMode={false} layers={ Array [ Object { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index 5cabeaee31575..cc6e969a10af9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -57,6 +57,7 @@ interface Props { fieldFormats: LayersFieldFormats; uiState?: PersistedState; singleTable?: boolean; + isDarkMode: boolean; } export const DataLayers: FC = ({ @@ -80,6 +81,7 @@ export const DataLayers: FC = ({ fieldFormats, uiState, singleTable, + isDarkMode, }) => { // for singleTable mode we should use y accessors from all layers for creating correct series name and getting color const allYAccessors = layers.flatMap((layer) => layer.accessors); @@ -169,6 +171,7 @@ export const DataLayers: FC = ({ allYAccessors, singleTable, multipleLayersWithSplits, + isDarkMode, }); const index = `${layer.layerId}-${accessorIndex}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index b8ac9d5cd0bbb..c241e476db5de 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -964,6 +964,7 @@ export function XYChart({ fieldFormats={fieldFormats} uiState={uiState} singleTable={singleTable} + isDarkMode={darkMode} /> )} {referenceLineLayers.length ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts new file mode 100644 index 0000000000000..b57f371eab2fd --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SeriesColorAccessorFn } from '@elastic/charts'; +import { getColorFactory, type ColorMapping, type ColorMappingInputData } from '@kbn/coloring'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; + +/** + * Return a color accessor function for XY charts depending on the split accessors received. + */ +export function getColorSeriesAccessorFn( + config: ColorMapping.Config, + getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette, + isDarkMode: boolean, + mappingData: ColorMappingInputData, + fieldId: string, + specialTokens: Map +): SeriesColorAccessorFn { + // inverse map to handle the conversion between the formatted string and their original format + // for any specified special tokens + const specialHandlingInverseMap: Map = new Map( + [...specialTokens.entries()].map((d) => [d[1], d[0]]) + ); + + const getColor = getColorFactory(config, getPaletteFn, isDarkMode, mappingData); + + return ({ splitAccessors }) => { + const splitValue = splitAccessors.get(fieldId); + // if there isn't a category associated in the split accessor, let's use the default color + if (splitValue === undefined) { + return null; + } + + // category can be also a number, range, ip, multi-field. We need to stringify it to be sure + // we can correctly match it a with user string + // if the separator exist, we de-construct it into a multifieldkey into values. + const categories = `${splitValue}`.split(MULTI_FIELD_KEY_SEPARATOR).map((category) => { + return specialHandlingInverseMap.get(category) ?? category; + }); + // we must keep the array nature of a multi-field key or just use a single string + // This is required because the rule stored are checked differently for single values or multi-values + return getColor(categories.length > 1 ? categories : categories[0]); + }; +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index 94b187055e6dd..990d1ab93a1bc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -95,6 +95,11 @@ export const getAllSeries = ( return allSeries; }; +/** + * This function joins every data series name available on each layer by the same color palette. + * The returned function `getRank` should return the position of a series name in this unified list by palette. + * + */ export function getColorAssignments( layers: CommonXYLayerConfig[], titles: LayersAccessorsTitles, diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index ff76ec511ffc9..1971409ab4223 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -16,6 +16,7 @@ import { SeriesName, StackMode, XYChartSeriesIdentifier, + SeriesColorAccessorFn, } from '@elastic/charts'; import { IFieldFormat } from '@kbn/field-formats-plugin/common'; import type { PersistedState } from '@kbn/visualizations-plugin/public'; @@ -23,6 +24,13 @@ import { Datatable } from '@kbn/expressions-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; +import { + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, + SPECIAL_TOKENS_STRING_CONVERTION, +} from '@kbn/coloring'; +import { getColorCategories } from '@kbn/chart-expressions-common'; import { isDataLayer } from '../../common/utils/layer_types_guards'; import { CommonXYDataLayerConfig, CommonXYLayerConfig, XScaleType } from '../../common'; import { AxisModes, SeriesTypes } from '../../common/constants'; @@ -32,6 +40,7 @@ import { ColorAssignments } from './color_assignment'; import { GroupsConfiguration } from './axes_configuration'; import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers'; import { getFormat } from './format'; +import { getColorSeriesAccessorFn } from './color/color_mapping_accessor'; type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps; @@ -57,6 +66,7 @@ type GetSeriesPropsFn = (config: { allYAccessors: Array; singleTable?: boolean; multipleLayersWithSplits: boolean; + isDarkMode: boolean; }) => SeriesSpec; type GetSeriesNameFn = ( @@ -399,6 +409,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ allYAccessors, singleTable, multipleLayersWithSplits, + isDarkMode, }): SeriesSpec => { const { table, isStacked, markSizeAccessor } = layer; const isPercentage = layer.isPercentage; @@ -478,6 +489,34 @@ export const getSeriesProps: GetSeriesPropsFn = ({ ); }; + const colorAccessorFn: SeriesColorAccessorFn = + // if colorMapping exist then we can apply it, if not let's use the legacy coloring method + layer.colorMapping && splitColumnIds.length > 0 + ? getColorSeriesAccessorFn( + JSON.parse(layer.colorMapping), // the color mapping is at this point just a strinfigied JSON + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories: getColorCategories(table.rows, splitColumnIds[0]), + }, + splitColumnIds[0], + SPECIAL_TOKENS_STRING_CONVERTION + ) + : (series) => + getColor( + series, + { + layer, + colorAssignments, + paletteService, + getSeriesNameFn, + syncColors, + }, + uiState, + singleTable + ); + return { splitSeriesAccessors: splitColumnIds.length ? splitColumnIds : [], stackAccessors: isStacked ? [xColumnId || 'unifiedX'] : [], @@ -497,19 +536,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ formatter?.id === 'bytes' && scaleType === ScaleType.Linear ? ScaleType.LinearBinary : scaleType, - color: (series) => - getColor( - series, - { - layer, - colorAssignments, - paletteService, - getSeriesNameFn, - syncColors, - }, - uiState, - singleTable - ), + color: colorAccessorFn, groupId: yAxis?.groupId, enableHistogramMode, stackMode, diff --git a/src/plugins/charts/kibana.jsonc b/src/plugins/charts/kibana.jsonc index 6b0e952969329..8c00cd40f4ad3 100644 --- a/src/plugins/charts/kibana.jsonc +++ b/src/plugins/charts/kibana.jsonc @@ -7,7 +7,8 @@ "server": true, "browser": true, "requiredPlugins": [ - "expressions" + "expressions", + "data" ], "extraPublicDirs": [ "common" diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 31bc7cf9ca544..369e56caf1859 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -37,7 +37,7 @@ export * from './significant_text_fn'; export * from './significant_text'; export * from './terms_fn'; export * from './terms'; -export { MultiFieldKey } from './multi_field_key'; +export { MultiFieldKey, isMultiFieldKey, MULTI_FIELD_KEY_SEPARATOR } from './multi_field_key'; export * from './multi_terms_fn'; export * from './multi_terms'; export * from './rare_terms_fn'; diff --git a/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts b/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts index 89ac1f4c00a54..5b02d0d8827f2 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts @@ -38,3 +38,13 @@ export class MultiFieldKey { return this[id]; } } + +export function isMultiFieldKey(field: unknown): field is MultiFieldKey { + return field instanceof MultiFieldKey; +} + +/** + * Multi-field key separator used in Visualizations (Lens, AggBased, TSVB). + * This differs from the separator used in the toString method of the MultiFieldKey + */ +export const MULTI_FIELD_KEY_SEPARATOR = ' › '; diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts index cc0a84cd4a908..34a67223b4be5 100644 --- a/src/plugins/data/common/search/expressions/esdsl.ts +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -126,7 +126,7 @@ export const getEsdslFn = ({ }); try { - const finalResponse = await lastValueFrom( + const { rawResponse } = await lastValueFrom( search( { params: { @@ -141,14 +141,14 @@ export const getEsdslFn = ({ const stats: RequestStatistics = {}; - if (finalResponse.rawResponse?.took) { + if (rawResponse?.took) { stats.queryTime = { label: i18n.translate('data.search.es_search.queryTimeLabel', { defaultMessage: 'Query time', }), value: i18n.translate('data.search.es_search.queryTimeValue', { defaultMessage: '{queryTime}ms', - values: { queryTime: finalResponse.rawResponse.took }, + values: { queryTime: rawResponse.took }, }), description: i18n.translate('data.search.es_search.queryTimeDescription', { defaultMessage: @@ -158,12 +158,12 @@ export const getEsdslFn = ({ }; } - if (finalResponse.rawResponse?.hits) { + if (rawResponse?.hits) { stats.hitsTotal = { label: i18n.translate('data.search.es_search.hitsTotalLabel', { defaultMessage: 'Hits (total)', }), - value: `${finalResponse.rawResponse.hits.total}`, + value: `${rawResponse.hits.total}`, description: i18n.translate('data.search.es_search.hitsTotalDescription', { defaultMessage: 'The number of documents that match the query.', }), @@ -173,19 +173,19 @@ export const getEsdslFn = ({ label: i18n.translate('data.search.es_search.hitsLabel', { defaultMessage: 'Hits', }), - value: `${finalResponse.rawResponse.hits.hits.length}`, + value: `${rawResponse.hits.hits.length}`, description: i18n.translate('data.search.es_search.hitsDescription', { defaultMessage: 'The number of documents returned by the query.', }), }; } - request.stats(stats).ok({ json: finalResponse }); + request.stats(stats).ok({ json: rawResponse }); request.json(dsl); return { type: 'es_raw_response', - body: finalResponse.rawResponse, + body: rawResponse, }; } catch (e) { request.error({ json: e }); diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index b2d6a0458c63b..8ef0f49588303 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -210,24 +210,24 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { return throwError(() => error); }), tap({ - next(finalResponse) { + next({ rawResponse }) { logInspectorRequest() .stats({ hits: { label: i18n.translate('data.search.es_search.hitsLabel', { defaultMessage: 'Hits', }), - value: `${finalResponse.rawResponse.values.length}`, + value: `${rawResponse.values.length}`, description: i18n.translate('data.search.es_search.hitsDescription', { defaultMessage: 'The number of documents returned by the query.', }), }, }) .json(params) - .ok({ json: finalResponse }); + .ok({ json: rawResponse }); }, error(error) { - logInspectorRequest().json(params).error({ json: error }); + logInspectorRequest().error({ json: error }); }, }) ); diff --git a/src/plugins/data/common/search/expressions/essql.ts b/src/plugins/data/common/search/expressions/essql.ts index e93ee85441a22..a5db4674a7d14 100644 --- a/src/plugins/data/common/search/expressions/essql.ts +++ b/src/plugins/data/common/search/expressions/essql.ts @@ -217,14 +217,14 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { return throwError(() => error); }), tap({ - next(finalResponse) { + next({ rawResponse, took }) { logInspectorRequest() .stats({ hits: { label: i18n.translate('data.search.es_search.hitsLabel', { defaultMessage: 'Hits', }), - value: `${finalResponse.rawResponse.rows.length}`, + value: `${rawResponse.rows.length}`, description: i18n.translate('data.search.es_search.hitsDescription', { defaultMessage: 'The number of documents returned by the query.', }), @@ -235,7 +235,7 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { }), value: i18n.translate('data.search.es_search.queryTimeValue', { defaultMessage: '{queryTime}ms', - values: { queryTime: finalResponse.took }, + values: { queryTime: took }, }), description: i18n.translate('data.search.es_search.queryTimeDescription', { defaultMessage: @@ -245,10 +245,10 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { }, }) .json(params) - .ok({ json: finalResponse }); + .ok({ json: rawResponse }); }, error(error) { - logInspectorRequest().json(params).error({ json: error }); + logInspectorRequest().error({ json: error }); }, }) ); diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index b2f818acaa0ac..cedfa3ee02274 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; import type { TransportRequestOptions } from '@elastic/elasticsearch'; import type { KibanaExecutionContext } from '@kbn/core/public'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -87,11 +86,6 @@ export interface IKibanaSearchResponse { * The raw response returned by the internal search method (usually the raw ES response) */ rawResponse: RawResponse; - - /** - * HTTP request parameters from elasticsearch transport client t - */ - requestParams?: ConnectionRequestParams; } export interface IKibanaSearchRequest { diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 414230b7e5add..00ed4226fea3d 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -29,7 +29,6 @@ import { takeUntil, tap, } from 'rxjs/operators'; -import type { ConnectionRequestParams } from '@elastic/transport'; import { PublicMethodsOf } from '@kbn/utility-types'; import type { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser'; import { BfetchRequestError } from '@kbn/bfetch-plugin/public'; @@ -305,38 +304,18 @@ export class SearchInterceptor { const cancel = () => id && !isSavedToBackground && sendCancelRequest(); - // Async search requires a series of requests - // 1) POST //_async_search/ - // 2..n) GET /_async_search/ - // - // First request contains useful request params for tools like Inspector. - // Preserve and project first request params into responses. - let firstRequestParams: ConnectionRequestParams; - return pollSearch(search, cancel, { pollInterval: this.deps.searchConfig.asyncSearch.pollInterval, ...options, abortSignal: searchAbortController.getSignal(), }).pipe( tap((response) => { - if (!firstRequestParams && response.requestParams) { - firstRequestParams = response.requestParams; - } - id = response.id; if (isCompleteResponse(response)) { searchTracker?.complete(); } }), - map((response) => { - return firstRequestParams - ? { - ...response, - requestParams: firstRequestParams, - } - : response; - }), catchError((e: Error) => { searchTracker?.error(); cancel(); diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index 7248206c8ee95..581920feef89d 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -8,7 +8,6 @@ import { firstValueFrom } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { errors } from '@elastic/elasticsearch'; import { BfetchServerSetup } from '@kbn/bfetch-plugin/server'; import type { ExecutionContextSetup } from '@kbn/core/server'; import apm from 'elastic-apm-node'; @@ -48,12 +47,6 @@ export function registerBsearchRoute( message: err.message, statusCode: err.statusCode, attributes: err.errBody?.error, - // TODO remove 'instanceof errors.ResponseError' check when - // eql strategy throws KbnServerError (like all of the other strategies) - requestParams: - err instanceof errors.ResponseError - ? err.meta?.meta?.request?.params - : err.requestParams, }; }) ) diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 45a7b4d90cd41..d6f5d948c784a 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -77,10 +77,7 @@ export const eqlSearchStrategyProvider = ( meta: true, }); - return toEqlKibanaSearchResponse( - response as TransportResult, - (response as TransportResult).meta?.request?.params - ); + return toEqlKibanaSearchResponse(response as TransportResult); }; const cancel = async () => { diff --git a/src/plugins/data/server/search/strategies/eql_search/response_utils.ts b/src/plugins/data/server/search/strategies/eql_search/response_utils.ts index 48c19c996fd52..f9bdf5bc7de30 100644 --- a/src/plugins/data/server/search/strategies/eql_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/eql_search/response_utils.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; import type { TransportResult } from '@elastic/elasticsearch'; import { EqlSearchResponse } from './types'; import { EqlSearchStrategyResponse } from '../../../../common'; @@ -16,14 +15,12 @@ import { EqlSearchStrategyResponse } from '../../../../common'; * (EQL does not provide _shard info, so total/loaded cannot be calculated.) */ export function toEqlKibanaSearchResponse( - response: TransportResult, - requestParams?: ConnectionRequestParams + response: TransportResult ): EqlSearchStrategyResponse { return { id: response.body.id, rawResponse: response, isPartial: response.body.is_partial, isRunning: response.body.is_running, - ...(requestParams ? { requestParams } : {}), }; } diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts index 679bb5ae2a699..15a6a4df7eed8 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts @@ -113,7 +113,7 @@ describe('ES search strategy', () => { ) ); const [, searchOptions] = esClient.search.mock.calls[0]; - expect(searchOptions).toEqual({ signal: undefined, maxRetries: 5, meta: true }); + expect(searchOptions).toEqual({ signal: undefined, maxRetries: 5 }); }); it('can be aborted', async () => { @@ -131,10 +131,7 @@ describe('ES search strategy', () => { ...params, track_total_hits: true, }); - expect(esClient.search.mock.calls[0][1]).toEqual({ - signal: expect.any(AbortSignal), - meta: true, - }); + expect(esClient.search.mock.calls[0][1]).toEqual({ signal: expect.any(AbortSignal) }); }); it('throws normalized error if ResponseError is thrown', async () => { diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts index 1dc9beb565c79..b2aed5804f248 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts @@ -50,13 +50,12 @@ export const esSearchStrategyProvider = ( ...(terminateAfter ? { terminate_after: terminateAfter } : {}), ...requestParams, }; - const { body, meta } = await esClient.asCurrentUser.search(params, { + const body = await esClient.asCurrentUser.search(params, { signal: abortSignal, ...transport, - meta: true, }); const response = shimHitsTotal(body, options); - return toKibanaSearchResponse(response, meta?.request?.params); + return toKibanaSearchResponse(response); } catch (e) { throw getKbnServerError(e); } diff --git a/src/plugins/data/server/search/strategies/es_search/response_utils.ts b/src/plugins/data/server/search/strategies/es_search/response_utils.ts index 6e364cbbc40bd..4773b6df3bbaf 100644 --- a/src/plugins/data/server/search/strategies/es_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/es_search/response_utils.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ISearchOptions } from '../../../../common'; @@ -25,15 +24,11 @@ export function getTotalLoaded(response: estypes.SearchResponse) { * Get the Kibana representation of this response (see `IKibanaSearchResponse`). * @internal */ -export function toKibanaSearchResponse( - rawResponse: estypes.SearchResponse, - requestParams?: ConnectionRequestParams -) { +export function toKibanaSearchResponse(rawResponse: estypes.SearchResponse) { return { rawResponse, isPartial: false, isRunning: false, - ...(requestParams ? { requestParams } : {}), ...getTotalLoaded(rawResponse), }; } diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index c8322d3083995..298933907b8bb 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -65,7 +65,7 @@ export const enhancedEsSearchStrategyProvider = ( ...(await getDefaultAsyncSubmitParams(uiSettingsClient, searchConfig, options)), ...request.params, }; - const { body, headers, meta } = id + const { body, headers } = id ? await client.asyncSearch.get( { ...params, id }, { ...options.transport, signal: options.abortSignal, meta: true } @@ -78,11 +78,7 @@ export const enhancedEsSearchStrategyProvider = ( const response = shimHitsTotal(body.response, options); - return toAsyncKibanaSearchResponse( - { ...body, response }, - headers?.warning, - meta?.request?.params - ); + return toAsyncKibanaSearchResponse({ ...body, response }, headers?.warning); }; const cancel = async () => { @@ -135,10 +131,8 @@ export const enhancedEsSearchStrategyProvider = ( ); const response = esResponse.body as estypes.SearchResponse; - const requestParams = esResponse.meta?.request?.params; return { rawResponse: shimHitsTotal(response, options), - ...(requestParams ? { requestParams } : {}), ...getTotalLoaded(response), }; } catch (e) { diff --git a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts index 5439e8a618dae..c9390a1b381d5 100644 --- a/src/plugins/data/server/search/strategies/ese_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/response_utils.ts @@ -6,25 +6,19 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; import type { AsyncSearchResponse } from './types'; import { getTotalLoaded } from '../es_search'; /** * Get the Kibana representation of an async search response (see `IKibanaSearchResponse`). */ -export function toAsyncKibanaSearchResponse( - response: AsyncSearchResponse, - warning?: string, - requestParams?: ConnectionRequestParams -) { +export function toAsyncKibanaSearchResponse(response: AsyncSearchResponse, warning?: string) { return { id: response.id, rawResponse: response.response, isPartial: response.is_partial, isRunning: response.is_running, ...(warning ? { warning } : {}), - ...(requestParams ? { requestParams } : {}), ...getTotalLoaded(response.response), }; } diff --git a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts index e61feaba15668..7f3f6f521853d 100644 --- a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts @@ -32,7 +32,7 @@ export const esqlSearchStrategyProvider = ( const search = async () => { try { const { terminateAfter, ...requestParams } = request.params ?? {}; - const { headers, body, meta } = await esClient.asCurrentUser.transport.request( + const { headers, body } = await esClient.asCurrentUser.transport.request( { method: 'POST', path: '/_query', @@ -45,12 +45,10 @@ export const esqlSearchStrategyProvider = ( meta: true, } ); - const transportRequestParams = meta?.request?.params; return { rawResponse: body, isPartial: false, isRunning: false, - ...(transportRequestParams ? { requestParams: transportRequestParams } : {}), warning: headers?.warning, }; } catch (e) { diff --git a/src/plugins/data/server/search/strategies/sql_search/response_utils.ts b/src/plugins/data/server/search/strategies/sql_search/response_utils.ts index 0f4fb3e275f0e..b859df9db4237 100644 --- a/src/plugins/data/server/search/strategies/sql_search/response_utils.ts +++ b/src/plugins/data/server/search/strategies/sql_search/response_utils.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SqlSearchStrategyResponse } from '../../../../common'; @@ -16,8 +15,7 @@ import { SqlSearchStrategyResponse } from '../../../../common'; export function toAsyncKibanaSearchResponse( response: SqlQueryResponse, startTime: number, - warning?: string, - requestParams?: ConnectionRequestParams + warning?: string ): SqlSearchStrategyResponse { return { id: response.id, @@ -26,6 +24,5 @@ export function toAsyncKibanaSearchResponse( isRunning: response.is_running, took: Date.now() - startTime, ...(warning ? { warning } : {}), - ...(requestParams ? { requestParams } : {}), }; } diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts index b6207787d8fbb..c8928a343eec5 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts @@ -9,7 +9,6 @@ import type { IncomingHttpHeaders } from 'http'; import type { IScopedClusterClient, Logger } from '@kbn/core/server'; import { catchError, tap } from 'rxjs/operators'; -import type { DiagnosticResult } from '@elastic/transport'; import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/types'; import { getKbnServerError } from '@kbn/kibana-utils-plugin/server'; import type { ISearchStrategy, SearchStrategyDependencies } from '../../types'; @@ -49,10 +48,9 @@ export const sqlSearchStrategyProvider = ( const { keep_cursor: keepCursor, ...params } = request.params ?? {}; let body: SqlQueryResponse; let headers: IncomingHttpHeaders; - let meta: DiagnosticResult['meta']; if (id) { - ({ body, headers, meta } = await client.sql.getAsync( + ({ body, headers } = await client.sql.getAsync( { format: params?.format ?? 'json', ...getDefaultAsyncGetParams(searchConfig, options), @@ -61,7 +59,7 @@ export const sqlSearchStrategyProvider = ( { ...options.transport, signal: options.abortSignal, meta: true } )); } else { - ({ headers, body, meta } = await client.sql.query( + ({ headers, body } = await client.sql.query( { format: params.format ?? 'json', ...getDefaultAsyncSubmitParams(searchConfig, options), @@ -81,7 +79,7 @@ export const sqlSearchStrategyProvider = ( } } - return toAsyncKibanaSearchResponse(body, startTime, headers?.warning, meta?.request?.params); + return toAsyncKibanaSearchResponse(body, startTime, headers?.warning); }; const cancel = async () => { diff --git a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts deleted file mode 100644 index 37b7a8dd6f283..0000000000000 --- a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { moveRequestParamsToTopLevel } from './move_request_params_to_top_level'; -import { RequestStatus } from './types'; - -describe('moveRequestParamsToTopLevel', () => { - test('should move request meta from error response', () => { - expect( - moveRequestParamsToTopLevel(RequestStatus.ERROR, { - json: { - attributes: {}, - err: { - message: 'simulated error', - requestParams: { - method: 'POST', - path: '/_query', - }, - }, - }, - time: 1, - }) - ).toEqual({ - json: { - attributes: {}, - err: { - message: 'simulated error', - }, - }, - requestParams: { - method: 'POST', - path: '/_query', - }, - time: 1, - }); - }); - - test('should move request meta from ok response', () => { - expect( - moveRequestParamsToTopLevel(RequestStatus.OK, { - json: { - rawResponse: {}, - requestParams: { - method: 'POST', - path: '/_query', - }, - }, - time: 1, - }) - ).toEqual({ - json: { - rawResponse: {}, - }, - requestParams: { - method: 'POST', - path: '/_query', - }, - time: 1, - }); - }); -}); diff --git a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts b/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts deleted file mode 100644 index a00a2d90559c7..0000000000000 --- a/src/plugins/inspector/common/adapters/request/move_request_params_to_top_level.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { ConnectionRequestParams } from '@elastic/transport'; -import { RequestStatus, Response } from './types'; - -interface ErrorResponse { - [key: string]: unknown; - err?: { - [key: string]: unknown; - requestParams?: ConnectionRequestParams; - }; -} - -interface OkResponse { - [key: string]: unknown; - requestParams?: ConnectionRequestParams; -} - -export function moveRequestParamsToTopLevel(status: RequestStatus, response: Response) { - if (status === RequestStatus.ERROR) { - const requestParams = (response.json as ErrorResponse)?.err?.requestParams; - if (!requestParams) { - return response; - } - - const json = { - ...response.json, - err: { ...(response.json as ErrorResponse).err }, - }; - delete json.err.requestParams; - return { - ...response, - json, - requestParams, - }; - } - - const requestParams = (response.json as OkResponse)?.requestParams; - if (!requestParams) { - return response; - } - - const json = { ...response.json } as OkResponse; - delete json.requestParams; - return { - ...response, - json, - requestParams, - }; -} diff --git a/src/plugins/inspector/common/adapters/request/request_responder.ts b/src/plugins/inspector/common/adapters/request/request_responder.ts index cf3a4b6c223da..1d3a999e4834d 100644 --- a/src/plugins/inspector/common/adapters/request/request_responder.ts +++ b/src/plugins/inspector/common/adapters/request/request_responder.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { Request, RequestStatistics, RequestStatus, Response } from './types'; -import { moveRequestParamsToTopLevel } from './move_request_params_to_top_level'; /** * An API to specify information about a specific request that will be logged. @@ -54,7 +53,7 @@ export class RequestResponder { public finish(status: RequestStatus, response: Response): void { this.request.time = response.time ?? Date.now() - this.request.startTime; this.request.status = status; - this.request.response = moveRequestParamsToTopLevel(status, response); + this.request.response = response; this.onChange(); } diff --git a/src/plugins/inspector/common/adapters/request/types.ts b/src/plugins/inspector/common/adapters/request/types.ts index d00e1304f74f5..4e6a8d324559f 100644 --- a/src/plugins/inspector/common/adapters/request/types.ts +++ b/src/plugins/inspector/common/adapters/request/types.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import type { ConnectionRequestParams } from '@elastic/transport'; - /** * The status a request can have. */ @@ -54,8 +52,6 @@ export interface RequestStatistic { } export interface Response { - // TODO replace object with IKibanaSearchResponse once IKibanaSearchResponse is seperated from data plugin. json?: object; - requestParams?: ConnectionRequestParams; time?: number; } diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index 58f5dd44f3f11..5ab50ba33a514 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -12,7 +12,6 @@ /* eslint-disable @elastic/eui/href-or-on-click */ import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import type { ConnectionRequestParams } from '@elastic/transport'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { compressToEncodedURIComponent } from 'lz-string'; @@ -22,7 +21,6 @@ import { InspectorPluginStartDeps } from '../../../../plugin'; interface RequestCodeViewerProps { indexPattern?: string; - requestParams?: ConnectionRequestParams; json: string; } @@ -41,37 +39,19 @@ const openInSearchProfilerLabel = i18n.translate('inspector.requests.openInSearc /** * @internal */ -export const RequestCodeViewer = ({ - indexPattern, - requestParams, - json, -}: RequestCodeViewerProps) => { +export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps) => { const { services } = useKibana(); const navigateToUrl = services.application?.navigateToUrl; - function getValue() { - if (!requestParams) { - return json; - } - - const fullPath = requestParams.querystring - ? `${requestParams.path}?${requestParams.querystring}` - : requestParams.path; - - return `${requestParams.method} ${fullPath}\n${json}`; - } - - const value = getValue(); - - const devToolsDataUri = compressToEncodedURIComponent(value); + const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`); const consoleHref = services.share.url.locators .get('CONSOLE_APP_LOCATOR') ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); // Check if both the Dev Tools UI and the Console UI are enabled. const canShowDevTools = services.application?.capabilities?.dev_tools.show && consoleHref !== undefined; - const shouldShowDevToolsLink = !!(requestParams && canShowDevTools); + const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools); const handleDevToolsLinkClick = useCallback( () => consoleHref && navigateToUrl && navigateToUrl(consoleHref), [consoleHref, navigateToUrl] @@ -155,7 +135,7 @@ export const RequestCodeViewer = ({ { return ( ); diff --git a/src/plugins/kibana_utils/server/report_server_error.ts b/src/plugins/kibana_utils/server/report_server_error.ts index a9fd5d9265bd3..0fcc0c34cc4a9 100644 --- a/src/plugins/kibana_utils/server/report_server_error.ts +++ b/src/plugins/kibana_utils/server/report_server_error.ts @@ -7,22 +7,14 @@ */ import { errors } from '@elastic/elasticsearch'; -import type { ConnectionRequestParams } from '@elastic/transport'; import { KibanaResponseFactory } from '@kbn/core/server'; import { KbnError } from '../common'; export class KbnServerError extends KbnError { public errBody?: Record; - public requestParams?: ConnectionRequestParams; - constructor( - message: string, - public readonly statusCode: number, - errBody?: Record, - requestParams?: ConnectionRequestParams - ) { + constructor(message: string, public readonly statusCode: number, errBody?: Record) { super(message); this.errBody = errBody; - this.requestParams = requestParams; } } @@ -36,8 +28,7 @@ export function getKbnServerError(e: Error) { return new KbnServerError( e.message ?? 'Unknown error', e instanceof errors.ResponseError ? e.statusCode! : 500, - e instanceof errors.ResponseError ? e.body : undefined, - e instanceof errors.ResponseError ? e.meta?.meta?.request?.params : undefined + e instanceof errors.ResponseError ? e.body : undefined ); } @@ -52,7 +43,6 @@ export function reportServerError(res: KibanaResponseFactory, err: KbnServerErro body: { message: err.message, attributes: err.errBody?.error, - ...(err.requestParams ? { requestParams: err.requestParams } : {}), }, }); } diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts index 58f0765a53913..9ce10dc38a643 100644 --- a/test/api_integration/apis/search/bsearch.ts +++ b/test/api_integration/apis/search/bsearch.ts @@ -232,351 +232,6 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); - - describe('request meta', () => { - describe('es', () => { - it(`should return request meta`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - query: { - match_all: {}, - }, - }, - }, - }, - options: { - strategy: 'es', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].result).to.have.property('requestParams'); - expect(jsonBody[0].result.requestParams.method).to.be('POST'); - expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_search'); - expect(jsonBody[0].result.requestParams.querystring).to.be('ignore_unavailable=true'); - }); - - it(`should return request meta when request fails`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - query: { - bool: { - filter: [ - { - error_query: { - indices: [ - { - error_type: 'exception', - message: 'simulated failure', - name: '.kibana', - }, - ], - }, - }, - ], - }, - }, - }, - }, - }, - options: { - strategy: 'es', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].error).to.have.property('requestParams'); - expect(jsonBody[0].error.requestParams.method).to.be('POST'); - expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_search'); - expect(jsonBody[0].error.requestParams.querystring).to.be('ignore_unavailable=true'); - }); - }); - - describe('ese', () => { - it(`should return request meta`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - query: { - match_all: {}, - }, - }, - }, - }, - options: { - strategy: 'ese', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].result).to.have.property('requestParams'); - expect(jsonBody[0].result.requestParams.method).to.be('POST'); - expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_async_search'); - expect(jsonBody[0].result.requestParams.querystring).to.be( - 'batched_reduce_size=64&ccs_minimize_roundtrips=true&wait_for_completion_timeout=200ms&keep_on_completion=false&keep_alive=60000ms&ignore_unavailable=true' - ); - }); - - it(`should return request meta when request fails`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - bool: { - filter: [ - { - error_query: { - indices: [ - { - error_type: 'exception', - message: 'simulated failure', - name: '.kibana', - }, - ], - }, - }, - ], - }, - }, - }, - }, - options: { - strategy: 'ese', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].error).to.have.property('requestParams'); - expect(jsonBody[0].error.requestParams.method).to.be('POST'); - expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_async_search'); - expect(jsonBody[0].error.requestParams.querystring).to.be( - 'batched_reduce_size=64&ccs_minimize_roundtrips=true&wait_for_completion_timeout=200ms&keep_on_completion=false&keep_alive=60000ms&ignore_unavailable=true' - ); - }); - }); - - describe('esql', () => { - it(`should return request meta`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - query: 'from .kibana | limit 1', - }, - }, - options: { - strategy: 'esql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].result).to.have.property('requestParams'); - expect(jsonBody[0].result.requestParams.method).to.be('POST'); - expect(jsonBody[0].result.requestParams.path).to.be('/_query'); - expect(jsonBody[0].result.requestParams.querystring).to.be(''); - }); - - it(`should return request meta when request fails`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - query: 'fro .kibana | limit 1', - }, - }, - options: { - strategy: 'esql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].error).to.have.property('requestParams'); - expect(jsonBody[0].error.requestParams.method).to.be('POST'); - expect(jsonBody[0].error.requestParams.path).to.be('/_query'); - expect(jsonBody[0].error.requestParams.querystring).to.be(''); - }); - }); - - describe('sql', () => { - it(`should return request meta`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - query: 'SELECT * FROM ".kibana" LIMIT 1', - }, - }, - options: { - strategy: 'sql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].result).to.have.property('requestParams'); - expect(jsonBody[0].result.requestParams.method).to.be('POST'); - expect(jsonBody[0].result.requestParams.path).to.be('/_sql'); - expect(jsonBody[0].result.requestParams.querystring).to.be('format=json'); - }); - - it(`should return request meta when request fails`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - query: 'SELEC * FROM ".kibana" LIMIT 1', - }, - }, - options: { - strategy: 'sql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].error).to.have.property('requestParams'); - expect(jsonBody[0].error.requestParams.method).to.be('POST'); - expect(jsonBody[0].error.requestParams.path).to.be('/_sql'); - expect(jsonBody[0].error.requestParams.querystring).to.be('format=json'); - }); - }); - - describe('eql', () => { - it(`should return request meta`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - query: 'any where true', - timestamp_field: 'created_at', - }, - }, - options: { - strategy: 'eql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].result).to.have.property('requestParams'); - expect(jsonBody[0].result.requestParams.method).to.be('POST'); - expect(jsonBody[0].result.requestParams.path).to.be('/.kibana/_eql/search'); - expect(jsonBody[0].result.requestParams.querystring).to.be('ignore_unavailable=true'); - }); - - it(`should return request meta when request fails`, async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - query: 'any where true', - }, - }, - options: { - strategy: 'eql', - }, - }, - ], - }); - - const jsonBody = parseBfetchResponse(resp); - - expect(resp.status).to.be(200); - expect(jsonBody[0].error).to.have.property('requestParams'); - expect(jsonBody[0].error.requestParams.method).to.be('POST'); - expect(jsonBody[0].error.requestParams.path).to.be('/.kibana/_eql/search'); - expect(jsonBody[0].error.requestParams.querystring).to.be('ignore_unavailable=true'); - }); - }); - }); }); }); } diff --git a/test/functional/apps/visualize/group2/_inspector.ts b/test/functional/apps/visualize/group2/_inspector.ts index 077a37a90c06c..80cfc42ab3cd6 100644 --- a/test/functional/apps/visualize/group2/_inspector.ts +++ b/test/functional/apps/visualize/group2/_inspector.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); + const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { @@ -40,8 +41,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.open(); await inspector.openInspectorRequestsView(); - const { body } = await inspector.getRequest(1); - expect(body.aggs['2'].max).property('missing', 10); + const requestTab = await inspector.getOpenRequestDetailRequestButton(); + await requestTab.click(); + const requestJSON = JSON.parse(await monacoEditor.getCodeEditorValue(1)); + + expect(requestJSON.aggs['2'].max).property('missing', 10); }); after(async () => { diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 7313187047a18..6222405aa6dae 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -299,21 +299,6 @@ export class InspectorService extends FtrService { return this.testSubjects.find('inspectorRequestDetailResponse'); } - public async getRequest( - codeEditorIndex: number = 0 - ): Promise<{ command: string; body: Record }> { - await (await this.getOpenRequestDetailRequestButton()).click(); - - await this.monacoEditor.waitCodeEditorReady('inspectorRequestCodeViewerContainer'); - const requestString = await this.monacoEditor.getCodeEditorValue(codeEditorIndex); - this.log.debug('Request string from inspector:', requestString); - const openBraceIndex = requestString.indexOf('{'); - return { - command: openBraceIndex >= 0 ? requestString.substring(0, openBraceIndex).trim() : '', - body: openBraceIndex >= 0 ? JSON.parse(requestString.substring(openBraceIndex)) : {}, - }; - } - public async getResponse(): Promise> { await (await this.getOpenRequestDetailResponseButton()).click(); diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index c7bb37566b0fe..90528b3321d22 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index ac809c756d2cc..4d94b530c86e2 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 6b52c8de57ae5..8c4e9fc5cd523 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 21e213ebb9c27..f142588711a31 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index afaac18bf342d..291e6b40e6bfd 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 03f74cc01d3e3..381d3afc54067 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 6e12a10d1e283..90528b3321d22 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index cb14c6ea89407..4d94b530c86e2 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json index 0910e67409423..8c4e9fc5cd523 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 21e213ebb9c27..f142588711a31 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index f340c5b653e35..291e6b40e6bfd 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index ecbafbbc0afba..381d3afc54067 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index 030b5c9bbed4c..bb2aec9d5819f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -128,6 +128,8 @@ "@kbn/ci-stats-shipper-cli/*": ["packages/kbn-ci-stats-shipper-cli/*"], "@kbn/cli-dev-mode": ["packages/kbn-cli-dev-mode"], "@kbn/cli-dev-mode/*": ["packages/kbn-cli-dev-mode/*"], + "@kbn/cloud": ["packages/cloud"], + "@kbn/cloud/*": ["packages/cloud/*"], "@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"], "@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"], "@kbn/cloud-chat-provider-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat_provider"], diff --git a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx index 20e4ca43e233b..5b2a9d880c1b8 100644 --- a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx +++ b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx @@ -109,7 +109,9 @@ export const FullTimeRangeSelector: FC = (props) => toasts, http, query, - showFrozenDataTierChoice ? frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE : false, + showFrozenDataTierChoice === false + ? false + : frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE, apiPath ); if (typeof callback === 'function' && fullTimeRange !== undefined) { @@ -192,9 +194,16 @@ export const FullTimeRangeSelector: FC = (props) => [sortOptions, frozenDataPreference, setPreference] ); - const buttonTooltip = useMemo( - () => - frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + const buttonTooltip = useMemo(() => { + if (showFrozenDataTierChoice === false) { + return ( + + ); + } else { + return frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( = (props) => id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip" defaultMessage="Use full range of data including frozen data tier, which might have slower search results." /> - ), - [frozenDataPreference] - ); + ); + } + }, [frozenDataPreference, showFrozenDataTierChoice]); return ( @@ -222,7 +231,7 @@ export const FullTimeRangeSelector: FC = (props) => /> - {showFrozenDataTierChoice ? ( + {showFrozenDataTierChoice === false ? null : ( = (props) => {popoverContent} - ) : null} + )} ); }; diff --git a/x-pack/performance/journeys/many_fields_discover.ts b/x-pack/performance/journeys/many_fields_discover.ts index a37207f6e092d..2a801dea4478f 100644 --- a/x-pack/performance/journeys/many_fields_discover.ts +++ b/x-pack/performance/journeys/many_fields_discover.ts @@ -13,7 +13,11 @@ export const journey = new Journey({ esArchives: ['test/functional/fixtures/es_archiver/many_fields'], }) .step('Go to Discover Page', async ({ page, kbnUrl, kibanaPage }) => { - await page.goto(kbnUrl.get(`/app/discover`)); + await page.goto( + kbnUrl.get( + `/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))&_a=(columns:!(),filters:!(),index:'35796250-bb09-11ec-a8e4-a9868e049a39',interval:auto,query:(language:kuery,query:''),sort:!())` + ) + ); await kibanaPage.waitForHeader(); await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]'); await page.waitForSelector(subj('globalLoadingIndicator-hidden')); diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx index 45b2ccc1c097a..c07af22a5f16a 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx @@ -356,6 +356,7 @@ const FieldPanel: FC = ({ = ({ !prevState)} aria-label={i18n.translate('xpack.aiops.changePointDetection.expandConfigLabel', { @@ -480,6 +482,7 @@ const FieldPanel: FC = ({ id={`panelContextMenu_${panelIndex}`} button={ = ({ <> openInDiscover(QUERY_MODE.INCLUDE)} iconType="plusInCircle" @@ -61,6 +62,7 @@ export const TableHeader: FC = ({ openInDiscover(QUERY_MODE.EXCLUDE)} iconType="minusInCircle" diff --git a/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx b/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx index 77ba11fd46cc5..208302083fd0c 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx @@ -43,7 +43,12 @@ export const LoadingCategorization: FC = ({ onClose }) => ( - onClose()}>Cancel + onClose()} + > + Cancel + diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index 5ccdca64d1036..bfa609bb5dd21 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -324,7 +324,12 @@ export const LogCategorizationPage: FC = () => { /> ) : ( - cancelRequest()}>Cancel + cancelRequest()} + > + Cancel + )} diff --git a/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/random_sampler_range_slider.tsx b/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/random_sampler_range_slider.tsx index 4f2b62729ca46..2cd70b35d139e 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/random_sampler_range_slider.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/random_sampler_range_slider.tsx @@ -92,6 +92,7 @@ export const RandomSamplerRangeSlider = ({ data-test-subj="dvRandomSamplerProbabilityRange" append={ { if (setSamplingProbability && isDefined(samplingProbabilityInput)) { diff --git a/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/sampling_menu.tsx b/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/sampling_menu.tsx index e4d18f6dbc260..c4e16d4fabbe6 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/sampling_menu.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/sampling_menu/sampling_menu.tsx @@ -118,6 +118,7 @@ export const SamplingMenu: FC = ({ randomSampler, reload }) => { id="aiopsSamplingOptions" button={ setShowSamplingOptionsPopover(!showSamplingOptionsPopover)} iconSide="right" iconType="arrowDown" diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx index f1b1a6d38de12..a62db054d4d52 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx @@ -409,7 +409,11 @@ export const LogRateAnalysisResults: FC = ({ )} {overrides !== undefined ? (

- startHandler(true)}> + startHandler(true)} + > void; + core: CoreStart; + docLinks: DocLinksStart; + cloud: CloudStart; + share: SharePluginStart; +} + +export const EndpointsModal = ({ core, share, cloud, docLinks, closeModal }: Props) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx similarity index 53% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx index 82b0e86e6569a..15270c5876214 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx @@ -4,17 +4,32 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; import { ChromeHelpMenuLink } from '@kbn/core-chrome-browser'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { CoreStart } from '@kbn/core/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; + +import { EndpointsModal } from './endpoints_modal'; export const createHelpMenuLinks = ({ docLinks, helpSupportUrl, + core, + cloud, + share, }: { docLinks: DocLinksStart; + core: CoreStart; + cloud: CloudStart; + share: SharePluginStart; helpSupportUrl: string; }) => { + const { overlays } = core; + const helpMenuLinks: ChromeHelpMenuLink[] = [ { title: i18n.translate('xpack.cloudLinks.helpMenuLinks.documentation', { @@ -34,6 +49,27 @@ export const createHelpMenuLinks = ({ }), href: docLinks.links.kibana.feedback, }, + { + title: i18n.translate('xpack.cloudLinks.helpMenuLinks.endpoints', { + defaultMessage: 'Endpoints', + }), + iconType: 'console', + dataTestSubj: 'endpointsHelpLink', + onClick: () => { + const modal = overlays.openModal( + toMountPoint( + modal.close()} + />, + { theme: core.theme, i18n: core.i18n } + ) + ); + }, + }, ]; return helpMenuLinks; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts index b9045fdc9a59f..d680d6cce4f4f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts @@ -8,6 +8,7 @@ import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; import { coreMock } from '@kbn/core/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; import { maybeAddCloudLinks } from './maybe_add_cloud_links'; @@ -18,6 +19,7 @@ describe('maybeAddCloudLinks', () => { maybeAddCloudLinks({ core, security, + share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: false }, }); // Since there's a promise, let's wait for the next tick @@ -35,6 +37,7 @@ describe('maybeAddCloudLinks', () => { maybeAddCloudLinks({ security, core, + share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, }); // Since there's a promise, let's wait for the next tick @@ -90,6 +93,12 @@ describe('maybeAddCloudLinks', () => { "href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback", "title": "Give feedback", }, + Object { + "dataTestSubj": "endpointsHelpLink", + "iconType": "console", + "onClick": [Function], + "title": "Endpoints", + }, ], ] `); @@ -103,6 +112,7 @@ describe('maybeAddCloudLinks', () => { maybeAddCloudLinks({ security, core, + share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, }); // Since there's a promise, let's wait for the next tick @@ -157,6 +167,12 @@ describe('maybeAddCloudLinks', () => { "href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback", "title": "Give feedback", }, + Object { + "dataTestSubj": "endpointsHelpLink", + "iconType": "console", + "onClick": [Function], + "title": "Endpoints", + }, ], ] `); @@ -172,6 +188,7 @@ describe('maybeAddCloudLinks', () => { maybeAddCloudLinks({ security, core, + share: sharePluginMock.createStartContract(), cloud: { ...cloudMock.createStart(), isCloudEnabled: true }, }); // Since there's a promise, let's wait for the next tick diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts index 33fb4df7bfce2..2772c87d124d3 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { CoreStart } from '@kbn/core/public'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { createUserMenuLinks } from './user_menu_links'; import { createHelpMenuLinks } from './help_menu_links'; @@ -18,9 +19,10 @@ export interface MaybeAddCloudLinksDeps { core: CoreStart; security: SecurityPluginStart; cloud: CloudStart; + share: SharePluginStart; } -export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinksDeps): void { +export function maybeAddCloudLinks({ core, security, cloud, share }: MaybeAddCloudLinksDeps): void { const userObservable = defer(() => security.authc.getCurrentUser()).pipe( // Check if user is a cloud user. map((user) => user.elastic_cloud_user), @@ -54,6 +56,9 @@ export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinks const helpMenuLinks = createHelpMenuLinks({ docLinks: core.docLinks, helpSupportUrl, + core, + share, + cloud, }); core.chrome.setHelpMenuLinks(helpMenuLinks); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts index d928b7a6f0e8a..d2f987337a440 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts @@ -11,6 +11,7 @@ import { coreMock } from '@kbn/core/public/mocks'; import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; describe('Cloud Links Plugin - public', () => { let plugin: CloudLinksPlugin; @@ -40,7 +41,11 @@ describe('Cloud Links Plugin - public', () => { coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; - plugin.start(coreStart, { cloud, guidedOnboarding }); + plugin.start(coreStart, { + cloud, + guidedOnboarding, + share: sharePluginMock.createStartContract(), + }); expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).toHaveBeenCalledTimes(1); }); @@ -48,14 +53,22 @@ describe('Cloud Links Plugin - public', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; - plugin.start(coreStart, { cloud, guidedOnboarding }); + plugin.start(coreStart, { + cloud, + guidedOnboarding, + share: sharePluginMock.createStartContract(), + }); expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled(); }); test('does not register the Onboarding Setup Guide link when cloud is not enabled', () => { const coreStart = coreMock.createStart(); const cloud = { ...cloudMock.createStart(), isCloudEnabled: false }; - plugin.start(coreStart, { cloud, guidedOnboarding }); + plugin.start(coreStart, { + cloud, + guidedOnboarding, + share: sharePluginMock.createStartContract(), + }); expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled(); }); }); @@ -72,7 +85,11 @@ describe('Cloud Links Plugin - public', () => { coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; - plugin.start(coreStart, { cloud, guidedOnboarding }); + plugin.start(coreStart, { + cloud, + guidedOnboarding, + share: sharePluginMock.createStartContract(), + }); expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled(); }); }); @@ -83,7 +100,7 @@ describe('Cloud Links Plugin - public', () => { coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; const security = securityMock.createStart(); - plugin.start(coreStart, { cloud, security }); + plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() }); expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(1); }); @@ -91,7 +108,7 @@ describe('Cloud Links Plugin - public', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; - plugin.start(coreStart, { cloud }); + plugin.start(coreStart, { cloud, share: sharePluginMock.createStartContract() }); expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0); }); @@ -100,7 +117,7 @@ describe('Cloud Links Plugin - public', () => { coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); const cloud = { ...cloudMock.createStart(), isCloudEnabled: true }; const security = securityMock.createStart(); - plugin.start(coreStart, { cloud, security }); + plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() }); expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0); }); @@ -108,7 +125,7 @@ describe('Cloud Links Plugin - public', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const security = securityMock.createStart(); - plugin.start(coreStart, { security }); + plugin.start(coreStart, { security, share: sharePluginMock.createStartContract() }); expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0); }); @@ -117,7 +134,7 @@ describe('Cloud Links Plugin - public', () => { coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); const cloud = { ...cloudMock.createStart(), isCloudEnabled: false }; const security = securityMock.createStart(); - plugin.start(coreStart, { cloud, security }); + plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() }); expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx index 38b568791b70b..bfebe531276d4 100755 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx @@ -11,6 +11,7 @@ import type { CoreStart, Plugin } from '@kbn/core/public'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { maybeAddCloudLinks } from './maybe_add_cloud_links'; interface CloudLinksDepsSetup { @@ -21,6 +22,7 @@ interface CloudLinksDepsSetup { interface CloudLinksDepsStart { cloud?: CloudStart; security?: SecurityPluginStart; + share: SharePluginStart; guidedOnboarding?: GuidedOnboardingPluginStart; } @@ -29,7 +31,7 @@ export class CloudLinksPlugin { public setup() {} - public start(core: CoreStart, { cloud, security, guidedOnboarding }: CloudLinksDepsStart) { + public start(core: CoreStart, { cloud, security, guidedOnboarding, share }: CloudLinksDepsStart) { if (cloud?.isCloudEnabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname)) { if (guidedOnboarding?.guidedOnboardingApi?.isEnabled) { core.chrome.registerGlobalHelpExtensionMenuLink({ @@ -42,11 +44,13 @@ export class CloudLinksPlugin priority: 1000, // We want this link to be at the very top. }); } + if (security) { maybeAddCloudLinks({ core, security, cloud, + share, }); } } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json index f1a67895cdd5e..43f411cadf060 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json @@ -23,6 +23,9 @@ "@kbn/user-profile-components", "@kbn/core-lifecycle-browser", "@kbn/kibana-react-plugin", + "@kbn/share-plugin", + "@kbn/cloud", + "@kbn/react-kibana-mount", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 47f62c8e19794..6ef34d045e20f 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -51,6 +51,7 @@ export interface FleetConfigType { fleetServerStandalone: boolean; onlyAllowAgentUpgradeToKnownVersions: boolean; activeAgentsSoftLimit?: number; + retrySetupOnBoot: boolean; registry: { kibanaVersionCheckEnabled: boolean; capabilities: string[]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx deleted file mode 100644 index 7e3b414cf0f6d..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; 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 { - EuiPopover, - EuiText, - EuiForm, - EuiFormRow, - EuiFieldText, - EuiCopy, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiLink, - EuiHeaderLink, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export interface Props { - cloudId: string; - managementUrl?: string; - learnMoreUrl: string; -} - -const Description = styled(EuiText)` - margin-bottom: ${({ theme }) => theme.eui.euiSizeL}; -`; - -export const DeploymentDetails = ({ cloudId, learnMoreUrl, managementUrl }: Props) => { - const [isOpen, setIsOpen] = React.useState(false); - - const button = ( - setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive> - {i18n.translate('xpack.fleet.integrations.deploymentButton', { - defaultMessage: 'View deployment details', - })} - - ); - - const management = managementUrl ? ( - - - - Create and manage API keys - - - - Learn more - - - - - ) : null; - - return ( - setIsOpen(false)} - button={button} - anchorPosition="downCenter" - > -

- - {i18n.translate('xpack.fleet.integrations.deploymentDescription', { - defaultMessage: - 'Send data to Elastic from your applications by referencing your deployment.', - })} - - - - - - - - - - {(copy) => ( - - )} - - - - - {management} - -
-
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx deleted file mode 100644 index 5b311b3443e36..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import type { Meta } from '@storybook/react'; -import { EuiHeader } from '@elastic/eui'; - -import { DeploymentDetails as ConnectedComponent } from './deployment_details'; -import type { Props as PureComponentProps } from './deployment_details.component'; -import { DeploymentDetails as PureComponent } from './deployment_details.component'; - -export default { - title: 'Sections/EPM/Deployment Details', - description: '', - decorators: [ - (storyFn) => { - const sections = [{ items: [] }, { items: [storyFn()] }]; - return ; - }, - ], -} as Meta; - -export const DeploymentDetails = () => { - return ; -}; - -DeploymentDetails.args = { - isCloudEnabled: true, -}; - -DeploymentDetails.argTypes = { - isCloudEnabled: { - type: { - name: 'boolean', - }, - defaultValue: true, - control: { - type: 'boolean', - }, - }, -}; - -export const Component = (props: PureComponentProps) => { - return ; -}; - -Component.args = { - cloudId: 'cloud-id', - learnMoreUrl: 'https://learn-more-url', - managementUrl: 'https://management-url', -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx index 968d596a5f9d5..ea17bc3f201c4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx @@ -6,13 +6,18 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover, EuiHeaderLink } from '@elastic/eui'; +import { + DeploymentDetailsKibanaProvider, + DeploymentDetails as DeploymentDetailsComponent, +} from '@kbn/cloud/deployment_details'; import { useStartServices } from '../../hooks'; -import { DeploymentDetails as Component } from './deployment_details.component'; - export const DeploymentDetails = () => { - const { share, cloud, docLinks } = useStartServices(); + const [isOpen, setIsOpen] = React.useState(false); + const { share, cloud, docLinks, application } = useStartServices(); // If the cloud plugin isn't enabled, we can't display the flyout. if (!cloud) { @@ -21,16 +26,36 @@ export const DeploymentDetails = () => { const { isCloudEnabled, cloudId } = cloud; - // If cloud isn't enabled or we don't have a cloudId we can't display the flyout. + // If cloud isn't enabled or we don't have a cloudId we don't render the button. if (!isCloudEnabled || !cloudId) { return null; } - const managementUrl = share.url.locators - .get('MANAGEMENT_APP_LOCATOR') - ?.useUrl({ sectionId: 'security', appId: 'api_keys' }); - - const learnMoreUrl = docLinks.links.fleet.apiKeysLearnMore; - - return ; + const button = ( + setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive> + {i18n.translate('xpack.fleet.integrations.endpointsButton', { + defaultMessage: 'Endpoints', + })} + + ); + + return ( + + setIsOpen(false)} + button={button} + anchorPosition="downCenter" + > +
+ +
+
+
+ ); }; diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index b68684460bf81..f1210a49f7f0c 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -188,6 +188,7 @@ export const config: PluginConfigDescriptor = { min: 0, }) ), + retrySetupOnBoot: schema.boolean({ defaultValue: false }), registry: schema.object( { kibanaVersionCheckEnabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts b/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts index b841c641c3af4..e2e7e9f7887e6 100644 --- a/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/fleet_usage_telemetry.test.ts @@ -20,8 +20,7 @@ import { waitForFleetSetup } from './helpers'; const logFilePath = path.join(__dirname, 'logs.log'); -// Failing: See https://github.com/elastic/kibana/issues/156245 -describe.skip('fleet usage telemetry', () => { +describe('fleet usage telemetry', () => { let core: any; let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; @@ -218,7 +217,7 @@ describe.skip('fleet usage telemetry', () => { version: '8.6.0', }, last_checkin_status: 'online', - last_checkin: '2023-09-13T12:26:24Z', + last_checkin: new Date(Date.now() - 1000 * 60 * 6).toISOString(), active: true, policy_id: 'policy2', }, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 9603eb2b47064..e0aa5315d8ff9 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { backOff } from 'exponential-backoff'; import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { take, filter } from 'rxjs/operators'; @@ -532,9 +533,39 @@ export class FleetPlugin ) .toPromise(); - await setupFleet( - new SavedObjectsClient(core.savedObjects.createInternalRepository()), - core.elasticsearch.client.asInternalUser + // Retry Fleet setup w/ backoff + await backOff( + async () => { + await setupFleet( + new SavedObjectsClient(core.savedObjects.createInternalRepository()), + core.elasticsearch.client.asInternalUser + ); + }, + { + // We only retry when this feature flag is enabled + numOfAttempts: this.configInitialValue.internal?.retrySetupOnBoot ? Infinity : 1, + // 250ms initial backoff + startingDelay: 250, + // 5m max backoff + maxDelay: 60000 * 5, + timeMultiple: 2, + // avoid HA contention with other Kibana instances + jitter: 'full', + retry: (error: any, attemptCount: number) => { + const summary = `Fleet setup attempt ${attemptCount} failed, will retry after backoff`; + logger.debug(summary, { error: { message: error } }); + + this.fleetStatus$.next({ + level: ServiceStatusLevels.available, + summary, + meta: { + attemptCount, + error, + }, + }); + return true; + }, + } ); this.fleetStatus$.next({ @@ -542,8 +573,7 @@ export class FleetPlugin summary: 'Fleet is available', }); } catch (error) { - logger.warn('Fleet setup failed'); - logger.warn(error); + logger.warn('Fleet setup failed', { error: { message: error } }); this.fleetStatus$.next({ // As long as Fleet has a dependency on EPR, we can't reliably set Kibana status to `unavailable` here. diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index b7fe0d95310ef..af3460e266af1 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -135,6 +135,7 @@ describe('_installPackage', () => { disableProxies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, + retrySetupOnBoot: false, registry: { kibanaVersionCheckEnabled: true, capabilities: [], @@ -192,6 +193,7 @@ describe('_installPackage', () => { disableILMPolicies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, + retrySetupOnBoot: false, registry: { kibanaVersionCheckEnabled: true, capabilities: [], @@ -265,6 +267,7 @@ describe('_installPackage', () => { disableProxies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, + retrySetupOnBoot: false, registry: { kibanaVersionCheckEnabled: true, capabilities: [], diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index b3f8a96417f9a..58cdfa25d1e08 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -101,5 +101,6 @@ "@kbn/core-saved-objects-base-server-internal", "@kbn/core-http-common", "@kbn/dashboard-plugin", + "@kbn/cloud", ] } diff --git a/x-pack/plugins/infra/common/inventory_models/host/index.ts b/x-pack/plugins/infra/common/inventory_models/host/index.ts index 04aa00fbdf2be..18af8bbfadc99 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/index.ts @@ -8,10 +8,6 @@ import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; -import { - aws as awsRequiredMetrics, - nginx as nginxRequireMetrics, -} from '../shared/metrics/required_metrics'; export { hostSnapshotMetricTypes } from './metrics'; @@ -38,19 +34,5 @@ export const host: InventoryModel = { cloudProvider: 'cloud.provider', }, metrics, - requiredMetrics: [ - 'hostSystemOverview', - 'hostCpuUsage', - 'hostLoad', - 'hostMemoryUsage', - 'hostNetworkTraffic', - 'hostK8sOverview', - 'hostK8sCpuCap', - 'hostK8sMemoryCap', - 'hostK8sDiskCap', - 'hostK8sPodCap', - ...awsRequiredMetrics, - ...nginxRequireMetrics, - ], tooltipMetrics: ['cpu', 'memory', 'tx', 'rx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/index.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/index.ts index e59aaefb2b82b..3eff64ccb6a7a 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/index.ts @@ -18,24 +18,6 @@ import { normalizedLoad1m } from './snapshot/normalized_load_1m'; import { rx } from './snapshot/rx'; import { tx } from './snapshot/tx'; -import { hostSystemOverview } from './tsvb/host_system_overview'; -import { hostCpuUsage } from './tsvb/host_cpu_usage'; -import { hostLoad } from './tsvb/host_load'; -import { hostMemoryUsage } from './tsvb/host_memory_usage'; -import { hostNetworkTraffic } from './tsvb/host_network_traffic'; -import { hostFilesystem } from './tsvb/host_filesystem'; - -import { hostK8sOverview } from './tsvb/host_k8s_overview'; -import { hostK8sCpuCap } from './tsvb/host_k8s_cpu_cap'; -import { hostK8sPodCap } from './tsvb/host_k8s_pod_cap'; -import { hostK8sDiskCap } from './tsvb/host_k8s_disk_cap'; -import { hostK8sMemoryCap } from './tsvb/host_k8s_memory_cap'; - -import { hostDockerTop5ByMemory } from './tsvb/host_docker_top_5_by_memory'; -import { hostDockerTop5ByCpu } from './tsvb/host_docker_top_5_by_cpu'; -import { hostDockerOverview } from './tsvb/host_docker_overview'; -import { hostDockerInfo } from './tsvb/host_docker_info'; - import { InventoryMetrics } from '../../types'; const exposedHostSnapshotMetrics = { @@ -59,23 +41,6 @@ export const hostSnapshotMetricTypes = Object.keys(exposedHostSnapshotMetrics) a >; export const metrics: InventoryMetrics = { - tsvb: { - hostSystemOverview, - hostCpuUsage, - hostLoad, - hostMemoryUsage, - hostNetworkTraffic, - hostFilesystem, - hostK8sOverview, - hostK8sCpuCap, - hostK8sPodCap, - hostK8sDiskCap, - hostK8sMemoryCap, - hostDockerOverview, - hostDockerInfo, - hostDockerTop5ByMemory, - hostDockerTop5ByCpu, - }, snapshot: hostSnapshotMetrics, defaultSnapshot: 'cpu', defaultTimeRangeInSeconds: 3600, // 1 hour diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_cpu_usage.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_cpu_usage.ts deleted file mode 100644 index bcafeb4ebc4cf..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_cpu_usage.ts +++ /dev/null @@ -1,254 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostCpuUsage: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostCpuUsage', - requires: ['system.cpu'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'user', - metrics: [ - { - field: 'system.cpu.user.pct', - id: 'avg-cpu-user', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-user', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'system', - metrics: [ - { - field: 'system.cpu.system.pct', - id: 'avg-cpu-system', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-system', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'steal', - metrics: [ - { - field: 'system.cpu.steal.pct', - id: 'avg-cpu-steal', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'avg-cpu-steal', - id: 'var-avg', - name: 'avg', - }, - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'irq', - metrics: [ - { - field: 'system.cpu.irq.pct', - id: 'avg-cpu-irq', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-irq', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'softirq', - metrics: [ - { - field: 'system.cpu.softirq.pct', - id: 'avg-cpu-softirq', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-softirq', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'iowait', - metrics: [ - { - field: 'system.cpu.iowait.pct', - id: 'avg-cpu-iowait', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-iowait', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'nice', - metrics: [ - { - field: 'system.cpu.nice.pct', - id: 'avg-cpu-nice', - type: 'avg', - }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - id: 'calc-avg-cores', - script: 'params.avg / params.cores', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - { - field: 'avg-cpu-nice', - id: 'var-avg', - name: 'avg', - }, - ], - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_info.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_info.ts deleted file mode 100644 index 4cc2b574362d7..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_info.ts +++ /dev/null @@ -1,56 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostDockerInfo: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostDockerInfo', - requires: ['docker.info'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'running', - metrics: [ - { - field: 'docker.info.containers.running', - id: 'max-running', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'paused', - metrics: [ - { - field: 'docker.info.containers.paused', - id: 'max-paused', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'stopped', - metrics: [ - { - field: 'docker.info.containers.stopped', - id: 'max-stopped', - type: 'max', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts deleted file mode 100644 index df56a21dbf5b7..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_overview.ts +++ /dev/null @@ -1,67 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostDockerOverview: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostDockerOverview', - requires: ['docker.info'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'top_n', - series: [ - { - id: 'total', - metrics: [ - { - field: 'docker.info.containers.total', - id: 'max-total', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'running', - metrics: [ - { - field: 'docker.info.containers.running', - id: 'max-running', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'paused', - metrics: [ - { - field: 'docker.info.containers.paused', - id: 'max-paused', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'stopped', - metrics: [ - { - field: 'docker.info.containers.stopped', - id: 'max-stopped', - type: 'max', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_cpu.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_cpu.ts deleted file mode 100644 index fd9eb97419736..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_cpu.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostDockerTop5ByCpu: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostDockerTop5ByCpu', - requires: ['docker.cpu'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'avg-cpu', - metrics: [ - { - field: 'docker.cpu.total.pct', - id: 'avg-cpu-metric', - type: 'avg', - }, - ], - split_mode: 'terms', - terms_field: 'container.name', - terms_order_by: 'avg-cpu', - terms_size: 5, - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_memory.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_memory.ts deleted file mode 100644 index cad828671d232..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_docker_top_5_by_memory.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostDockerTop5ByMemory: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostDockerTop5ByMemory', - requires: ['docker.memory'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'avg-memory', - metrics: [ - { - field: 'docker.memory.usage.pct', - id: 'avg-memory-metric', - type: 'avg', - }, - ], - split_mode: 'terms', - terms_field: 'container.name', - terms_order_by: 'avg-memory', - terms_size: 5, - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_filesystem.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_filesystem.ts deleted file mode 100644 index ce284345410fc..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_filesystem.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostFilesystem: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostFilesystem', - requires: ['system.filesystem'], - filter: 'system.filesystem.device_name:\\/*', - index_pattern: indexPattern, - time_field: timeField, - interval, - type: 'timeseries', - series: [ - { - id: 'used', - metrics: [ - { - field: 'system.filesystem.used.pct', - id: 'avg-filesystem-used', - type: 'avg', - }, - ], - split_mode: 'terms', - terms_field: 'system.filesystem.device_name', - terms_order_by: 'used', - terms_size: 5, - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_cpu_cap.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_cpu_cap.ts deleted file mode 100644 index d33e4cdeb34cd..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_cpu_cap.ts +++ /dev/null @@ -1,58 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostK8sCpuCap: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostK8sCpuCap', - map_field_to: 'kubernetes.node.name', - requires: ['kubernetes.node'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'capacity', - metrics: [ - { - field: 'kubernetes.node.cpu.allocatable.cores', - id: 'max-cpu-cap', - type: 'max', - }, - { - id: 'calc-nanocores', - type: 'calculation', - variables: [ - { - id: 'var-cores', - field: 'max-cpu-cap', - name: 'cores', - }, - ], - script: 'params.cores * 1000000000', - }, - ], - split_mode: 'everything', - }, - { - id: 'used', - metrics: [ - { - field: 'kubernetes.node.cpu.usage.nanocores', - id: 'avg-cpu-usage', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_disk_cap.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_disk_cap.ts deleted file mode 100644 index e9e512a136631..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_disk_cap.ts +++ /dev/null @@ -1,45 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; -export const hostK8sDiskCap: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostK8sDiskCap', - map_field_to: 'kubernetes.node.name', - requires: ['kubernetes.node'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'capacity', - metrics: [ - { - field: 'kubernetes.node.fs.capacity.bytes', - id: 'max-fs-cap', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'used', - metrics: [ - { - field: 'kubernetes.node.fs.used.bytes', - id: 'avg-fs-used', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_memory_cap.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_memory_cap.ts deleted file mode 100644 index ccc227ef1854e..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_memory_cap.ts +++ /dev/null @@ -1,46 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostK8sMemoryCap: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostK8sMemoryCap', - map_field_to: 'kubernetes.node.name', - requires: ['kubernetes.node'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'capacity', - metrics: [ - { - field: 'kubernetes.node.memory.allocatable.bytes', - id: 'max-memory-cap', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'used', - metrics: [ - { - field: 'kubernetes.node.memory.usage.bytes', - id: 'avg-memory-usage', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts deleted file mode 100644 index 2da74d50dff1b..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_overview.ts +++ /dev/null @@ -1,155 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostK8sOverview: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostK8sOverview', - requires: ['kubernetes'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'top_n', - series: [ - { - id: 'cpucap', - split_mode: 'everything', - metrics: [ - { - field: 'kubernetes.node.cpu.allocatable.cores', - id: 'max-cpu-cap', - type: 'max', - }, - { - field: 'kubernetes.node.cpu.usage.nanocores', - id: 'avg-cpu-usage', - type: 'avg', - }, - { - id: 'calc-used-cap', - script: 'params.used / (params.cap * 1000000000)', - type: 'calculation', - variables: [ - { - field: 'max-cpu-cap', - id: 'var-cap', - name: 'cap', - }, - { - field: 'avg-cpu-usage', - id: 'var-used', - name: 'used', - }, - ], - }, - ], - }, - { - id: 'diskcap', - metrics: [ - { - field: 'kubernetes.node.fs.capacity.bytes', - id: 'max-fs-cap', - type: 'max', - }, - { - field: 'kubernetes.node.fs.used.bytes', - id: 'avg-fs-used', - type: 'avg', - }, - { - id: 'calc-used-cap', - script: 'params.used / params.cap', - type: 'calculation', - variables: [ - { - field: 'max-fs-cap', - id: 'var-cap', - name: 'cap', - }, - { - field: 'avg-fs-used', - id: 'var-used', - name: 'used', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'memorycap', - metrics: [ - { - field: 'kubernetes.node.memory.allocatable.bytes', - id: 'max-memory-cap', - type: 'max', - }, - { - field: 'kubernetes.node.memory.usage.bytes', - id: 'avg-memory-usage', - type: 'avg', - }, - { - id: 'calc-used-cap', - script: 'params.used / params.cap', - type: 'calculation', - variables: [ - { - field: 'max-memory-cap', - id: 'var-cap', - name: 'cap', - }, - { - field: 'avg-memory-usage', - id: 'var-used', - name: 'used', - }, - ], - }, - ], - split_mode: 'everything', - }, - { - id: 'podcap', - metrics: [ - { - field: 'kubernetes.node.pod.capacity.total', - id: 'max-pod-cap', - type: 'max', - }, - { - field: 'kubernetes.pod.uid', - id: 'card-pod-name', - type: 'cardinality', - }, - { - id: 'calc-used-cap', - script: 'params.used / params.cap', - type: 'calculation', - variables: [ - { - field: 'max-pod-cap', - id: 'var-cap', - name: 'cap', - }, - { - field: 'card-pod-name', - id: 'var-used', - name: 'used', - }, - ], - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_pod_cap.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_pod_cap.ts deleted file mode 100644 index 85cb798eaf9b9..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_k8s_pod_cap.ts +++ /dev/null @@ -1,47 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostK8sPodCap: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostK8sPodCap', - requires: ['kubernetes.node'], - map_field_to: 'kubernetes.node.name', - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - - series: [ - { - id: 'capacity', - metrics: [ - { - field: 'kubernetes.node.pod.allocatable.total', - id: 'max-pod-cap', - type: 'max', - }, - ], - split_mode: 'everything', - }, - { - id: 'used', - metrics: [ - { - field: 'kubernetes.pod.uid', - id: 'avg-pod', - type: 'cardinality', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_load.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_load.ts deleted file mode 100644 index bef170c743e6c..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_load.ts +++ /dev/null @@ -1,56 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostLoad: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostLoad', - requires: ['system.cpu'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'load_1m', - metrics: [ - { - field: 'system.load.1', - id: 'avg-load-1m', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - { - id: 'load_5m', - metrics: [ - { - field: 'system.load.5', - id: 'avg-load-5m', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - { - id: 'load_15m', - metrics: [ - { - field: 'system.load.15', - id: 'avg-load-15m', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_memory_usage.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_memory_usage.ts deleted file mode 100644 index bda81dea4bd28..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_memory_usage.ts +++ /dev/null @@ -1,78 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostMemoryUsage: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostMemoryUsage', - requires: ['system.memory'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'free', - metrics: [ - { - field: 'system.memory.free', - id: 'avg-memory-free', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - { - id: 'used', - metrics: [ - { - field: 'system.memory.actual.used.bytes', - id: 'avg-memory-used', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - { - id: 'cache', - metrics: [ - { - field: 'system.memory.actual.used.bytes', - id: 'avg-memory-actual-used', - type: 'avg', - }, - { - field: 'system.memory.used.bytes', - id: 'avg-memory-used', - type: 'avg', - }, - { - id: 'calc-used-actual', - script: 'params.used - params.actual', - type: 'calculation', - variables: [ - { - field: 'avg-memory-actual-used', - id: 'var-actual', - name: 'actual', - }, - { - field: 'avg-memory-used', - id: 'var-used', - name: 'used', - }, - ], - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_network_traffic.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_network_traffic.ts deleted file mode 100644 index b3dfc5e91ba0a..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_network_traffic.ts +++ /dev/null @@ -1,109 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostNetworkTraffic: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostNetworkTraffic', - requires: ['system.network'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'tx', - metrics: [ - { - field: 'host.network.egress.bytes', - id: 'avg-net-out', - type: 'avg', - }, - { - id: 'max-period', - type: 'max', - field: 'metricset.period', - }, - { - id: '3216b170-f192-11ec-a8e3-dd984b7213e2', - type: 'calculation', - variables: [ - { - id: '34e64c30-f192-11ec-a8e3-dd984b7213e2', - name: 'value', - field: 'avg-net-out', - }, - { - id: '3886cb80-f192-11ec-a8e3-dd984b7213e2', - name: 'period', - field: 'max-period', - }, - ], - script: 'params.value / (params.period / 1000)', - }, - ], - filter: { - language: 'kuery', - query: 'host.network.egress.bytes : * ', - }, - split_mode: 'everything', - }, - { - id: 'rx', - metrics: [ - { - field: 'host.network.ingress.bytes', - id: 'avg-net-in', - type: 'avg', - }, - { - id: 'calc-invert-rate', - script: 'params.rate * -1', - type: 'calculation', - variables: [ - { - field: 'avg-net-in', - id: 'var-rate', - name: 'rate', - }, - ], - }, - { - id: 'max-period', - type: 'max', - field: 'metricset.period', - }, - { - id: '3216b170-f192-11ec-a8e3-dd984b7213e2', - type: 'calculation', - variables: [ - { - id: '34e64c30-f192-11ec-a8e3-dd984b7213e2', - name: 'value', - field: 'calc-invert-rate', - }, - { - id: '3886cb80-f192-11ec-a8e3-dd984b7213e2', - name: 'period', - field: 'max-period', - }, - ], - script: 'params.value / (params.period / 1000)', - }, - ], - filter: { - language: 'kuery', - query: 'host.network.ingress.bytes : * ', - }, - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts deleted file mode 100644 index 69ebd0aa35947..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts +++ /dev/null @@ -1,130 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const hostSystemOverview: TSVBMetricModelCreator = ( - timeField, - indexPattern, - interval -): TSVBMetricModel => ({ - id: 'hostSystemOverview', - requires: ['system.cpu', 'system.memory', 'system.load', 'system.network'], - index_pattern: indexPattern, - interval, - time_field: timeField, - type: 'top_n', - series: [ - { - id: 'cpu', - split_mode: 'everything', - metrics: [ - { - field: 'system.cpu.total.norm.pct', - id: 'avg-cpu-total', - type: 'avg', - }, - ], - }, - { - id: 'load', - split_mode: 'everything', - metrics: [ - { - field: 'system.load.5', - id: 'avg-load-5m', - type: 'avg', - }, - ], - }, - { - id: 'memory', - split_mode: 'everything', - metrics: [ - { - field: 'system.memory.actual.used.pct', - id: 'avg-memory-actual-used', - type: 'avg', - }, - ], - }, - { - id: 'rx', - metrics: [ - { - field: 'host.network.ingress.bytes', - id: 'avg-net-in', - type: 'avg', - }, - { - id: 'max-period', - type: 'max', - field: 'metricset.period', - }, - { - id: '3216b170-f192-11ec-a8e3-dd984b7213e2', - type: 'calculation', - variables: [ - { - id: '34e64c30-f192-11ec-a8e3-dd984b7213e2', - name: 'value', - field: 'avg-net-in', - }, - { - id: '3886cb80-f192-11ec-a8e3-dd984b7213e2', - name: 'period', - field: 'max-period', - }, - ], - script: 'params.value / (params.period / 1000)', - }, - ], - filter: { - language: 'kuery', - query: 'host.network.ingress.bytes : * ', - }, - split_mode: 'everything', - }, - { - id: 'tx', - metrics: [ - { - field: 'host.network.egress.bytes', - id: 'avg-net-out', - type: 'avg', - }, - { - id: 'max-period', - type: 'max', - field: 'metricset.period', - }, - { - id: '3216b170-f192-11ec-a8e3-dd984b7213e2', - type: 'calculation', - variables: [ - { - id: '34e64c30-f192-11ec-a8e3-dd984b7213e2', - name: 'value', - field: 'avg-net-out', - }, - { - id: '3886cb80-f192-11ec-a8e3-dd984b7213e2', - name: 'period', - field: 'max-period', - }, - ], - script: 'params.value / (params.period / 1000)', - }, - ], - filter: { - language: 'kuery', - query: 'host.network.egress.bytes : * ', - }, - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/index.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/index.ts index 775dff525b7f5..2dac80de51161 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/index.ts @@ -10,12 +10,6 @@ import { nginxActiveConnections } from './tsvb/nginx_active_connections'; import { nginxHits } from './tsvb/nginx_hits'; import { nginxRequestsPerConnection } from './tsvb/nginx_requests_per_connection'; -import { awsCpuUtilization } from './tsvb/aws_cpu_utilization'; -import { awsDiskioBytes } from './tsvb/aws_diskio_bytes'; -import { awsDiskioOps } from './tsvb/aws_diskio_ops'; -import { awsNetworkBytes } from './tsvb/aws_network_bytes'; -import { awsNetworkPackets } from './tsvb/aws_network_packets'; -import { awsOverview } from './tsvb/aws_overview'; import { InventoryMetrics } from '../../types'; import { count } from './snapshot/count'; @@ -25,12 +19,6 @@ export const metrics: InventoryMetrics = { nginxHits, nginxRequestRate, nginxRequestsPerConnection, - awsCpuUtilization, - awsDiskioBytes, - awsDiskioOps, - awsNetworkBytes, - awsNetworkPackets, - awsOverview, }, snapshot: { count, diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/required_metrics.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/required_metrics.ts index 5b29fc93e0d0c..172b48a1ba68d 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/required_metrics.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/required_metrics.ts @@ -13,12 +13,3 @@ export const nginx: InventoryMetric[] = [ 'nginxActiveConnections', 'nginxRequestsPerConnection', ]; - -export const aws: InventoryMetric[] = [ - 'awsOverview', - 'awsCpuUtilization', - 'awsNetworkBytes', - 'awsNetworkPackets', - 'awsDiskioOps', - 'awsDiskioBytes', -]; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_cpu_utilization.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_cpu_utilization.ts deleted file mode 100644 index 51b9a4cfc7b3f..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_cpu_utilization.ts +++ /dev/null @@ -1,35 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const awsCpuUtilization: TSVBMetricModelCreator = ( - timeField, - indexPattern -): TSVBMetricModel => ({ - id: 'awsCpuUtilization', - requires: ['aws.ec2'], - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - index_pattern: indexPattern, - interval: '>=5m', - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'cpu-util', - metrics: [ - { - field: 'aws.ec2.cpu.total.pct', - id: 'avg-cpu-util', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_bytes.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_bytes.ts deleted file mode 100644 index 5224545c006b7..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_bytes.ts +++ /dev/null @@ -1,84 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const awsDiskioBytes: TSVBMetricModelCreator = ( - timeField, - indexPattern -): TSVBMetricModel => ({ - id: 'awsDiskioBytes', - requires: ['aws.ec2'], - index_pattern: indexPattern, - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - interval: '>=5m', - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'writes', - metrics: [ - { - field: 'aws.ec2.diskio.write.bytes', - id: 'sum-diskio-out', - type: 'sum', - }, - { - id: 'csum-sum-diskio-out', - field: 'sum-diskio-out', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-diskio-out', - unit: '1s', - type: 'derivative', - field: 'csum-sum-diskio-out', - }, - { - id: 'posonly-deriv-csum-sum-diskio-out', - field: 'deriv-csum-sum-diskio-out', - type: 'positive_only', - }, - ], - split_mode: 'everything', - }, - { - id: 'reads', - metrics: [ - { - field: 'aws.ec2.diskio.read.bytes', - id: 'sum-diskio-in', - type: 'sum', - }, - { - id: 'csum-sum-diskio-in', - field: 'sum-diskio-in', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-diskio-in', - unit: '1s', - type: 'derivative', - field: 'csum-sum-diskio-in', - }, - { - id: 'posonly-deriv-csum-sum-diskio-in', - field: 'deriv-csum-sum-diskio-in', - type: 'positive_only', - }, - { - id: 'inverted-posonly-deriv-csum-sum-diskio-in', - type: 'calculation', - variables: [{ id: 'var-rate', name: 'rate', field: 'posonly-deriv-csum-sum-diskio-in' }], - script: 'params.rate * -1', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_ops.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_ops.ts deleted file mode 100644 index c362a6d88c27a..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_diskio_ops.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const awsDiskioOps: TSVBMetricModelCreator = (timeField, indexPattern): TSVBMetricModel => ({ - id: 'awsDiskioOps', - requires: ['aws.ec2'], - index_pattern: indexPattern, - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - interval: '>=5m', - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'writes', - metrics: [ - { - field: 'aws.ec2.diskio.write.count', - id: 'sum-diskio-writes', - type: 'sum', - }, - { - id: 'csum-sum-diskio-writes', - field: 'sum-diskio-writes', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-diskio-writes', - unit: '1s', - type: 'derivative', - field: 'csum-sum-diskio-writes', - }, - { - id: 'posonly-deriv-csum-sum-diskio-writes', - field: 'deriv-csum-sum-diskio-writes', - type: 'positive_only', - }, - ], - split_mode: 'everything', - }, - { - id: 'reads', - metrics: [ - { - field: 'aws.ec2.diskio.read.count', - id: 'sum-diskio-reads', - type: 'sum', - }, - { - id: 'csum-sum-diskio-reads', - field: 'sum-diskio-reads', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-diskio-reads', - unit: '1s', - type: 'derivative', - field: 'csum-sum-diskio-reads', - }, - { - id: 'posonly-deriv-csum-sum-diskio-reads', - field: 'deriv-csum-sum-diskio-reads', - type: 'positive_only', - }, - { - id: 'inverted-posonly-deriv-csum-sum-diskio-reads', - type: 'calculation', - variables: [ - { id: 'var-rate', name: 'rate', field: 'posonly-deriv-csum-sum-diskio-reads' }, - ], - script: 'params.rate * -1', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_bytes.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_bytes.ts deleted file mode 100644 index b142feb95450c..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_bytes.ts +++ /dev/null @@ -1,86 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -// see discussion in: https://github.com/elastic/kibana/issues/42687 - -export const awsNetworkBytes: TSVBMetricModelCreator = ( - timeField, - indexPattern -): TSVBMetricModel => ({ - id: 'awsNetworkBytes', - requires: ['aws.ec2'], - index_pattern: indexPattern, - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - interval: '>=5m', - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'tx', - metrics: [ - { - field: 'aws.ec2.network.out.bytes', - id: 'sum-net-out', - type: 'sum', - }, - { - id: 'csum-sum-net-out', - field: 'sum-net-out', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-net-out', - unit: '1s', - type: 'derivative', - field: 'csum-sum-net-out', - }, - { - id: 'posonly-deriv-csum-sum-net-out', - field: 'deriv-csum-sum-net-out', - type: 'positive_only', - }, - ], - split_mode: 'everything', - }, - { - id: 'rx', - metrics: [ - { - field: 'aws.ec2.network.in.bytes', - id: 'sum-net-in', - type: 'sum', - }, - { - id: 'csum-sum-net-in', - field: 'sum-net-in', - type: 'cumulative_sum', - }, - { - id: 'deriv-csum-sum-net-in', - unit: '1s', - type: 'derivative', - field: 'csum-sum-net-in', - }, - { - id: 'posonly-deriv-csum-sum-net-in', - field: 'deriv-csum-sum-net-in', - type: 'positive_only', - }, - { - id: 'inverted-posonly-deriv-csum-sum-net-in', - type: 'calculation', - variables: [{ id: 'var-rate', name: 'rate', field: 'posonly-deriv-csum-sum-net-in' }], - script: 'params.rate * -1', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_packets.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_packets.ts deleted file mode 100644 index 9d39582b66864..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_network_packets.ts +++ /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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const awsNetworkPackets: TSVBMetricModelCreator = ( - timeField, - indexPattern -): TSVBMetricModel => ({ - id: 'awsNetworkPackets', - requires: ['aws.ec2'], - index_pattern: indexPattern, - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - interval: '>=5m', - time_field: timeField, - type: 'timeseries', - series: [ - { - id: 'packets-out', - metrics: [ - { - field: 'aws.ec2.network.out.packets', - id: 'avg-net-out', - type: 'avg', - }, - ], - split_mode: 'everything', - }, - { - id: 'packets-in', - metrics: [ - { - field: 'aws.ec2.network.in.packets', - id: 'avg-net-in', - type: 'avg', - }, - { - id: 'inverted-avg-net-in', - type: 'calculation', - variables: [{ id: 'var-avg', name: 'avg', field: 'avg-net-in' }], - script: 'params.avg * -1', - }, - ], - split_mode: 'everything', - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts deleted file mode 100644 index 3fe12d62d3352..0000000000000 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/tsvb/aws_overview.ts +++ /dev/null @@ -1,65 +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 { TSVBMetricModelCreator, TSVBMetricModel } from '../../../types'; - -export const awsOverview: TSVBMetricModelCreator = (timeField, indexPattern): TSVBMetricModel => ({ - id: 'awsOverview', - requires: ['aws.ec2'], - index_pattern: indexPattern, - map_field_to: 'cloud.instance.id', - id_type: 'cloud', - interval: '>=5m', - time_field: timeField, - type: 'top_n', - series: [ - { - id: 'cpu-util', - split_mode: 'everything', - metrics: [ - { - field: 'aws.ec2.cpu.total.pct', - id: 'cpu-total-pct', - type: 'max', - }, - ], - }, - { - id: 'status-check-failed', - split_mode: 'everything', - metrics: [ - { - field: 'aws.ec2.status.check_failed', - id: 'status-check-failed', - type: 'max', - }, - ], - }, - { - id: 'packets-out', - split_mode: 'everything', - metrics: [ - { - field: 'aws.ec2.network.out.packets', - id: 'network-out-packets', - type: 'avg', - }, - ], - }, - { - id: 'packets-in', - split_mode: 'everything', - metrics: [ - { - field: 'aws.ec2.network.in.packets', - id: 'network-in-packets', - type: 'avg', - }, - ], - }, - ], -}); diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 5bc36429e2ba2..fd52c1c230c17 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -37,21 +37,6 @@ export type InventoryFormatterType = rt.TypeOf; export type InventoryItemType = rt.TypeOf; export const InventoryMetricRT = rt.keyof({ - hostSystemOverview: null, - hostCpuUsage: null, - hostFilesystem: null, - hostK8sOverview: null, - hostK8sCpuCap: null, - hostK8sDiskCap: null, - hostK8sMemoryCap: null, - hostK8sPodCap: null, - hostLoad: null, - hostMemoryUsage: null, - hostNetworkTraffic: null, - hostDockerOverview: null, - hostDockerInfo: null, - hostDockerTop5ByCpu: null, - hostDockerTop5ByMemory: null, podOverview: null, podCpuUsage: null, podMemoryUsage: null, @@ -71,12 +56,6 @@ export const InventoryMetricRT = rt.keyof({ nginxRequestRate: null, nginxActiveConnections: null, nginxRequestsPerConnection: null, - awsOverview: null, - awsCpuUtilization: null, - awsNetworkBytes: null, - awsNetworkPackets: null, - awsDiskioBytes: null, - awsDiskioOps: null, awsEC2CpuUtilization: null, awsEC2NetworkTraffic: null, awsEC2DiskIOBytes: null, @@ -377,7 +356,7 @@ export const SnapshotMetricTypeRT = rt.keyof(SnapshotMetricTypeKeys); export type SnapshotMetricType = rt.TypeOf; export interface InventoryMetrics { - tsvb: { [name: string]: TSVBMetricModelCreator }; + tsvb?: { [name: string]: TSVBMetricModelCreator }; snapshot: { [name: string]: MetricsUIAggregation | undefined }; defaultSnapshot: SnapshotMetricType; /** This is used by the inventory view to calculate the appropriate amount of time for the metrics detail page. Some metris like awsS3 require multiple days where others like host only need an hour.*/ @@ -403,7 +382,7 @@ export interface InventoryModel { uptime: boolean; }; metrics: InventoryMetrics; - requiredMetrics: InventoryMetric[]; + requiredMetrics?: InventoryMetric[]; tooltipMetrics: SnapshotMetricType[]; nodeFilter?: object[]; } diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata.ts index 2750394da702f..c06344270c2f0 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata.ts @@ -15,16 +15,23 @@ import { throwErrors, createPlainError } from '../../../../common/runtime_types' import { getFilteredMetrics } from '../../../pages/metrics/metric_detail/lib/get_filtered_metrics'; import type { InventoryItemType, InventoryMetric } from '../../../../common/inventory_models/types'; -export function useMetadata( - nodeId: string, - nodeType: InventoryItemType, - requiredMetrics: InventoryMetric[], - sourceId: string, +interface UseMetadataProps { + assetId: string; + assetType: InventoryItemType; + requiredMetrics?: InventoryMetric[]; + sourceId: string; timeRange: { from: number; to: number; - } -) { + }; +} +export function useMetadata({ + assetId, + assetType, + sourceId, + timeRange, + requiredMetrics = [], +}: UseMetadataProps) { const decodeResponse = (response: any) => { return pipe(InfraMetadataRT.decode(response), fold(throwErrors(createPlainError), identity)); }; @@ -32,8 +39,8 @@ export function useMetadata( '/api/infra/metadata', 'POST', JSON.stringify({ - nodeId, - nodeType, + nodeId: assetId, + nodeType: assetType, sourceId, timeRange, }), @@ -49,17 +56,13 @@ export function useMetadata( return { name: (response && response.name) || '', filteredRequiredMetrics: - (response && getFilteredMetrics(requiredMetrics, response.features)) || [], + response && requiredMetrics.length > 0 + ? getFilteredMetrics(requiredMetrics, response.features) + : [], error: (error && error.message) || null, loading, metadata: response, - cloudId: - (response && - response.info && - response.info.cloud && - response.info.cloud.instance && - response.info.cloud.instance.id) || - '', + cloudId: response?.info?.cloud?.instance?.id || '', reload: makeRequest, }; } diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts index 4d0f0c66f67c8..0df9ae9a152dd 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts @@ -7,7 +7,6 @@ import { useEffect, useCallback } from 'react'; import createContainer from 'constate'; -import { findInventoryModel } from '../../../../common/inventory_models'; import { useSourceContext } from '../../../containers/metrics_source'; import { useMetadata } from './use_metadata'; import { AssetDetailsProps } from '../types'; @@ -19,16 +18,14 @@ export type UseMetadataProviderProps = Pick { reload(); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout.tsx index a2b48e9b7f2f3..c89fa6f7fe601 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout.tsx @@ -13,7 +13,6 @@ import { AwsRDSLayout } from './layouts/aws_rds_layout'; import { AwsS3Layout } from './layouts/aws_s3_layout'; import { AwsSQSLayout } from './layouts/aws_sqs_layout'; import { ContainerLayout } from './layouts/container_layout'; -import { HostLayout } from './layouts/host_layout'; import { PodLayout } from './layouts/pod_layout'; export const Layout = ({ @@ -31,9 +30,9 @@ export const Layout = ({ return ; case 'container': return ; - case 'host': - return ; case 'pod': return ; + default: + throw new Error(`${inventoryItemType} is not supported.`); } }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/aws_layout_sections.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/aws_layout_sections.tsx deleted file mode 100644 index 75c06c30b968a..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/aws_layout_sections.tsx +++ /dev/null @@ -1,242 +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 { i18n } from '@kbn/i18n'; -import { withTheme } from '@kbn/kibana-react-plugin/common'; -import React from 'react'; -import type { LayoutPropsWithTheme } from '../../types'; -import { ChartSectionVis } from '../chart_section_vis'; -import { GaugesSectionVis } from '../gauges_section_vis'; -import { Section } from '../section'; -import { SubSection } from '../sub_section'; - -export const AwsLayoutSection = withTheme( - ({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( - -
- - - - - - - - - - - - - - - - - - -
-
- ) -); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/host_layout.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/host_layout.tsx deleted file mode 100644 index 41e0204bc7b29..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layouts/host_layout.tsx +++ /dev/null @@ -1,376 +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 { EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { withTheme } from '@kbn/kibana-react-plugin/common'; -import React from 'react'; -import type { LayoutPropsWithTheme } from '../../types'; -import { ChartSectionVis } from '../chart_section_vis'; -import { GaugesSectionVis } from '../gauges_section_vis'; -import { MetadataDetails } from '../metadata_details'; -import { Section } from '../section'; -import { SubSection } from '../sub_section'; -import { AwsLayoutSection } from './aws_layout_sections'; -import { NginxLayoutSection } from './nginx_layout_sections'; - -export const HostLayout = withTheme( - ({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( - - - -
- - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - -
- - -
-
- ) -); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx index 1f049814a23d3..15425ef618049 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx @@ -42,7 +42,13 @@ export const MetricDetailPage = () => { loading: metadataLoading, cloudId, metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); + } = useMetadata({ + assetId: nodeId, + assetType: nodeType, + requiredMetrics: inventoryModel.requiredMetrics, + sourceId, + timeRange: parsedTimeRange, + }); const [sideNav, setSideNav] = useState([]); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 4b618dbd82e85..6eec0c9f28629 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -131,7 +131,6 @@ const setEvaluationResults = (response: Array>) => { jest.requireMock('./lib/evaluate_rule').evaluateRule.mockImplementation(() => response); }; -// FAILING: https://github.com/elastic/kibana/issues/155534 describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { afterAll(() => clearInstances()); diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index cd92c902a110e..56eccbbe160e9 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -34,30 +34,46 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const soClient = (await requestContext.core).savedObjects.client; - const source = await libs.sources.getSourceConfiguration(soClient, sourceId); - - UsageCollector.countNode(nodeType); - - const options: InfraMetricsRequestOptions = { - nodeIds: { - nodeId, - cloudId, - }, - nodeType, - sourceConfiguration: source.configuration, - metrics, - timerange, - }; - return response.ok({ - body: NodeDetailsMetricDataResponseRT.encode({ - metrics: await libs.metrics.getMetrics(requestContext, options, request), - }), - }); + try { + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const soClient = (await requestContext.core).savedObjects.client; + const source = await libs.sources.getSourceConfiguration(soClient, sourceId); + + UsageCollector.countNode(nodeType); + + const options: InfraMetricsRequestOptions = { + nodeIds: { + nodeId, + cloudId, + }, + nodeType, + sourceConfiguration: source.configuration, + metrics, + timerange, + }; + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); + } catch (err) { + if (Boom.isBoom(err)) { + return response.customError({ + statusCode: err.output.statusCode, + body: { message: err.output.payload.message }, + }); + } + + return response.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message ?? 'An unexpected error occurred', + }, + }); + } } ); }; diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index ff269070a18c2..34baa25120e09 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -8,7 +8,7 @@ import type { Filter, FilterMeta } from '@kbn/es-query'; import type { Position } from '@elastic/charts'; import type { $Values } from '@kbn/utility-types'; -import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { CustomPaletteParams, PaletteOutput, ColorMapping } from '@kbn/coloring'; import type { ColorMode } from '@kbn/charts-plugin/common'; import type { LegendSize } from '@kbn/visualizations-plugin/common'; import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; @@ -71,6 +71,7 @@ export interface SharedPieLayerState { legendMaxLines?: number; legendSize?: LegendSize; truncateLegend?: boolean; + colorMapping?: ColorMapping.Config; } export type PieLayerState = SharedPieLayerState & { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 22c5a21ad3377..d68476598ad99 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -154,7 +154,7 @@ export function App({ useExecutionContext(executionContext, { type: 'application', - id: savedObjectId || 'new', + id: savedObjectId || 'new', // TODO: this doesn't consider when lens is saved by value page: 'editor', }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 0e677804d5f16..d9cfa8c84c62f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -18,6 +18,7 @@ import { type EventAnnotationGroupConfig, EVENT_ANNOTATION_GROUP_TYPE, } from '@kbn/event-annotation-common'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { Datasource, DatasourceMap, @@ -289,7 +290,8 @@ export function initializeVisualization({ visualizationMap[visualizationState.activeId]?.initialize( () => '', visualizationState.state, - undefined, + // initialize a new visualization always with the new color mapping + { type: 'colorMapping', value: { ...DEFAULT_COLOR_MAPPING_CONFIG } }, annotationGroups, references ) ?? visualizationState.state diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index e86f602465584..235e3b34538b8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -457,7 +457,13 @@ describe('suggestion helpers', () => { it('should pass passed in main palette if specified', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; + const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = { + type: 'legacyPalette', + value: { + type: 'palette', + name: 'mock', + }, + }; datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(0), generateSuggestion(1), @@ -490,7 +496,13 @@ describe('suggestion helpers', () => { it('should query active visualization for main palette if not specified', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; + const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = { + type: 'legacyPalette', + value: { + type: 'palette', + name: 'mock', + }, + }; mockVisualization1.getMainPalette = jest.fn(() => mainPalette); datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(0), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 6679e8b042480..c1032d144ac33 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -6,12 +6,11 @@ */ import type { Datatable } from '@kbn/expressions-plugin/common'; -import type { PaletteOutput } from '@kbn/coloring'; import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { DragDropIdentifier } from '@kbn/dom-drag-drop'; import { showMemoizedErrorNotification } from '../../lens_ui_errors'; -import type { +import { Visualization, Datasource, TableSuggestion, @@ -21,6 +20,7 @@ import type { VisualizeEditorContext, Suggestion, DatasourceLayers, + SuggestionRequest, } from '../../types'; import type { LayerType } from '../../../common/types'; import { @@ -64,7 +64,7 @@ export function getSuggestions({ visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; activeData?: Record; dataViews: DataViewsState; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }): Suggestion[] { const datasources = Object.entries(datasourceMap).filter( @@ -237,7 +237,7 @@ function getVisualizationSuggestions( datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, currentVisualizationState: unknown, subVisualizationId?: string, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], isFromContext?: boolean, activeData?: Record, allowMixed?: boolean diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 5b14bb43dfb16..3e613d5a23e89 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -519,7 +519,10 @@ describe('chart_switch', () => { it('should query main palette from active chart and pass into suggestions', async () => { const visualizationMap = mockVisualizationMap(); const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' }; - visualizationMap.visA.getMainPalette = jest.fn(() => mockPalette); + visualizationMap.visA.getMainPalette = jest.fn(() => ({ + type: 'legacyPalette', + value: mockPalette, + })); visualizationMap.visB.getSuggestions.mockReturnValueOnce([]); const frame = mockFrame(['a', 'b', 'c']); const currentVisState = {}; @@ -550,7 +553,7 @@ describe('chart_switch', () => { expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ keptLayerIds: ['a'], - mainPalette: mockPalette, + mainPalette: { type: 'legacyPalette', value: mockPalette }, }) ); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx index b910354f1f68d..523c98c9d6903 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx @@ -27,8 +27,10 @@ export function PalettePanelContainer({ handleClose, siblingRef, children, + title, }: { isOpen: boolean; + title: string; handleClose: () => void; siblingRef: MutableRefObject; children?: React.ReactElement | React.ReactElement[]; @@ -76,16 +78,12 @@ export function PalettePanelContainer({ -

- - {i18n.translate('xpack.lens.table.palettePanelTitle', { - defaultMessage: 'Color', - })} - -

+ {title} +
diff --git a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx index efd1caba7e4da..51977e551128e 100644 --- a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx @@ -36,28 +36,24 @@ export function PalettePicker({ }); return ( - <> - { - setPalette({ - type: 'palette', - name: newPalette, - }); - }} - valueOfSelected={activePalette?.name || 'default'} - selectionDisplay={'palette'} - /> - + { + setPalette({ + type: 'palette', + name: newPalette, + }); + }} + valueOfSelected={activePalette?.name || 'default'} + selectionDisplay={'palette'} + /> ); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d549fbb71bdcf..0c09d84df9adc 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -7,7 +7,7 @@ import type { Ast } from '@kbn/interpreter'; import type { IconType } from '@elastic/eui/src/components/icon/icon'; import type { CoreStart, SavedObjectReference, ResolvedSimpleSavedObject } from '@kbn/core/public'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { ColorMapping, PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject, ReactElement } from 'react'; import type { Filter, TimeRange } from '@kbn/es-query'; @@ -863,7 +863,12 @@ export interface SuggestionRequest { * State is only passed if the visualization is active. */ state?: T; - mainPalette?: PaletteOutput; + /** + * Passing the legacy palette or the new color mapping if available + */ + mainPalette?: + | { type: 'legacyPalette'; value: PaletteOutput } + | { type: 'colorMapping'; value: ColorMapping.Config }; isFromContext?: boolean; /** * The visualization needs to know which table is being suggested @@ -1026,11 +1031,15 @@ export interface Visualization string, nonPersistedState?: T, mainPalette?: PaletteOutput): T; + ( + addNewLayer: () => string, + nonPersistedState?: T, + mainPalette?: SuggestionRequest['mainPalette'] + ): T; ( addNewLayer: () => string, persistedState: P, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], annotationGroups?: AnnotationGroups, references?: SavedObjectReference[] ): T; @@ -1042,7 +1051,7 @@ export interface Visualization string[]; - getMainPalette?: (state: T) => undefined | PaletteOutput; + getMainPalette?: (state: T) => undefined | SuggestionRequest['mainPalette']; /** * Supported triggers of this visualization type when embedded somewhere diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx index 09f1bc93d6779..60646bdb8d054 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx @@ -253,6 +253,9 @@ export function TableDimensionEditor( siblingRef={props.panelRef} isOpen={isPaletteOpen} handleClose={() => setIsPaletteOpen(!isPaletteOpen)} + title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', { + defaultMessage: 'Color', + })} > setIsPaletteOpen(!isPaletteOpen)} + title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', { + defaultMessage: 'Color', + })} > {activePalette && ( & { paletteService: PaletteRegistry; + isDarkMode: boolean; }; export function DimensionEditor(props: DimensionEditorProps) { @@ -30,10 +52,14 @@ export function DimensionEditor(props: DimensionEditorProps) { value: props.state, onChange: props.setState, }); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); const currentLayer = localState.layers.find((layer) => layer.layerId === props.layerId); - const setConfig = React.useCallback( + const canUseColorMapping = currentLayer && currentLayer.colorMapping ? true : false; + const [useNewColorMapping, setUseNewColorMapping] = useState(canUseColorMapping); + + const setConfig = useCallback( ({ color }) => { if (!currentLayer) { return; @@ -61,6 +87,23 @@ export function DimensionEditor(props: DimensionEditorProps) { [currentLayer, localState, props.accessor, setLocalState] ); + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState({ + ...localState, + layers: localState.layers.map((layer) => + layer.layerId === currentLayer?.layerId + ? { + ...layer, + colorMapping, + } + : layer + ), + }); + }, + [localState, currentLayer, setLocalState] + ); + if (!currentLayer) { return null; } @@ -84,17 +127,125 @@ export function DimensionEditor(props: DimensionEditorProps) { }) : undefined; + const colors = getColorsFromMapping(props.isDarkMode, currentLayer.colorMapping); + const table = props.frame.activeData?.[currentLayer.layerId]; + const splitCategories = getColorCategories(table?.rows ?? [], props.accessor); + return ( <> {props.accessor === firstNonCollapsedColumnId && ( - { - setLocalState({ ...props.state, palette: newPalette }); - }} - /> + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + data-test-subj="lns_colorMappingOrLegacyPalette_switch" + compressed + checked={useNewColorMapping} + onChange={({ target: { checked } }) => { + trackUiCounterEvents( + `color_mapping_switch_${checked ? 'enabled' : 'disabled'}` + ); + setColorMapping( + checked ? { ...DEFAULT_COLOR_MAPPING_CONFIG } : undefined + ); + setUseNewColorMapping(checked); + }} + /> + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setLocalState({ ...props.state, palette: newPalette }); + }} + /> + )} + + +
+
+
+
+
)} + {/* TODO: understand how this works */} {showColorPicker && ( { }); it('should keep passed in palette', () => { - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; const results = suggestions({ table: { layerId: 'first', @@ -617,10 +616,13 @@ describe('suggestions', () => { }, state: undefined, keptLayerIds: ['first'], - mainPalette, + mainPalette: { + type: 'legacyPalette', + value: { type: 'palette', name: 'mock' }, + }, }); - expect(results[0].state.palette).toEqual(mainPalette); + expect(results[0].state.palette).toEqual({ type: 'palette', name: 'mock' }); }); it('should keep the layer settings and palette when switching from treemap', () => { @@ -681,6 +683,7 @@ describe('suggestions', () => { legendMaxLines: 1, truncateLegend: true, nestedLegend: true, + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }, @@ -1060,6 +1063,7 @@ describe('suggestions', () => { Object { "allowMultipleMetrics": false, "categoryDisplay": "default", + "colorMapping": undefined, "layerId": "first", "layerType": "data", "legendDisplay": "show", @@ -1169,6 +1173,7 @@ describe('suggestions', () => { "layers": Array [ Object { "categoryDisplay": "default", + "colorMapping": undefined, "layerId": "first", "layerType": "data", "legendDisplay": "show", diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index f3dea7c54b989..e78c203670aec 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -7,6 +7,7 @@ import { partition } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, TableSuggestionColumn, @@ -131,7 +132,7 @@ export function suggestions({ score: state && !hasCustomSuggestionsExists(state.shape) ? 0.6 : 0.4, state: { shape: newShape, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -140,6 +141,11 @@ export function suggestions({ primaryGroups: groups.map((col) => col.columnId), metrics: metricColumnIds, layerType: layerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -150,6 +156,11 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }, ], }, @@ -196,7 +207,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.TREEMAP ? 0.7 : 0.5, state: { shape: PieChartTypes.TREEMAP, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -209,6 +220,10 @@ export function suggestions({ ? CategoryDisplay.DEFAULT : state.layers[0].categoryDisplay, layerType: layerTypes.DATA, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -219,6 +234,7 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, @@ -243,7 +259,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5, state: { shape: PieChartTypes.MOSAIC, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -255,6 +271,10 @@ export function suggestions({ categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, allowMultipleMetrics: false, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -267,6 +287,7 @@ export function suggestions({ nestedLegend: false, layerType: layerTypes.DATA, allowMultipleMetrics: false, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, @@ -290,7 +311,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.4, state: { shape: PieChartTypes.WAFFLE, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -301,6 +322,10 @@ export function suggestions({ secondaryGroups: [], categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -311,6 +336,7 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, diff --git a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts index c592f7d369eb3..29e5fd399d148 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts @@ -7,7 +7,7 @@ import type { Ast } from '@kbn/interpreter'; import { Position } from '@elastic/charts'; -import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring'; +import { PaletteOutput, PaletteRegistry } from '@kbn/coloring'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import type { @@ -175,7 +175,6 @@ const generateCommonArguments = ( const datasource = datasourceLayers[layer.layerId]; const columnToLabelMap = getColumnToLabelMap(layer.metrics, datasource); const sortedMetricAccessors = getSortedAccessorsForGroup(datasource, layer, 'metrics'); - return { labels: generateCommonLabelsAstArgs(state, attributes, layer, columnToLabelMap), buckets: operations @@ -200,6 +199,7 @@ const generateCommonArguments = ( layer.truncateLegend ?? getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, palette: generatePaletteAstArguments(paletteService, state.palette), addTooltip: true, + colorMapping: layer.colorMapping ? JSON.stringify(layer.colorMapping) : undefined, }; }; diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 6b4767b9177e5..429089f743c33 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -8,13 +8,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { + ColorMapping, + DEFAULT_COLOR_MAPPING_CONFIG, + PaletteRegistry, + getColorsFromMapping, +} from '@kbn/coloring'; import { ThemeServiceStart } from '@kbn/core/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { EuiSpacer } from '@elastic/eui'; import { PartitionVisConfiguration } from '@kbn/visualizations-plugin/common/convert_to_lens'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { AccessorConfig } from '@kbn/visualization-ui-components'; +import useObservable from 'react-use/lib/useObservable'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { Visualization, @@ -51,7 +57,7 @@ const metricLabel = i18n.translate('xpack.lens.pie.groupMetricLabelSingular', { defaultMessage: 'Metric', }); -function newLayerState(layerId: string): PieLayerState { +function newLayerState(layerId: string, colorMapping: ColorMapping.Config): PieLayerState { return { layerId, primaryGroups: [], @@ -62,6 +68,7 @@ function newLayerState(layerId: string): PieLayerState { legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: LayerTypes.DATA, + colorMapping, }; } @@ -137,7 +144,9 @@ export const getPieVisualization = ({ clearLayer(state) { return { shape: state.shape, - layers: state.layers.map((l) => newLayerState(l.layerId)), + layers: state.layers.map((l) => + newLayerState(l.layerId, { ...DEFAULT_COLOR_MAPPING_CONFIG }) + ), }; }, @@ -156,13 +165,29 @@ export const getPieVisualization = ({ return ( state || { shape: PieChartTypes.DONUT, - layers: [newLayerState(addNewLayer())], - palette: mainPalette, + layers: [ + newLayerState( + addNewLayer(), + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : { ...DEFAULT_COLOR_MAPPING_CONFIG } + ), + ], + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : undefined, } ); }, - getMainPalette: (state) => (state ? state.palette : undefined), + getMainPalette: (state) => { + if (!state) { + return undefined; + } + return state.layers.length > 0 && state.layers[0].colorMapping + ? { type: 'colorMapping', value: state.layers[0].colorMapping } + : state.palette + ? { type: 'legacyPalette', value: state.palette } + : undefined; + }, getSuggestions: suggestions, @@ -174,6 +199,19 @@ export const getPieVisualization = ({ const datasource = frame.datasourceLayers[layer.layerId]; + let colors: string[] = []; + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = state.layers[0]?.colorMapping + ? getColorsFromMapping(theme.darkMode, state.layers[0].colorMapping) + : paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params); + }, + }) + .unsubscribe(); + const getPrimaryGroupConfig = (): VisualizationDimensionGroupConfig => { const originalOrder = getSortedAccessorsForGroup(datasource, layer, 'primaryGroups'); // When we add a column it could be empty, and therefore have no order @@ -187,9 +225,7 @@ export const getPieVisualization = ({ accessors.forEach((accessorConfig) => { if (firstNonCollapsedColumnId === accessorConfig.columnId) { accessorConfig.triggerIconType = 'colorBy'; - accessorConfig.palette = paletteService - .get(state.palette?.name || 'default') - .getCategoricalColors(10, state.palette?.params); + accessorConfig.palette = colors; } }); @@ -459,7 +495,8 @@ export const getPieVisualization = ({ }; }, DimensionEditorComponent(props) { - return ; + const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + return ; }, DimensionEditorDataExtraComponent(props) { return ; diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts index 129d8f4eb545f..e58f8fe673127 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts @@ -19,7 +19,7 @@ export class TagcloudVisualization { editorFrame.registerVisualization(async () => { const { getTagcloudVisualization } = await import('../../async_services'); const palettes = await charts.palettes.getPalettes(); - return getTagcloudVisualization({ paletteService: palettes, theme: core.theme }); + return getTagcloudVisualization({ paletteService: palettes, kibanaTheme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts index c85f7b0b28fe2..4a528c99d41ad 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts @@ -7,6 +7,7 @@ import { partition } from 'lodash'; import { IconChartTagcloud } from '@kbn/chart-icons'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, VisualizationSuggestion } from '../../types'; import type { TagcloudState } from './types'; import { DEFAULT_STATE, TAGCLOUD_LABEL } from './constants'; @@ -48,6 +49,11 @@ export function getSuggestions({ tagAccessor: bucket.columnId, valueAccessor: metrics[0].columnId, ...DEFAULT_STATE, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }, }; }); diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx index d1bd1ec337bc0..c9449f2dad178 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -16,9 +16,10 @@ import { buildExpressionFunction, ExpressionFunctionTheme, } from '@kbn/expressions-plugin/common'; -import { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry, DEFAULT_COLOR_MAPPING_CONFIG, getColorsFromMapping } from '@kbn/coloring'; import { IconChartTagcloud } from '@kbn/chart-icons'; import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common'; +import useObservable from 'react-use/lib/useObservable'; import type { OperationMetadata, Visualization } from '../..'; import type { TagcloudState } from './types'; import { getSuggestions } from './suggestions'; @@ -31,10 +32,10 @@ const METRIC_GROUP_ID = 'metric'; export const getTagcloudVisualization = ({ paletteService, - theme, + kibanaTheme, }: { paletteService: PaletteRegistry; - theme: ThemeServiceStart; + kibanaTheme: ThemeServiceStart; }): Visualization => ({ id: 'lnsTagcloud', @@ -89,6 +90,15 @@ export const getTagcloudVisualization = ({ }, }; }, + getMainPalette: (state) => { + if (!state) return; + + return state.colorMapping + ? { type: 'colorMapping', value: state.colorMapping } + : state.palette + ? { type: 'legacyPalette', value: state.palette } + : undefined; + }, triggers: [VIS_EVENT_TO_TRIGGER.filter], @@ -98,11 +108,27 @@ export const getTagcloudVisualization = ({ layerId: addNewLayer(), layerType: LayerTypes.DATA, ...DEFAULT_STATE, + colorMapping: { ...DEFAULT_COLOR_MAPPING_CONFIG }, } ); }, getConfiguration({ state }) { + const canUseColorMapping = state.colorMapping ? true : false; + let colors: string[] = []; + if (canUseColorMapping) { + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = getColorsFromMapping(theme.darkMode, state.colorMapping); + }, + }) + .unsubscribe(); + } else { + colors = paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params); + } return { groups: [ { @@ -116,9 +142,7 @@ export const getTagcloudVisualization = ({ { columnId: state.tagAccessor, triggerIconType: 'colorBy', - palette: paletteService - .get(state.palette?.name || 'default') - .getCategoricalColors(10, state.palette?.params), + palette: colors, }, ] : [], @@ -197,6 +221,7 @@ export const getTagcloudVisualization = ({ ), ]).toAst(), showLabel: state.showLabel, + colorMapping: state.colorMapping ? JSON.stringify(state.colorMapping) : undefined, }).toAst(), ], }; @@ -235,6 +260,7 @@ export const getTagcloudVisualization = ({ ), ]).toAst(), showLabel: false, + colorMapping: state.colorMapping ? JSON.stringify(state.colorMapping) : undefined, }).toAst(), ], }; @@ -266,12 +292,16 @@ export const getTagcloudVisualization = ({ }, DimensionEditorComponent(props) { + const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; if (props.groupId === TAG_GROUP_ID) { return ( ); } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx index e91a73982dd38..1728e6240ad9f 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx @@ -6,27 +6,196 @@ */ import React from 'react'; -import { PaletteRegistry } from '@kbn/coloring'; +import { + PaletteRegistry, + CategoricalColorMapping, + DEFAULT_COLOR_MAPPING_CONFIG, + ColorMapping, + SPECIAL_TOKENS_STRING_CONVERTION, + PaletteOutput, + AVAILABLE_PALETTES, + getColorsFromMapping, +} from '@kbn/coloring'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiColorPaletteDisplay, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiFormRow, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { useState, MutableRefObject, useCallback } from 'react'; +import { PalettePicker } from '@kbn/coloring/src/shared_components/coloring/palette_picker'; +import { useDebouncedValue } from '@kbn/visualization-ui-components'; +import { getColorCategories } from '@kbn/chart-expressions-common'; import type { TagcloudState } from './types'; -import { PalettePicker } from '../../shared_components'; +import { PalettePanelContainer } from '../../shared_components'; +import { FramePublicAPI } from '../../types'; +import { trackUiCounterEvents } from '../../lens_ui_telemetry'; interface Props { paletteService: PaletteRegistry; state: TagcloudState; setState: (state: TagcloudState) => void; + frame: FramePublicAPI; + panelRef: MutableRefObject; + isDarkMode: boolean; } -export function TagsDimensionEditor(props: Props) { +export function TagsDimensionEditor({ + state, + frame, + setState, + panelRef, + isDarkMode, + paletteService, +}: Props) { + const { inputValue: localState, handleInputChange: setLocalState } = + useDebouncedValue({ + value: state, + onChange: setState, + }); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const [useNewColorMapping, setUseNewColorMapping] = useState(state.colorMapping ? true : false); + + const colors = getColorsFromMapping(isDarkMode, state.colorMapping); + const table = frame.activeData?.[state.layerId]; + const splitCategories = getColorCategories(table?.rows ?? [], state.tagAccessor); + + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState({ + ...localState, + colorMapping, + }); + }, + [localState, setLocalState] + ); + + const setPalette = useCallback( + (palette: PaletteOutput) => { + setLocalState({ + ...localState, + palette, + colorMapping: undefined, + }); + }, + [localState, setLocalState] + ); + + const canUseColorMapping = state.colorMapping; + return ( - { - props.setState({ - ...props.state, - palette: newPalette, - }); - }} - /> + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + data-test-subj="lns_colorMappingOrLegacyPalette_switch" + compressed + checked={useNewColorMapping} + onChange={({ target: { checked } }) => { + trackUiCounterEvents( + `color_mapping_switch_${checked ? 'enabled' : 'disabled'}` + ); + setColorMapping(checked ? { ...DEFAULT_COLOR_MAPPING_CONFIG } : undefined); + setUseNewColorMapping(checked); + }} + /> + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setPalette(newPalette); + }} + /> + )} + + +
+
+
+
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts index c4a6ff1ddb6ad..afc83074c1fa2 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts @@ -7,7 +7,7 @@ import { $Values } from '@kbn/utility-types'; import { Datatable } from '@kbn/expressions-plugin/common'; -import type { PaletteOutput } from '@kbn/coloring'; +import { PaletteOutput, ColorMapping } from '@kbn/coloring'; import { Orientation } from '@kbn/expression-tagcloud-plugin/common'; export interface TagcloudState { @@ -19,6 +19,7 @@ export interface TagcloudState { orientation: $Values; palette?: PaletteOutput; showLabel: boolean; + colorMapping?: ColorMapping.Config; } export interface TagcloudConfig extends TagcloudState { diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index 4e8264a733398..2b80a39fc3b53 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -7,7 +7,7 @@ import { Ast } from '@kbn/interpreter'; import { Position, ScaleType } from '@elastic/charts'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry } from '@kbn/coloring'; import { buildExpression, buildExpressionFunction, @@ -511,6 +511,7 @@ const dataLayerToExpression = ( name: 'default', }), ]).toAst(), + colorMapping: layer.colorMapping ? JSON.stringify(layer.colorMapping) : undefined, }); return { diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index e863f04bcdc05..8961c38b1582a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { $Values } from '@kbn/utility-types'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { ColorMapping, PaletteOutput } from '@kbn/coloring'; import type { LegendConfig, AxisExtentConfig, @@ -103,6 +103,7 @@ export interface XYDataLayerConfig { xScaleType?: XScaleType; isHistogram?: boolean; columnToLabel?: string; + colorMapping?: ColorMapping.Config; } export interface XYReferenceLineLayerConfig { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx index e89b3a843e8ac..9024e43292939 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx @@ -57,6 +57,7 @@ import { } from './visualization_helpers'; import { cloneDeep } from 'lodash'; import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import { EUIAmsterdamColorBlindPalette } from '@kbn/coloring'; const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column'; const exampleAnnotation: EventAnnotationConfig = { @@ -221,8 +222,30 @@ describe('xy_visualization', () => { "layers": Array [ Object { "accessors": Array [], + "colorMapping": Object { + "assignmentMode": "auto", + "assignments": Array [], + "colorMode": Object { + "type": "categorical", + }, + "paletteId": "${EUIAmsterdamColorBlindPalette.id}", + "specialAssignments": Array [ + Object { + "color": Object { + "colorIndex": 1, + "paletteId": "neutral", + "type": "categorical", + }, + "rule": Object { + "type": "other", + }, + "touched": false, + }, + ], + }, "layerId": "l1", "layerType": "data", + "palette": undefined, "position": "top", "seriesType": "bar_stacked", "showGridlines": false, @@ -708,12 +731,14 @@ describe('xy_visualization', () => { }, }; }); + it('when there is no date histogram annotation layer is disabled', () => { const supportedAnnotationLayer = xyVisualization .getSupportedLayers(exampleState()) .find((a) => a.type === 'annotations'); expect(supportedAnnotationLayer?.disabled).toBeTruthy(); }); + it('for data with date histogram annotation layer is enabled and calculates initial dimensions', () => { const supportedAnnotationLayer = xyVisualization .getSupportedLayers(exampleState(), frame) @@ -3193,6 +3218,34 @@ describe('xy_visualization', () => { }); describe('info', () => { + function createStateWithAnnotationProps(annotation: Partial) { + return { + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + splitAccessor: undefined, + xAccessor: DATE_HISTORGRAM_COLUMN_ID, + accessors: ['b'], + }, + { + layerId: 'layerId', + layerType: 'annotations', + indexPatternId: 'first', + annotations: [ + { + label: 'Event', + id: '1', + type: 'query', + timeField: 'start_date', + ...annotation, + }, + ], + }, + ], + } as XYState; + } function getFrameMock() { const datasourceMock = createMockDatasource('testDatasource'); datasourceMock.publicAPIMock.getOperationForColumnId.mockImplementation((id) => @@ -3216,21 +3269,47 @@ describe('xy_visualization', () => { }); } - it('should return an info message if annotation layer is ignoring the global filters', () => { - const initialState = exampleState(); + it('should not return an info message if annotation layer is ignoring the global filters but contains only manual annotations', () => { + const initialState = createStateWithAnnotationProps({}); const state: State = { ...initialState, layers: [ - ...initialState.layers, + // replace the existing annotation layers with a new one + ...initialState.layers.filter(({ layerType }) => layerType !== layerTypes.ANNOTATIONS), { layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, - annotations: [exampleAnnotation2], + annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'an3' }], ignoreGlobalFilters: true, indexPatternId: 'myIndexPattern', }, ], }; + expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() })).toHaveLength(0); + }); + + it("should return an info message if the annotation layer is ignoring filters and there's at least a query annotation", () => { + const state = createStateWithAnnotationProps({ + filter: { + language: 'kuery', + query: 'agent.keyword: *', + type: 'kibana_query', + }, + id: 'newColId', + key: { + type: 'point_in_time', + }, + label: 'agent.keyword: *', + timeField: 'timestamp', + type: 'query', + }); + + const annotationLayer = state.layers.find( + ({ layerType }) => layerType === layerTypes.ANNOTATIONS + )! as XYAnnotationLayerConfig; + annotationLayer.ignoreGlobalFilters = true; + annotationLayer.annotations.push(exampleAnnotation2); + expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() })).toContainEqual( expect.objectContaining({ displayLocations: [{ id: 'embeddableBadge' }], @@ -3241,6 +3320,30 @@ describe('xy_visualization', () => { }) ); }); + + it('should not return an info message if annotation layer is not ignoring the global filters', () => { + const state = createStateWithAnnotationProps({ + filter: { + language: 'kuery', + query: 'agent.keyword: *', + type: 'kibana_query', + }, + id: 'newColId', + key: { + type: 'point_in_time', + }, + label: 'agent.keyword: *', + timeField: 'timestamp', + type: 'query', + }); + + const annotationLayer = state.layers.find( + ({ layerType }) => layerType === layerTypes.ANNOTATIONS + )! as XYAnnotationLayerConfig; + annotationLayer.ignoreGlobalFilters = false; + annotationLayer.annotations.push(exampleAnnotation2); + expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() })).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 9f5f9755d1781..07ebec4c47b58 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -25,6 +25,8 @@ import type { EventAnnotationGroupConfig } from '@kbn/event-annotation-common'; import { isEqual } from 'lodash'; import { type AccessorConfig, DimensionTrigger } from '@kbn/visualization-ui-components'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { DEFAULT_COLOR_MAPPING_CONFIG, getColorsFromMapping } from '@kbn/coloring'; +import useObservable from 'react-use/lib/useObservable'; import { generateId } from '../../id_generator'; import { isDraggedDataViewField, @@ -254,7 +256,7 @@ export const getXyVisualization = ({ initialize( addNewLayer, state, - _mainPalette?, + mainPalette?, annotationGroups?: AnnotationGroups, references?: SavedObjectReference[] ) { @@ -276,6 +278,11 @@ export const getXyVisualization = ({ seriesType: defaultSeriesType, showGridlines: false, layerType: LayerTypes.DATA, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : undefined, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : { ...DEFAULT_COLOR_MAPPING_CONFIG }, }, ], } @@ -416,6 +423,22 @@ export const getXyVisualization = ({ } ).length < 2; + const canUseColorMapping = layer.colorMapping ? true : false; + let colors: string[] = []; + if (canUseColorMapping) { + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = getColorsFromMapping(theme.darkMode, layer.colorMapping); + }, + }) + .unsubscribe(); + } else { + colors = paletteService + .get(dataLayer.palette?.name || 'default') + .getCategoricalColors(10, dataLayer.palette?.params); + } + return { groups: [ { @@ -447,11 +470,7 @@ export const getXyVisualization = ({ { columnId: dataLayer.splitAccessor, triggerIconType: dataLayer.collapseFn ? 'aggregate' : 'colorBy', - palette: dataLayer.collapseFn - ? undefined - : paletteService - .get(dataLayer.palette?.name || 'default') - .getCategoricalColors(10, dataLayer.palette?.params), + palette: dataLayer.collapseFn ? undefined : colors, }, ] : [], @@ -469,7 +488,13 @@ export const getXyVisualization = ({ getMainPalette: (state) => { if (!state || state.layers.length === 0) return; - return getFirstDataLayer(state.layers)?.palette; + const firstDataLayer = getFirstDataLayer(state.layers); + + return firstDataLayer?.colorMapping + ? { type: 'colorMapping', value: firstDataLayer.colorMapping } + : firstDataLayer?.palette + ? { type: 'legacyPalette', value: firstDataLayer.palette } + : undefined; }, getDropProps(dropProps) { @@ -641,13 +666,15 @@ export const getXyVisualization = ({ formatFactory: fieldFormats.deserialize, paletteService, }; + + const darkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; const dimensionEditor = isReferenceLayer(layer) ? ( ) : isAnnotationsLayer(layer) ? ( ) : ( - + ); return dimensionEditor; @@ -1122,7 +1149,10 @@ function getNotifiableFeatures( fieldFormats: FieldFormatsStart ): UserMessage[] { const annotationsWithIgnoreFlag = getAnnotationsLayers(state.layers).filter( - (layer) => layer.ignoreGlobalFilters + (layer) => + layer.ignoreGlobalFilters && + // If all annotations are manual, do not report it + layer.annotations.some((annotation) => annotation.type !== 'manual') ); if (!annotationsWithIgnoreFlag.length) { return []; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx index 94c7a326ccbb4..44114e8d560ed 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx @@ -5,21 +5,45 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; -import type { PaletteRegistry } from '@kbn/coloring'; import { useDebouncedValue } from '@kbn/visualization-ui-components'; import { ColorPicker } from '@kbn/visualization-ui-components'; + +import { + EuiBadge, + EuiButtonGroup, + EuiButtonIcon, + EuiColorPaletteDisplay, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; +import { + PaletteRegistry, + ColorMapping, + DEFAULT_COLOR_MAPPING_CONFIG, + CategoricalColorMapping, + PaletteOutput, + SPECIAL_TOKENS_STRING_CONVERTION, + AVAILABLE_PALETTES, + getColorsFromMapping, +} from '@kbn/coloring'; +import { getColorCategories } from '@kbn/chart-expressions-common'; import type { VisualizationDimensionEditorProps } from '../../../types'; import { State, XYState, XYDataLayerConfig, YConfig, YAxisMode } from '../types'; import { FormatFactory } from '../../../../common/types'; import { getSeriesColor, isHorizontalChart } from '../state_helpers'; -import { PalettePicker } from '../../../shared_components'; +import { PalettePanelContainer, PalettePicker } from '../../../shared_components'; import { getDataLayers } from '../visualization_helpers'; import { CollapseSetting } from '../../../shared_components/collapse_setting'; import { getSortedAccessors } from '../to_expression'; import { getColorAssignments, getAssignedColorConfig } from '../color_assignment'; +import { trackUiCounterEvents } from '../../../lens_ui_telemetry'; type UnwrapArray = T extends Array ? P : T; @@ -43,11 +67,16 @@ export function DataDimensionEditor( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; + darkMode: boolean; } ) { - const { state, setState, layerId, accessor } = props; + const { state, layerId, accessor, darkMode } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index] as XYDataLayerConfig; + const canUseColorMapping = layer.colorMapping ? true : false; + + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const [useNewColorMapping, setUseNewColorMapping] = useState(canUseColorMapping); const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ value: props.state, @@ -79,6 +108,19 @@ export function DataDimensionEditor( [accessor, index, localState, layer, setLocalState] ); + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState(updateLayer(localState, { ...layer, colorMapping }, index)); + }, + [index, localState, layer, setLocalState] + ); + const setPalette = useCallback( + (palette: PaletteOutput) => { + setLocalState(updateLayer(localState, { ...layer, palette }, index)); + }, + [index, localState, layer, setLocalState] + ); + const overwriteColor = getSeriesColor(layer, accessor); const assignedColor = useMemo(() => { const sortedAccessors: string[] = getSortedAccessors( @@ -105,19 +147,128 @@ export function DataDimensionEditor( }, [props.frame, props.paletteService, state.layers, accessor, props.formatFactory, layer]); const localLayer: XYDataLayerConfig = layer; - if (props.groupId === 'breakdown') { + + const colors = layer.colorMapping + ? getColorsFromMapping(props.darkMode, layer.colorMapping) + : props.paletteService + .get(layer.palette?.name || 'default') + .getCategoricalColors(10, layer.palette); + + const table = props.frame.activeData?.[layer.layerId]; + const { splitAccessor } = layer; + const splitCategories = getColorCategories(table?.rows ?? [], splitAccessor); + + if (props.groupId === 'breakdown' && !layer.collapseFn) { return ( - <> - {!layer.collapseFn && ( - { - setState(updateLayer(localState, { ...localLayer, palette: newPalette }, index)); - }} - /> - )} - + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + data-test-subj="lns_colorMappingOrLegacyPalette_switch" + compressed + checked={useNewColorMapping} + onChange={({ target: { checked } }) => { + trackUiCounterEvents( + `color_mapping_switch_${checked ? 'enabled' : 'disabled'}` + ); + setColorMapping(checked ? { ...DEFAULT_COLOR_MAPPING_CONFIG } : undefined); + setUseNewColorMapping(checked); + }} + /> + + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setPalette(newPalette); + }} + /> + )} + + +
+
+
+
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx index 252c3de6b8e57..9a98f5bae168b 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx @@ -272,6 +272,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -299,6 +300,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -347,6 +349,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -392,6 +395,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -437,6 +441,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts index 8417d02d79995..a12afc7465579 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts @@ -18,7 +18,7 @@ import { generateId } from '../../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; -import type { PaletteOutput } from '@kbn/coloring'; +import { DEFAULT_COLOR_MAPPING_CONFIG, PaletteOutput } from '@kbn/coloring'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; @@ -757,7 +757,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, keptLayerIds: [], - mainPalette, + mainPalette: { type: 'legacyPalette', value: mainPalette }, }); expect((suggestion.state.layers as XYDataLayerConfig[])[0].palette).toEqual(mainPalette); @@ -773,7 +773,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, keptLayerIds: [], - mainPalette, + mainPalette: { type: 'legacyPalette', value: mainPalette }, }); expect((suggestion.state.layers as XYDataLayerConfig[])[0].palette).toEqual(undefined); @@ -913,7 +913,13 @@ describe('xy_suggestions', () => { expect(suggestions[0].state).toEqual({ ...currentState, preferredSeriesType: 'line', - layers: [{ ...currentState.layers[0], seriesType: 'line' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'line', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(suggestions[0].title).toEqual('Line chart'); }); @@ -954,12 +960,24 @@ describe('xy_suggestions', () => { expect(seriesSuggestion.state).toEqual({ ...currentState, preferredSeriesType: 'line', - layers: [{ ...currentState.layers[0], seriesType: 'line' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'line', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(stackSuggestion.state).toEqual({ ...currentState, preferredSeriesType: 'bar_stacked', - layers: [{ ...currentState.layers[0], seriesType: 'bar_stacked' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'bar_stacked', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(seriesSuggestion.title).toEqual('Line chart'); expect(stackSuggestion.title).toEqual('Stacked'); @@ -1081,6 +1099,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'product', splitAccessor: 'category', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); @@ -1126,6 +1145,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'category', splitAccessor: 'product', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); @@ -1172,6 +1192,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'timestamp', splitAccessor: 'product', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts index 33381822b6eed..b63acd9513300 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { partition } from 'lodash'; import { Position } from '@elastic/charts'; -import type { PaletteOutput } from '@kbn/coloring'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, VisualizationSuggestion, @@ -96,7 +96,7 @@ function getSuggestionForColumns( keptLayerIds: string[], currentState?: State, seriesType?: SeriesType, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], allowMixed?: boolean ): VisualizationSuggestion | Array> | undefined { const [buckets, values] = partition(table.columns, (col) => col.operation.isBucketed); @@ -230,7 +230,7 @@ function getSuggestionsForLayer({ tableLabel?: string; keptLayerIds: string[]; requestedSeriesType?: SeriesType; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); @@ -493,7 +493,7 @@ function buildSuggestion({ changeType: TableChangeType; keptLayerIds: string[]; hide?: boolean; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }) { if (seriesType.includes('percentage') && xValue?.operation.scale === 'ordinal' && !splitBy) { @@ -505,10 +505,11 @@ function buildSuggestion({ const newLayer: XYDataLayerConfig = { ...(existingLayer || {}), palette: - mainPalette || - (existingLayer && 'palette' in existingLayer + mainPalette?.type === 'legacyPalette' + ? mainPalette.value + : existingLayer && 'palette' in existingLayer ? (existingLayer as XYDataLayerConfig).palette - : undefined), + : undefined, layerId, seriesType, xAccessor: xValue?.columnId, @@ -519,6 +520,11 @@ function buildSuggestion({ ? existingLayer.yConfig.filter(({ forAccessor }) => accessors.indexOf(forAccessor) !== -1) : undefined, layerType: LayerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }; const hasDateHistogramDomain = diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index ca536dc187c3f..4fb8f849d6d27 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -86,6 +86,7 @@ "@kbn/content-management-utils", "@kbn/serverless", "@kbn/ebt-tools", + "@kbn/chart-expressions-common", "@kbn/search-response-warnings", ], "exclude": [ diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts index 3284610f53bcc..f390f7a89f87c 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DataViewListItem } from '@kbn/data-views-plugin/common'; import { AllDatasetSelection } from './all_dataset_selection'; import { SingleDatasetSelection } from './single_dataset_selection'; import { UnresolvedDatasetSelection } from './unresolved_dataset_selection'; @@ -14,6 +15,7 @@ export type DatasetSelection = | SingleDatasetSelection | UnresolvedDatasetSelection; export type DatasetSelectionChange = (datasetSelection: DatasetSelection) => void; +export type DataViewSelection = (dataView: DataViewListItem) => void; export const isDatasetSelection = (input: any): input is DatasetSelection => { return ( diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx index 1cbcd6a0f032a..28a5401f98048 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx @@ -12,8 +12,10 @@ export const INTEGRATIONS_PANEL_ID = 'dataset-selector-integrations-panel'; export const INTEGRATIONS_TAB_ID = 'dataset-selector-integrations-tab'; export const UNCATEGORIZED_PANEL_ID = 'dataset-selector-uncategorized-panel'; export const UNCATEGORIZED_TAB_ID = 'dataset-selector-uncategorized-tab'; +export const DATA_VIEWS_PANEL_ID = 'dataset-selector-data-views-panel'; +export const DATA_VIEWS_TAB_ID = 'dataset-selector-data-views-tab'; -export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 300; +export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 400; export const showAllLogsLabel = i18n.translate('xpack.logExplorer.datasetSelector.showAllLogs', { defaultMessage: 'Show all logs', @@ -28,6 +30,14 @@ export const uncategorizedLabel = i18n.translate( { defaultMessage: 'Uncategorized' } ); +export const dataViewsLabel = i18n.translate('xpack.logExplorer.datasetSelector.dataViews', { + defaultMessage: 'Data Views', +}); + +export const openDiscoverLabel = i18n.translate('xpack.logExplorer.datasetSelector.openDiscover', { + defaultMessage: 'Opens in Discover', +}); + export const sortOrdersLabel = i18n.translate('xpack.logExplorer.datasetSelector.sortOrders', { defaultMessage: 'Sort directions', }); @@ -43,6 +53,17 @@ export const noDatasetsDescriptionLabel = i18n.translate( } ); +export const noDataViewsLabel = i18n.translate('xpack.logExplorer.datasetSelector.noDataViews', { + defaultMessage: 'No data views found', +}); + +export const noDataViewsDescriptionLabel = i18n.translate( + 'xpack.logExplorer.datasetSelector.noDataViewsDescription', + { + defaultMessage: 'No data views or search results found.', + } +); + export const noIntegrationsLabel = i18n.translate( 'xpack.logExplorer.datasetSelector.noIntegrations', { defaultMessage: 'No integrations found' } diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx index 10bc958c8f2ce..82178164994eb 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx @@ -11,6 +11,7 @@ import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import type { Meta, Story } from '@storybook/react'; import { IndexPattern } from '@kbn/io-ts-utils'; +import { DataViewListItem } from '@kbn/data-views-plugin/common'; import { AllDatasetSelection, DatasetSelection, @@ -29,6 +30,10 @@ const meta: Meta = { options: [null, { message: 'Failed to fetch data streams' }], control: { type: 'radio' }, }, + dataViewsError: { + options: [null, { message: 'Failed to fetch data data views' }], + control: { type: 'radio' }, + }, integrationsError: { options: [null, { message: 'Failed to fetch data integrations' }], control: { type: 'radio' }, @@ -71,20 +76,39 @@ const DatasetSelectorTemplate: Story = (args) => { const sortedDatasets = search.sortOrder === 'asc' ? filteredDatasets : filteredDatasets.reverse(); + const filteredDataViews = mockDataViews.filter((dataView) => + dataView.name?.includes(search.name as string) + ); + + const sortedDataViews = + search.sortOrder === 'asc' ? filteredDataViews : filteredDataViews.reverse(); + + const { + datasetsError, + dataViewsError, + integrationsError, + isLoadingDataViews, + isLoadingIntegrations, + isLoadingUncategorized, + } = args; + return ( ); }; @@ -92,12 +116,18 @@ const DatasetSelectorTemplate: Story = (args) => { export const Basic = DatasetSelectorTemplate.bind({}); Basic.args = { datasetsError: null, + dataViewsError: null, integrationsError: null, + isLoadingDataViews: false, isLoadingIntegrations: false, - isLoadingStreams: false, + isLoadingUncategorized: false, + isSearchingIntegrations: false, + onDataViewsReload: () => alert('Reload data views...'), + onDataViewSelection: (dataView) => alert(`Navigate to data view "${dataView.name}"`), + onDataViewsTabClick: () => console.log('Load data views...'), onIntegrationsReload: () => alert('Reload integrations...'), - onStreamsEntryClick: () => console.log('Load uncategorized streams...'), - onUnmanagedStreamsReload: () => alert('Reloading streams...'), + onUncategorizedTabClick: () => console.log('Load uncategorized streams...'), + onUncategorizedReload: () => alert('Reloading streams...'), }; const mockIntegrations: Integration[] = [ @@ -477,3 +507,25 @@ const mockDatasets: Dataset[] = [ { name: 'data-load-balancing-logs-*' as IndexPattern }, { name: 'data-scaling-logs-*' as IndexPattern }, ].map((dataset) => Dataset.create(dataset)); + +const mockDataViews: DataViewListItem[] = [ + { + id: 'logs-*', + namespaces: ['default'], + title: 'logs-*', + name: 'logs-*', + }, + { + id: 'metrics-*', + namespaces: ['default'], + title: 'metrics-*', + name: 'metrics-*', + }, + { + id: '7258d186-6430-4b51-bb67-2603cdfb4652', + namespaces: ['default'], + title: 'synthetics-*', + typeMeta: {}, + name: 'synthetics-dashboard', + }, +]; diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx index f9e722effc784..10d8b4c046c9a 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx @@ -10,6 +10,9 @@ import styled from '@emotion/styled'; import { EuiContextMenu, EuiHorizontalRule, EuiTab, EuiTabs } from '@elastic/eui'; import { useIntersectionRef } from '../../hooks/use_intersection_ref'; import { + dataViewsLabel, + DATA_VIEWS_PANEL_ID, + DATA_VIEWS_TAB_ID, DATA_VIEW_POPOVER_CONTENT_WIDTH, integrationsLabel, INTEGRATIONS_PANEL_ID, @@ -25,19 +28,30 @@ import { SelectorActions } from './sub_components/selector_actions'; import { DatasetSelectorProps } from './types'; import { buildIntegrationsTree, + createDataViewsStatusItem, createIntegrationStatusItem, createUncategorizedStatusItem, } from './utils'; +import { getDataViewTestSubj } from '../../utils/get_data_view_test_subj'; +import { DataViewsPanelTitle } from './sub_components/data_views_panel_title'; export function DatasetSelector({ datasets, - datasetsError, datasetSelection, + datasetsError, + dataViews, + dataViewsError, integrations, integrationsError, + isLoadingDataViews, isLoadingIntegrations, - isLoadingStreams, + isLoadingUncategorized, isSearchingIntegrations, + onDataViewSelection, + onDataViewsReload, + onDataViewsSearch, + onDataViewsSort, + onDataViewsTabClick, onIntegrationsLoadMore, onIntegrationsReload, onIntegrationsSearch, @@ -45,10 +59,10 @@ export function DatasetSelector({ onIntegrationsStreamsSearch, onIntegrationsStreamsSort, onSelectionChange, - onStreamsEntryClick, - onUnmanagedStreamsReload, - onUnmanagedStreamsSearch, - onUnmanagedStreamsSort, + onUncategorizedReload, + onUncategorizedSearch, + onUncategorizedSort, + onUncategorizedTabClick, }: DatasetSelectorProps) { const { panelId, @@ -62,21 +76,26 @@ export function DatasetSelector({ searchByName, selectAllLogDataset, selectDataset, + selectDataView, sortByOrder, switchToIntegrationsTab, switchToUncategorizedTab, + switchToDataViewsTab, togglePopover, } = useDatasetSelector({ initialContext: { selection: datasetSelection }, + onDataViewSelection, + onDataViewsSearch, + onDataViewsSort, onIntegrationsLoadMore, onIntegrationsReload, onIntegrationsSearch, onIntegrationsSort, onIntegrationsStreamsSearch, onIntegrationsStreamsSort, - onUnmanagedStreamsSearch, - onUnmanagedStreamsSort, - onUnmanagedStreamsReload, + onUncategorizedSearch, + onUncategorizedSort, + onUncategorizedReload, onSelectionChange, }); @@ -117,8 +136,8 @@ export function DatasetSelector({ createUncategorizedStatusItem({ data: datasets, error: datasetsError, - isLoading: isLoadingStreams, - onRetry: onUnmanagedStreamsReload, + isLoading: isLoadingUncategorized, + onRetry: onUncategorizedReload, }), ]; } @@ -127,7 +146,26 @@ export function DatasetSelector({ name: dataset.title, onClick: () => selectDataset(dataset), })); - }, [datasets, datasetsError, isLoadingStreams, selectDataset, onUnmanagedStreamsReload]); + }, [datasets, datasetsError, isLoadingUncategorized, selectDataset, onUncategorizedReload]); + + const dataViewsItems = useMemo(() => { + if (!dataViews || dataViews.length === 0) { + return [ + createDataViewsStatusItem({ + data: dataViews, + error: dataViewsError, + isLoading: isLoadingDataViews, + onRetry: onDataViewsReload, + }), + ]; + } + + return dataViews.map((dataView) => ({ + 'data-test-subj': getDataViewTestSubj(dataView.title), + name: dataView.name, + onClick: () => selectDataView(dataView), + })); + }, [dataViews, dataViewsError, isLoadingDataViews, selectDataView, onDataViewsReload]); const tabs = [ { @@ -140,11 +178,20 @@ export function DatasetSelector({ id: UNCATEGORIZED_TAB_ID, name: uncategorizedLabel, onClick: () => { - onStreamsEntryClick(); // Lazy-load uncategorized datasets only when accessing the Uncategorized tab + onUncategorizedTabClick(); // Lazy-load uncategorized datasets only when accessing the Uncategorized tab switchToUncategorizedTab(); }, 'data-test-subj': 'datasetSelectorUncategorizedTab', }, + { + id: DATA_VIEWS_TAB_ID, + name: dataViewsLabel, + onClick: () => { + onDataViewsTabClick(); // Lazy-load data views only when accessing the Data Views tab + switchToDataViewsTab(); + }, + 'data-test-subj': 'datasetSelectorDataViewsTab', + }, ]; const tabEntries = tabs.map((tab) => ( @@ -175,7 +222,7 @@ export function DatasetSelector({ search={search} onSearch={searchByName} onSort={sortByOrder} - isLoading={isSearchingIntegrations || isLoadingStreams} + isLoading={isSearchingIntegrations || isLoadingUncategorized} /> {/* For a smoother user experience, we keep each tab content mount and we only show the select one @@ -215,6 +262,22 @@ export function DatasetSelector({ data-test-subj="uncategorizedContextMenu" size="s" /> + {/* Data views tab content */} +