diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts index ca73a1d6a21e1..dcd7ac4c5a7c7 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts +++ b/src/plugins/chart_expressions/expression_gauge/public/components/utils/accessors.ts @@ -70,7 +70,7 @@ export const getMaxValue = ( if (isRespectRanges && paletteParams?.rangeMax) { const metricValue = accessors?.metric ? getValueFromAccessor(accessors.metric, row) : undefined; return !metricValue || metricValue < paletteParams?.rangeMax - ? paletteParams?.rangeMax + ? paletteParams.rangeMax : metricValue; } @@ -93,16 +93,16 @@ export const getMinValue = ( accessors?: Accessors, paletteParams?: CustomPaletteParams, isRespectRanges?: boolean -) => { +): number => { const currentValue = accessors?.min ? getValueFromAccessor(accessors.min, row) : undefined; - if (currentValue !== undefined && currentValue !== null) { + if (currentValue != null) { return currentValue; } if (isRespectRanges && paletteParams?.rangeMin) { const metricValue = accessors?.metric ? getValueFromAccessor(accessors.metric, row) : undefined; return !metricValue || metricValue > paletteParams?.rangeMin - ? paletteParams?.rangeMin + ? paletteParams.rangeMin : metricValue; } @@ -121,7 +121,7 @@ export const getMinValue = ( export const getGoalValue = (row?: DatatableRow, accessors?: Accessors) => { const currentValue = accessors?.goal ? getValueFromAccessor(accessors.goal, row) : undefined; - if (currentValue !== undefined && currentValue !== null) { + if (currentValue != null) { return currentValue; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx b/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx index c17339945161b..4afcd4b8bbc5e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormBasedLayer } from '../..'; +import { InfoBadge } from '../../shared_components/info_badges/info_badge'; import { FramePublicAPI, VisualizationInfo } from '../../types'; import { getSamplingValue } from './utils'; @@ -22,38 +22,27 @@ export function ReducedSamplingSectionEntries({ visualizationInfo: VisualizationInfo; dataViews: FramePublicAPI['dataViews']; }) { - const { euiTheme } = useEuiTheme(); return ( <> {layers.map(([id, layer], layerIndex) => { const dataView = dataViews.indexPatterns[layer.indexPatternId]; + const layerInfo = visualizationInfo.layers.find(({ layerId, label }) => layerId === id); const layerTitle = - visualizationInfo.layers.find(({ layerId }) => layerId === id)?.label || + layerInfo?.label || i18n.translate('xpack.lens.indexPattern.samplingPerLayer.fallbackLayerName', { defaultMessage: 'Data layer', }); + const layerPalette = layerInfo?.palette; return ( -
  • - - - {layerTitle} - - - {`${Number(getSamplingValue(layer)) * 100}%`} - - -
  • + {`${Number(getSamplingValue(layer)) * 100}%`} + ); })} diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/formula_editor.tsx index dd41a62fd2431..941047519cdac 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/formula_editor.tsx @@ -53,9 +53,10 @@ import { LANGUAGE_ID } from './math_tokenization'; import './formula.scss'; import { FormulaIndexPatternColumn } from '../formula'; import { insertOrReplaceFormulaColumn } from '../parse'; -import { filterByVisibleOperation, nonNullable } from '../util'; +import { filterByVisibleOperation } from '../util'; import { getColumnTimeShiftWarnings, getDateHistogramInterval } from '../../../../time_shift_utils'; import { getDocumentationSections } from './formula_help'; +import { nonNullable } from '../../../../../../utils'; function tableHasData( activeData: ParamEditorProps['activeData'], diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts index 11d6797a1c997..773dd5c97009e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts @@ -23,10 +23,11 @@ import type { import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; import moment from 'moment'; +import { nonNullable } from '../../../../../../utils'; import { DateRange } from '../../../../../../../common/types'; import type { IndexPattern } from '../../../../../../types'; import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; -import { tinymathFunctions, groupArgsByType, unquotedStringRegex, nonNullable } from '../util'; +import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util'; import type { GenericOperationDefinition } from '../..'; import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help'; import { hasFunctionFieldArgument } from '../validation'; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx index 20432dcdb6e24..c61cf7685682a 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { uniqBy } from 'lodash'; +import { nonNullable } from '../../../../../utils'; import type { BaseIndexPatternColumn, FieldBasedOperationErrorMessage, @@ -18,7 +19,7 @@ import { runASTValidation, tryToParse } from './validation'; import { WrappedFormulaEditor } from './editor'; import { insertOrReplaceFormulaColumn } from './parse'; import { generateFormula } from './generate'; -import { filterByVisibleOperation, nonNullable } from './util'; +import { filterByVisibleOperation } from './util'; import { getManagedColumnsFrom } from '../../layer_helpers'; import { generateMissingFieldMessage, getFilter, isColumnFormatted } from '../helpers'; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/parse.ts index 57a529df033c4..b01bd77077842 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/parse.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isObject } from 'lodash'; import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; +import { nonNullable } from '../../../../../utils'; import type { DateRange } from '../../../../../../common/types'; import type { IndexPattern } from '../../../../../types'; import { @@ -26,7 +27,6 @@ import { getOperationParams, groupArgsByType, mergeWithGlobalFilters, - nonNullable, } from './util'; import { FormulaIndexPatternColumn, isFormulaIndexPatternColumn } from './formula'; import { getColumnOrder } from '../../layer_helpers'; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts index 1d3301bd050e6..6dcc727ec0e70 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts @@ -14,6 +14,7 @@ import type { TinymathVariable, } from '@kbn/tinymath'; import type { Query } from '@kbn/es-query'; +import { nonNullable } from '../../../../../utils'; import type { OperationDefinition, GenericIndexPatternColumn, @@ -736,10 +737,6 @@ Example: Average revenue per customer but in some cases customer id is not provi }, }; -export function nonNullable(v: T): v is NonNullable { - return v != null; -} - export function isMathNode(node: TinymathAST | string) { return isObject(node) && node.type === 'function' && tinymathFunctions[node.name]; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts index 40b6a827e5ac2..95eb8a84e6849 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts @@ -18,6 +18,7 @@ import { REASON_ID_TYPES, validateAbsoluteTimeShift, } from '@kbn/data-plugin/common'; +import { nonNullable } from '../../../../../utils'; import { DateRange } from '../../../../../../common/types'; import { findMathNodes, @@ -27,7 +28,6 @@ import { getValueOrName, groupArgsByType, isMathNode, - nonNullable, tinymathFunctions, } from './util'; 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 9a31b98328b53..d489230d14587 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 @@ -7,16 +7,14 @@ import { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public'; import { Ast } from '@kbn/interpreter'; -import memoizeOne from 'memoize-one'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { difference } from 'lodash'; import type { DataViewsContract, DataViewSpec } from '@kbn/data-views-plugin/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import type { TimefilterContract } from '@kbn/data-plugin/public'; -import { +import type { Datasource, - DatasourceLayers, DatasourceMap, IndexPattern, IndexPatternMap, @@ -28,9 +26,10 @@ import { import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; import { getActiveDatasourceIdFromDoc, sortDataViewRefs } from '../../utils'; -import type { DatasourceStates, DataViewsState, VisualizationState } from '../../state_management'; +import type { DatasourceStates, VisualizationState } from '../../state_management'; import { readFromStorage } from '../../settings_storage'; import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader'; +import { getDatasourceLayers } from '../../state_management/utils'; function getIndexPatterns( references?: SavedObjectReference[], @@ -283,30 +282,6 @@ export function initializeDatasources({ return states; } -export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( - datasourceStates: DatasourceStates, - datasourceMap: DatasourceMap, - indexPatterns: DataViewsState['indexPatterns'] -) { - const datasourceLayers: DatasourceLayers = {}; - Object.keys(datasourceMap) - .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading) - .forEach((id) => { - const datasourceState = datasourceStates[id].state; - const datasource = datasourceMap[id]; - - const layers = datasource.getLayers(datasourceState); - layers.forEach((layer) => { - datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ - state: datasourceState, - layerId: layer, - indexPatterns, - }); - }); - }); - return datasourceLayers; -}); - export async function persistedStateToExpression( datasourceMap: DatasourceMap, visualizations: VisualizationMap, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 882638fac9684..787200e53315b 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -126,6 +126,7 @@ import { } from '../app_plugin/get_application_user_messages'; import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list'; import { EmbeddableFeatureBadge } from './embeddable_info_badges'; +import { getDatasourceLayers } from '../state_management/utils'; export type LensSavedObjectAttributes = Omit; @@ -610,7 +611,7 @@ export class Embeddable visualizationMap: this.deps.visualizationMap, activeDatasource: this.activeDatasource, activeDatasourceState: { - isLoading: Boolean(this.activeDatasourceState), + isLoading: !this.activeDatasourceState, state: this.activeDatasourceState, }, dataViews: { @@ -631,7 +632,16 @@ export class Embeddable indexPatterns: this.indexPatterns, indexPatternRefs: this.indexPatternRefs, }, - datasourceLayers: {}, // TODO + datasourceLayers: getDatasourceLayers( + { + [this.activeDatasourceId!]: { + isLoading: !this.activeDatasourceState, + state: this.activeDatasourceState, + }, + }, + this.deps.datasourceMap, + this.indexPatterns + ), query: this.savedVis.state.query, filters: mergedSearchContext.filters ?? [], dateRange: { @@ -646,7 +656,8 @@ export class Embeddable setState: () => {}, frame: frameDatasourceAPI, visualizationInfo: this.activeVisualization?.getVisualizationInfo?.( - this.activeVisualizationState + this.activeVisualizationState, + frameDatasourceAPI ), }) ?? []), ...(this.activeVisualization?.getUserMessages?.(this.activeVisualizationState, { @@ -802,6 +813,13 @@ export class Embeddable ); this.activeData = newActiveData; + + // Refresh messanges if info type is found as with active data + // these messages can be enriched + if (this._userMessages.some(({ severity }) => severity === 'info')) { + this.loadUserMessages(); + this.renderUserMessages(); + } }; private onRender: ExpressionWrapperProps['onRender$'] = () => { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx index d47892b0b3aa0..88e91f34c4f35 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx @@ -49,7 +49,7 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } title={iconTitle} size="s" css={css` - color: ${euiTheme.colors.emptyShade}; + color: transparent; font-size: ${xsFontSize}; height: ${euiTheme.size.l} !important; .euiButtonEmpty__content { @@ -68,22 +68,29 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] } isOpen={isPopoverOpen} closePopover={closePopover} > -
    - {messages.map(({ shortMessage, longMessage }, index) => ( - - ))} +
    + {messages.map(({ shortMessage, longMessage }, index) => { + return ( + <> + {index ? : null} + + + ); + })}
    ); diff --git a/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.test.tsx b/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.test.tsx new file mode 100644 index 0000000000000..f1a718b5c2976 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { InfoBadge } from './info_badge'; + +describe('Info badge', () => { + it('should render no icon if no palette is passed', () => { + const res = render( + + ); + + expect(res.queryByTestId('prefix-0-icon')).not.toBeInTheDocument(); + expect(res.queryByTestId('prefix-0-palette')).not.toBeInTheDocument(); + }); + + it('should render an icon if a single palette color is passed over', () => { + const res = render( + + ); + + expect(res.queryByTestId('prefix-0-icon')).toBeInTheDocument(); + expect(res.queryByTestId('prefix-0-palette')).not.toBeInTheDocument(); + }); + + it('should render both an icon an a palette indicator if multiple colors are passed over', () => { + const res = render( + + ); + + expect(res.queryByTestId('prefix-0-icon')).toBeInTheDocument(); + expect(res.queryByTestId('prefix-0-palette')).toBeInTheDocument(); + }); + + it('should render children as value when passed', () => { + const res = render( + +
    100%
    +
    + ); + expect(res.getByText('100%')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.tsx b/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.tsx new file mode 100644 index 0000000000000..cc78c59b69eea --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/info_badges/info_badge.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiColorPaletteDisplay, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { type ReactChildren, type ReactChild } from 'react'; + +export function InfoBadge({ + title, + dataView, + index, + palette, + children, + 'data-test-subj-prefix': dataTestSubjPrefix, +}: { + title: string; + dataView: string; + index: number; + palette?: string[]; + children?: ReactChild | ReactChildren; + 'data-test-subj-prefix': string; +}) { + const { euiTheme } = useEuiTheme(); + const hasColor = Boolean(palette); + const hasSingleColor = palette && palette.length === 1; + const hasMultipleColors = palette && palette.length > 1; + const iconType = hasSingleColor ? 'stopFilled' : 'color'; + return ( +
  • + + {hasColor ? ( + + + + ) : null} + + + {title} + + + {children} + + {hasMultipleColors ? ( +
    + +
    + ) : null} +
  • + ); +} diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index a9a98c50c38d6..407aacba9e4a2 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -11,7 +11,7 @@ import { SavedObjectReference } from '@kbn/core/public'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import { LensState } from './types'; import { Datasource, DatasourceMap, VisualizationMap } from '../types'; -import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; +import { getDatasourceLayers } from './utils'; export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc; export const selectQuery = (state: LensState) => state.lens.query; diff --git a/x-pack/plugins/lens/public/state_management/utils.ts b/x-pack/plugins/lens/public/state_management/utils.ts new file mode 100644 index 0000000000000..3d19326938d35 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/utils.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import memoizeOne from 'memoize-one'; +import type { DatasourceMap, DatasourceLayers } from '../types'; +import type { DatasourceStates, DataViewsState } from './types'; + +export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( + datasourceStates: DatasourceStates, + datasourceMap: DatasourceMap, + indexPatterns: DataViewsState['indexPatterns'] +) { + const datasourceLayers: DatasourceLayers = {}; + Object.keys(datasourceMap) + .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading) + .forEach((id) => { + const datasourceState = datasourceStates[id].state; + const datasource = datasourceMap[id]; + + const layers = datasource.getLayers(datasourceState); + layers.forEach((layer) => { + datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ + state: datasourceState, + layerId: layer, + indexPatterns, + }); + }); + }); + return datasourceLayers; +}); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index faa3d8e14e5bd..1904be8c1541c 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -164,6 +164,7 @@ export interface VisualizationInfo { icon?: IconType; label?: string; dimensions: Array<{ name: string; id: string; dimensionType: string }>; + palette?: string[]; }>; } @@ -1291,7 +1292,7 @@ export interface Visualization { props: VisualizationStateFromContextChangeProps ) => Suggestion | undefined; - getVisualizationInfo?: (state: T) => VisualizationInfo; + getVisualizationInfo?: (state: T, frame?: FramePublicAPI) => VisualizationInfo; /** * A visualization can return custom dimensions for the reporting tool */ diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 0ac87039c819c..dd08bd8a2c5f2 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -361,3 +361,7 @@ export const getSearchWarningMessages = ( return [...warningsMap.values()].flat(); }; + +export function nonNullable(v: T): v is NonNullable { + return v != null; +} diff --git a/x-pack/plugins/lens/public/visualization_container.scss b/x-pack/plugins/lens/public/visualization_container.scss index cdadb22feb634..028b8aa8a3a14 100644 --- a/x-pack/plugins/lens/public/visualization_container.scss +++ b/x-pack/plugins/lens/public/visualization_container.scss @@ -30,5 +30,6 @@ // Make the visualization modifiers icon appear only on panel hover .embPanel__content:hover .lnsEmbeddablePanelFeatureList_button { color: $euiTextColor; - transition: color $euiAnimSpeedSlow; + background: $euiColorEmptyShade; + transition: color $euiAnimSpeedSlow, background $euiAnimSpeedSlow; } \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 9d7266b711bef..ecc6bc958124f 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -610,7 +610,11 @@ export const getDatatableVisualization = ({ return suggestion; }, - getVisualizationInfo(state: DatatableVisualizationState) { + getVisualizationInfo(state) { + const visibleMetricColumns = state.columns.filter( + (c) => !c.hidden && c.colorMode && c.colorMode !== 'none' + ); + return { layers: [ { @@ -618,6 +622,11 @@ export const getDatatableVisualization = ({ layerType: state.layerType, chartType: 'table', ...this.getDescription(state), + palette: + // if multiple columns have color by value, do not show the palette for now: see #154349 + visibleMetricColumns.length > 1 + ? undefined + : visibleMetricColumns[0]?.palette?.params?.stops?.map(({ color }) => color), dimensions: state.columns.map((column) => { let name = i18n.translate('xpack.lens.datatable.metric', { defaultMessage: 'Metric', diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index aa8ef574cd7c7..ddab89444419f 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -27,6 +27,7 @@ import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { DatasourceLayers, + FramePublicAPI, OperationMetadata, Suggestion, UserMessage, @@ -234,23 +235,12 @@ export const getGaugeVisualization = ({ getSuggestions, getConfiguration({ state, frame }) { - const hasColoring = Boolean(state.colorMode !== 'none' && state.palette?.params?.stops); - const row = state?.layerId ? frame?.activeData?.[state?.layerId]?.rows?.[0] : undefined; - const { metricAccessor } = state ?? {}; - - const accessors = getAccessorsFromState(state); - - let palette; - if (!(row == null || metricAccessor == null || state?.palette == null || !hasColoring)) { - const currentMinMax = { - min: getMinValue(row, accessors), - max: getMaxValue(row, accessors), - }; - - const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax); - palette = displayStops.map(({ color }) => color); - } + const { palette, metricAccessor, accessors } = getConfigurationAccessorsAndPalette( + state, + paletteService, + frame.activeData + ); return { groups: [ @@ -602,11 +592,16 @@ export const getGaugeVisualization = ({ return suggestion; }, - getVisualizationInfo(state: GaugeVisualizationState) { + getVisualizationInfo(state, frame) { + const { palette, accessors } = getConfigurationAccessorsAndPalette( + state, + paletteService, + frame?.activeData + ); const dimensions = []; - if (state.metricAccessor) { + if (accessors?.metric) { dimensions.push({ - id: state.metricAccessor, + id: accessors.metric, name: i18n.translate('xpack.lens.gauge.metricLabel', { defaultMessage: 'Metric', }), @@ -614,9 +609,9 @@ export const getGaugeVisualization = ({ }); } - if (state.maxAccessor) { + if (accessors?.max) { dimensions.push({ - id: state.maxAccessor, + id: accessors.max, name: i18n.translate('xpack.lens.gauge.maxValueLabel', { defaultMessage: 'Maximum value', }), @@ -624,9 +619,9 @@ export const getGaugeVisualization = ({ }); } - if (state.minAccessor) { + if (accessors?.min) { dimensions.push({ - id: state.minAccessor, + id: accessors.min, name: i18n.translate('xpack.lens.gauge.minValueLabel', { defaultMessage: 'Minimum value', }), @@ -634,9 +629,9 @@ export const getGaugeVisualization = ({ }); } - if (state.goalAccessor) { + if (accessors?.goal) { dimensions.push({ - id: state.goalAccessor, + id: accessors.goal, name: i18n.translate('xpack.lens.gauge.goalValueLabel', { defaultMessage: 'Goal value', }), @@ -651,8 +646,44 @@ export const getGaugeVisualization = ({ chartType: state.shape, ...this.getDescription(state), dimensions, + palette, }, ], }; }, }); + +// When the active data comes from the embeddable side it might not have been indexed by layerId +// rather using a "default" key +function getActiveDataForLayer( + layerId: string | undefined, + activeData: FramePublicAPI['activeData'] | undefined +) { + if (activeData && layerId) { + return activeData[layerId] || activeData.default; + } +} + +function getConfigurationAccessorsAndPalette( + state: GaugeVisualizationState, + paletteService: PaletteRegistry, + activeData?: FramePublicAPI['activeData'] +) { + const hasColoring = Boolean(state.colorMode !== 'none' && state.palette?.params?.stops); + + const row = getActiveDataForLayer(state?.layerId, activeData)?.rows?.[0]; + const { metricAccessor } = state ?? {}; + + const accessors = getAccessorsFromState(state); + + let palette; + if (row != null && metricAccessor != null && state?.palette != null && hasColoring) { + const currentMinMax = { + min: getMinValue(row, accessors), + max: getMaxValue(row, accessors), + }; + const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax); + palette = displayStops.map(({ color }) => color); + } + return { metricAccessor, accessors, palette }; +} diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx index 91c9e60f38d8f..7d448e56e9fee 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx @@ -515,7 +515,7 @@ export const getHeatmapVisualization = ({ return suggestion; }, - getVisualizationInfo(state: HeatmapVisualizationState) { + getVisualizationInfo(state, frame) { const dimensions = []; if (state.xAccessor) { dimensions.push({ @@ -543,6 +543,15 @@ export const getHeatmapVisualization = ({ }); } + const { displayStops } = getSafePaletteParams( + paletteService, + // When the active data comes from the embeddable side it might not have been indexed by layerId + // rather using a "default" key + frame?.activeData?.[state.layerId] || frame?.activeData?.default, + state.valueAccessor, + state?.palette && state.palette.accessor === state.valueAccessor ? state.palette : undefined + ); + return { layers: [ { @@ -551,6 +560,7 @@ export const getHeatmapVisualization = ({ chartType: state.shape, ...this.getDescription(state), dimensions, + palette: displayStops.length ? displayStops.map(({ color }) => color) : undefined, }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx index 957010b5b131e..85fea42623fc4 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx @@ -324,6 +324,9 @@ export const getLegacyMetricVisualization = ({ }); } + const hasColoring = state.palette != null; + const stops = state.palette?.params?.stops || []; + return { layers: [ { @@ -332,6 +335,7 @@ export const getLegacyMetricVisualization = ({ chartType: 'metric', ...this.getDescription(state), dimensions, + palette: hasColoring ? stops.map(({ color }) => color) : undefined, }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index b44f783cb83ef..b4b0b159a2711 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -35,6 +35,7 @@ import { Toolbar } from './toolbar'; import { generateId } from '../../id_generator'; import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector'; import { toExpression } from './to_expression'; +import { nonNullable } from '../../utils'; export const DEFAULT_MAX_COLUMNS = 3; @@ -666,7 +667,7 @@ export const getMetricVisualization = ({ return suggestion; }, - getVisualizationInfo(state: MetricVisualizationState) { + getVisualizationInfo(state) { const dimensions = []; if (state.metricAccessor) { dimensions.push({ @@ -706,6 +707,10 @@ export const getMetricVisualization = ({ }); } + const stops = state.palette?.params?.stops || []; + const hasStaticColoring = !!state.color; + const hasDynamicColoring = !!state.palette; + return { layers: [ { @@ -714,6 +719,12 @@ export const getMetricVisualization = ({ chartType: 'metric', ...this.getDescription(state), dimensions, + palette: (hasDynamicColoring + ? stops.map(({ color }) => color) + : hasStaticColoring + ? [state.color] + : [getDefaultColor(state)] + ).filter(nonNullable), }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index da52c6efc105b..ca6216f3838b0 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -46,6 +46,7 @@ import { DimensionDataExtraEditor, DimensionEditor, PieToolbar } from './toolbar import { LayerSettings } from './layer_settings'; import { checkTableForContainsSmallValues } from './render_helpers'; import { DatasourcePublicAPI } from '../..'; +import { nonNullable } from '../../utils'; const metricLabel = i18n.translate('xpack.lens.pie.groupMetricLabelSingular', { defaultMessage: 'Metric', @@ -202,7 +203,7 @@ export const getPieVisualization = ({ // count multiple metrics as a bucket dimension so that the rest of the dimension // groups UI behaves correctly. const multiMetricsBucketDimensionCount = - layer.metrics.length > 1 && state.shape !== 'mosaic' ? 1 : 0; + layer.metrics.length > 1 && state.shape !== PieChartTypes.MOSAIC ? 1 : 0; const totalNonCollapsedAccessors = accessors.reduce( @@ -223,8 +224,8 @@ export const getPieVisualization = ({ : undefined; switch (state.shape) { - case 'donut': - case 'pie': + case PieChartTypes.DONUT: + case PieChartTypes.PIE: return { ...primaryGroupConfigBaseProps, groupLabel: i18n.translate('xpack.lens.pie.sliceGroupLabel', { @@ -239,7 +240,7 @@ export const getPieVisualization = ({ dataTestSubj: 'lnsPie_sliceByDimensionPanel', hideGrouping: true, }; - case 'mosaic': + case PieChartTypes.MOSAIC: return { ...primaryGroupConfigBaseProps, groupLabel: i18n.translate('xpack.lens.pie.verticalAxisLabel', { @@ -267,7 +268,7 @@ export const getPieVisualization = ({ dimensionsTooMany: totalNonCollapsedAccessors - PartitionChartsMeta[state.shape].maxBuckets, dataTestSubj: 'lnsPie_groupByDimensionPanel', - hideGrouping: state.shape === 'treemap', + hideGrouping: state.shape === PieChartTypes.TREEMAP, }; } }; @@ -297,7 +298,7 @@ export const getPieVisualization = ({ ); switch (state.shape) { - case 'mosaic': + case PieChartTypes.MOSAIC: return { ...secondaryGroupConfigBaseProps, groupLabel: i18n.translate('xpack.lens.pie.horizontalAxisLabel', { @@ -374,8 +375,8 @@ export const getPieVisualization = ({ return { groups: [getPrimaryGroupConfig(), getSecondaryGroupConfig(), getMetricGroupConfig()].filter( - Boolean - ) as VisualizationDimensionGroupConfig[], + nonNullable + ), }; }, @@ -595,7 +596,7 @@ export const getPieVisualization = ({ if ( numericColumn && - state.shape === 'waffle' && + state.shape === PieChartTypes.WAFFLE && layer.primaryGroups.length && checkTableForContainsSmallValues(frame.activeData[layerId], numericColumn.id, 1) ) { @@ -619,7 +620,7 @@ export const getPieVisualization = ({ return metricColId; } }) - .filter(Boolean) as string[]; + .filter(nonNullable); if (metricsWithArrayValues.length) { const labels = metricsWithArrayValues.map( @@ -650,10 +651,42 @@ export const getPieVisualization = ({ return [...errors, ...warningMessages]; }, - getVisualizationInfo(state: PieVisualizationState) { + getVisualizationInfo(state, frame) { const layer = state.layers[0]; const dimensions: VisualizationInfo['layers'][number]['dimensions'] = []; + const datasource = frame?.datasourceLayers[layer.layerId]; + const hasSliceBy = layer.primaryGroups.length + (layer.secondaryGroups?.length || 0); + const hasMultipleMetrics = layer.allowMultipleMetrics; + const palette = []; + + if (!hasSliceBy && datasource) { + if (hasMultipleMetrics) { + palette.push( + ...layer.metrics.map( + (columnId) => + layer.colorsByDimension?.[columnId] ?? + getDefaultColorForMultiMetricDimension({ + layer, + columnId, + paletteService, + datasource, + }) + ) + ); + } else if (!layer.primaryGroups?.length) { + // This is a logic integrated in the renderer, here simulated + // In the particular case of no color assigned (as no sliceBy dimension defined) + // the color is generated on the fly from the default palette + palette.push( + ...paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(Math.max(10, layer.metrics.length)) + .slice(0, layer.metrics.length) + ); + } + } + layer.metrics.forEach((metric) => { dimensions.push({ id: metric, @@ -662,7 +695,7 @@ export const getPieVisualization = ({ }); }); - if (state.shape === 'mosaic' && layer.secondaryGroups && layer.secondaryGroups.length) { + if (state.shape === PieChartTypes.MOSAIC && layer.secondaryGroups?.length) { layer.secondaryGroups.forEach((accessor) => { dimensions.push({ name: i18n.translate('xpack.lens.pie.horizontalAxisLabel', { @@ -674,18 +707,19 @@ export const getPieVisualization = ({ }); } - if (layer.primaryGroups && layer.primaryGroups.length) { + if (layer.primaryGroups?.length) { let name = i18n.translate('xpack.lens.pie.treemapGroupLabel', { defaultMessage: 'Group by', }); let dimensionType = 'group_by'; - if (state.shape === 'mosaic') { + + if (state.shape === PieChartTypes.MOSAIC) { name = i18n.translate('xpack.lens.pie.verticalAxisLabel', { defaultMessage: 'Vertical axis', }); dimensionType = 'vertical_axis'; } - if (state.shape === 'donut' || state.shape === 'pie') { + if (state.shape === PieChartTypes.DONUT || state.shape === PieChartTypes.PIE) { name = i18n.translate('xpack.lens.pie.sliceGroupLabel', { defaultMessage: 'Slice by', }); @@ -698,8 +732,18 @@ export const getPieVisualization = ({ id: accessor, }); }); + + if (layer.primaryGroups.some((id) => !isCollapsed(id, layer))) { + palette.push( + ...paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params) + ); + } } + const finalPalette = palette.filter(nonNullable); + return { layers: [ { @@ -708,6 +752,7 @@ export const getPieVisualization = ({ chartType: state.shape, ...this.getDescription(state), dimensions, + palette: finalPalette.length ? finalPalette : undefined, }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts b/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts index 5c0afb1eb47a7..1416797ad8014 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts @@ -44,15 +44,13 @@ export function getColorAssignments( ): ColorAssignments { const layersPerPalette: Record = {}; - layers - .filter((layer): layer is XYDataLayerConfig => isDataLayer(layer)) - .forEach((layer) => { - const palette = layer.palette?.name || 'default'; - if (!layersPerPalette[palette]) { - layersPerPalette[palette] = []; - } - layersPerPalette[palette].push(layer); - }); + layers.filter(isDataLayer).forEach((layer) => { + const palette = layer.palette?.name || 'default'; + if (!layersPerPalette[palette]) { + layersPerPalette[palette] = []; + } + layersPerPalette[palette].push(layer); + }); return mapValues(layersPerPalette, (paletteLayers) => { const seriesPerLayer = paletteLayers.map((layer, layerIndex) => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx b/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx index d3c0ac1653ad5..fcff325cb9b23 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { InfoBadge } from '../../shared_components/info_badges/info_badge'; import { FramePublicAPI, VisualizationInfo } from '../../types'; import { XYAnnotationLayerConfig } from './types'; @@ -21,30 +20,25 @@ export function IgnoredGlobalFiltersEntries({ visualizationInfo: VisualizationInfo; dataViews: FramePublicAPI['dataViews']; }) { - const { euiTheme } = useEuiTheme(); return ( <> {layers.map((layer, layerIndex) => { const dataView = dataViews.indexPatterns[layer.indexPatternId]; + const layerInfo = visualizationInfo.layers.find(({ layerId }) => layerId === layer.layerId); const layerTitle = - visualizationInfo.layers.find(({ layerId, label }) => layerId === layer.layerId)?.label || + layerInfo?.label || i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', { defaultMessage: 'Annotations', }); + const layerPalette = layerInfo?.palette; return ( -
  • - - - {layerTitle} - - -
  • + ); })} diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 9b82ab8c0d90e..9f1d1625835c1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -2242,7 +2242,8 @@ describe('xy_visualization', () => { expect(yConfigs?.accessors[1].columnId).toEqual('b'); expect(yConfigs?.accessors[1].color).toEqual('green'); - paletteGetter.mockClear(); + // This call restores the initial state of the paletteGetter + paletteGetter.mockRestore(); }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 2f5df56c7b42d..1ecd9adbd8cc5 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -26,6 +26,7 @@ import { isDraggedDataViewField, isOperationFromCompatibleGroup, isOperationFromTheSameGroup, + nonNullable, renewIDs, } from '../../utils'; import { getSuggestions } from './xy_suggestions'; @@ -74,6 +75,7 @@ import { getUniqueLabels, onAnnotationDrop, isDateHistogram, + getSingleColorAnnotationConfig, } from './annotations/helpers'; import { checkXAccessorCompatibility, @@ -868,7 +870,7 @@ export const getXyVisualization = ({ ); } - const info = getNotifiableFeatures(state, frame.dataViews); + const info = getNotifiableFeatures(state, frame, paletteService, fieldFormats); return errors.concat(warnings, info); }, @@ -913,7 +915,9 @@ export const getXyVisualization = ({ return suggestion; }, - getVisualizationInfo, + getVisualizationInfo(state, frame) { + return getVisualizationInfo(state, frame, paletteService, fieldFormats); + }, }); const getMappedAccessors = ({ @@ -954,18 +958,26 @@ const getMappedAccessors = ({ return mappedAccessors; }; -function getVisualizationInfo(state: XYState) { +function getVisualizationInfo( + state: XYState, + frame: Partial | undefined, + paletteService: PaletteRegistry, + fieldFormats: FieldFormatsStart +) { const isHorizontal = isHorizontalChart(state.layers); const visualizationLayersInfo = state.layers.map((layer) => { + const palette = []; const dimensions = []; let chartType: SeriesType | undefined; let icon; let label; + if (isDataLayer(layer)) { chartType = layer.seriesType; const layerVisType = visualizationTypes.find((visType) => visType.id === chartType); icon = layerVisType?.icon; label = layerVisType?.fullLabel || layerVisType?.label; + if (layer.xAccessor) { dimensions.push({ name: getAxisName('x', { isHorizontal }), @@ -981,6 +993,21 @@ function getVisualizationInfo(state: XYState) { dimensionType: 'y', }); }); + if (frame?.datasourceLayers && frame.activeData) { + const sortedAccessors: string[] = getSortedAccessors( + frame.datasourceLayers[layer.layerId], + layer + ); + const mappedAccessors = getMappedAccessors({ + state, + frame: frame as Pick, + layer, + fieldFormats, + paletteService, + accessors: sortedAccessors, + }); + palette.push(...mappedAccessors.flatMap(({ color }) => color)); + } } if (layer.splitAccessor) { dimensions.push({ @@ -990,6 +1017,13 @@ function getVisualizationInfo(state: XYState) { dimensionType: 'breakdown', id: layer.splitAccessor, }); + if (!layer.collapseFn) { + palette.push( + ...paletteService + .get(layer.palette?.name || 'default') + .getCategoricalColors(10, layer.palette?.params) + ); + } } } if (isReferenceLayer(layer) && layer.accessors && layer.accessors.length) { @@ -1006,6 +1040,20 @@ function getVisualizationInfo(state: XYState) { defaultMessage: 'Reference lines', }); icon = IconChartBarReferenceLine; + if (frame?.datasourceLayers && frame.activeData) { + const sortedAccessors: string[] = getSortedAccessors( + frame.datasourceLayers[layer.layerId], + layer + ); + palette.push( + ...getReferenceConfiguration({ + state, + frame: frame as Pick, + layer, + sortedAccessors, + }).groups.flatMap(({ accessors }) => accessors.map(({ color }) => color)) + ); + } } if (isAnnotationsLayer(layer) && layer.annotations && layer.annotations.length) { layer.annotations.forEach((annotation) => { @@ -1021,8 +1069,15 @@ function getVisualizationInfo(state: XYState) { defaultMessage: 'Annotations', }); icon = IconChartBarAnnotations; + palette.push( + ...layer.annotations + .filter(({ isHidden }) => !isHidden) + .map((annotation) => getSingleColorAnnotationConfig(annotation).color) + ); } + const finalPalette = palette?.filter(nonNullable); + return { layerId: layer.layerId, layerType: layer.layerType, @@ -1030,6 +1085,7 @@ function getVisualizationInfo(state: XYState) { icon, label, dimensions, + palette: finalPalette.length ? finalPalette : undefined, }; }); return { @@ -1039,7 +1095,9 @@ function getVisualizationInfo(state: XYState) { function getNotifiableFeatures( state: XYState, - dataViews: FramePublicAPI['dataViews'] + frame: Pick & Partial, + paletteService: PaletteRegistry, + fieldFormats: FieldFormatsStart ): UserMessage[] { const annotationsWithIgnoreFlag = getAnnotationsLayers(state.layers).filter( (layer) => layer.ignoreGlobalFilters @@ -1047,7 +1105,7 @@ function getNotifiableFeatures( if (!annotationsWithIgnoreFlag.length) { return []; } - const visualizationInfo = getVisualizationInfo(state); + const visualizationInfo = getVisualizationInfo(state, frame, paletteService, fieldFormats); return [ { @@ -1061,7 +1119,7 @@ function getNotifiableFeatures( ), displayLocations: [{ id: 'embeddableBadge' }],