;
diff --git a/src/plugins/vis_types/vislib/public/vis_controller.tsx b/src/plugins/vis_types/vislib/public/vis_controller.tsx
index 7bae32d031b46..1e940d23e83da 100644
--- a/src/plugins/vis_types/vislib/public/vis_controller.tsx
+++ b/src/plugins/vis_types/vislib/public/vis_controller.tsx
@@ -79,9 +79,6 @@ export const createVislibVisController = (
return;
}
- const [, { kibanaLegacy }] = await core.getStartServices();
- kibanaLegacy.loadFontAwesome();
-
// @ts-expect-error
const { Vis: Vislib } = await import('./vislib/vis');
const { uiState, event: fireEvent } = handlers;
diff --git a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/_tooltip.scss b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/_tooltip.scss
index bafec7edf3b94..6448ceca38c23 100644
--- a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/_tooltip.scss
+++ b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/_tooltip.scss
@@ -49,8 +49,7 @@
}
.visTooltip__headerIcon {
- flex: 0 0 auto;
- padding-right: $euiSizeS;
+ margin-right: $euiSizeXS;
}
.visTooltip__headerText {
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/_alerts.scss b/src/plugins/vis_types/vislib/public/vislib/lib/_alerts.scss
deleted file mode 100644
index 596f4675b1254..0000000000000
--- a/src/plugins/vis_types/vislib/public/vislib/lib/_alerts.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-
-.visAlerts__tray {
- position: absolute;
- bottom: ($euiSizeXS + 1px);
- left: 0;
- right: 0;
- list-style: none;
- padding: 0;
-
- transition-property: opacity;
- transition-delay: $euiAnimSpeedExtraFast;
- transition-duration: $euiAnimSpeedExtraFast;
-}
-
-.visAlerts__icon {
- margin: 0;
- padding: 0 $euiSizeS;
- flex: 0 0 auto;
- align-self: center;
-}
-
-.visAlerts__text {
- flex: 1 1 auto;
- margin: 0;
- padding: 0;
-}
-
-.visAlerts__close {
- cursor: pointer;
-}
-
-.visAlert {
- margin: 0 $euiSizeS $euiSizeS;
- padding: $euiSizeXS $euiSizeS $euiSizeXS $euiSizeXS;
- display: flex;
-}
-
-// Modifier naming and colors.
-$visAlertTypes: (
- info: $euiColorPrimary,
- success: $euiColorSecondary,
- warning: $euiColorWarning,
- danger: $euiColorDanger,
-);
-
-// Create button modifiders based upon the map.
-@each $name, $color in $visAlertTypes {
- .visAlert--#{$name} {
- $backgroundColor: tintOrShade($color, 90%, 70%);
- $textColor: makeHighContrastColor($color, $backgroundColor);
-
- background-color: $backgroundColor;
- color: $textColor;
- }
-}
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/_index.scss b/src/plugins/vis_types/vislib/public/vislib/lib/_index.scss
index 6ab284fd8d122..5217bc7300bf5 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/_index.scss
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/_index.scss
@@ -1,2 +1 @@
-@import './alerts';
@import './layout/index';
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/alerts.js b/src/plugins/vis_types/vislib/public/vislib/lib/alerts.js
deleted file mode 100644
index 6d4299c125edf..0000000000000
--- a/src/plugins/vis_types/vislib/public/vislib/lib/alerts.js
+++ /dev/null
@@ -1,79 +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 $ from 'jquery';
-import _ from 'lodash';
-
-/**
- * Adds alerts that float in front of a visualization
- *
- * @class Alerts
- * @constructor
- * @param el {HTMLElement} Reference to DOM element
- */
-export class Alerts {
- constructor(vis, alertDefs) {
- this.vis = vis;
- this.data = vis.data;
- this.alertDefs = _.cloneDeep(alertDefs);
-
- this.alerts = _(alertDefs)
- .map((alertDef) => {
- if (!alertDef) return;
- if (alertDef.test && !alertDef.test(vis, this.data)) return;
- return this._addAlert(alertDef);
- })
- .compact();
- }
-
- _addAlert(alertDef) {
- const type = alertDef.type || 'info';
- const icon = alertDef.icon || type;
- const msg = alertDef.msg;
- // alert container
- const $icon = $('').addClass('visAlerts__icon fa fa-' + icon);
- const $text = $(' ').addClass('visAlerts__text').text(msg);
- const $closeIcon = $('').addClass('fa fa-close');
- const $closeDiv = $('').addClass('visAlerts__close').append($closeIcon);
-
- const $alert = $('
')
- .addClass('visAlert visAlert--' + type)
- .append([$icon, $text, $closeDiv]);
- $closeDiv.on('click', () => {
- $alert.remove();
- });
-
- return $alert;
- }
-
- // renders initial alerts
- render() {
- const alerts = this.alerts;
- const vis = this.vis;
-
- $(vis.element).find('.visWrapper__alerts').append($('
').addClass('visAlerts__tray'));
- if (!alerts.size()) return;
- $(vis.element).find('.visAlerts__tray').append(alerts.value());
- }
-
- // shows new alert
- show(msg, type) {
- const vis = this.vis;
- const alert = {
- msg: msg,
- type: type,
- };
- if (this.alertDefs.find((alertDef) => alertDef.msg === alert.msg)) return;
- this.alertDefs.push(alert);
- $(vis.element).find('.visAlerts__tray').append(this._addAlert(alert));
- }
-
- destroy() {
- $(this.vis.element).find('.visWrapper__alerts').remove();
- }
-}
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/binder.ts b/src/plugins/vis_types/vislib/public/vislib/lib/binder.ts
index 886745ba19563..31e49697d4bd9 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/binder.ts
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/binder.ts
@@ -33,14 +33,14 @@ export class Binder {
destroyers.forEach((fn) => fn());
}
- jqOn(el: HTMLElement, ...args: [string, (event: JQueryEventObject) => void]) {
+ jqOn(el: HTMLElement, ...args: [string, (event: JQuery.Event) => void]) {
const $el = $(el);
$el.on(...args);
this.disposal.push(() => $el.off(...args));
}
- fakeD3Bind(el: HTMLElement, event: string, handler: (event: JQueryEventObject) => void) {
- this.jqOn(el, event, (e: JQueryEventObject) => {
+ fakeD3Bind(el: HTMLElement, event: string, handler: (event: JQuery.Event) => void) {
+ this.jqOn(el, event, (e: JQuery.Event) => {
// mimic https://github.com/mbostock/d3/blob/3abb00113662463e5c19eb87cd33f6d0ddc23bc0/src/selection/on.js#L87-L94
const o = d3.event; // Events can be reentrant (e.g., focus).
d3.event = e;
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/handler.js b/src/plugins/vis_types/vislib/public/vislib/lib/handler.js
index a2b747f4d5d9c..7a68b128faf09 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/handler.js
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/handler.js
@@ -17,7 +17,6 @@ import { visTypes as chartTypes } from '../visualizations/vis_types';
import { NoResults } from '../errors';
import { Layout } from './layout/layout';
import { ChartTitle } from './chart_title';
-import { Alerts } from './alerts';
import { Axis } from './axis/axis';
import { ChartGrid as Grid } from './chart_grid';
import { Binder } from './binder';
@@ -46,7 +45,6 @@ export class Handler {
this.ChartClass = chartTypes[visConfig.get('type')];
this.uiSettings = uiSettings;
this.charts = [];
-
this.vis = vis;
this.visConfig = visConfig;
this.data = visConfig.data;
@@ -56,7 +54,6 @@ export class Handler {
.map((axisArgs) => new Axis(visConfig, axisArgs));
this.valueAxes = visConfig.get('valueAxes').map((axisArgs) => new Axis(visConfig, axisArgs));
this.chartTitle = new ChartTitle(visConfig);
- this.alerts = new Alerts(this, visConfig.get('alerts'));
this.grid = new Grid(this, visConfig.get('grid'));
if (visConfig.get('type') === 'point_series') {
@@ -69,7 +66,7 @@ export class Handler {
this.layout = new Layout(visConfig);
this.binder = new Binder();
- this.renderArray = _.filter([this.layout, this.chartTitle, this.alerts], Boolean);
+ this.renderArray = _.filter([this.layout, this.chartTitle], Boolean);
this.renderArray = this.renderArray
.concat(this.valueAxes)
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss b/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss
index 7ead0b314c7ad..4612602d93f1c 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss
@@ -271,10 +271,6 @@
min-width: 0;
}
-.visWrapper__alerts {
- position: relative;
-}
-
// General Axes
.visAxis__column--top .axis-div svg {
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/column_layout.js b/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/column_layout.js
index e8cc0f15e89e2..02910394035f8 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/column_layout.js
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/column_layout.js
@@ -90,10 +90,6 @@ export function columnLayout(el, data) {
class: 'visWrapper__chart',
splits: chartSplit,
},
- {
- type: 'div',
- class: 'visWrapper__alerts',
- },
{
type: 'div',
class: 'visAxis--x visAxis__column--bottom',
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/gauge_layout.js b/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/gauge_layout.js
index 6b38f126232c7..e3b808b63c8c1 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/gauge_layout.js
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/layout/types/gauge_layout.js
@@ -51,10 +51,6 @@ export function gaugeLayout(el, data) {
class: 'visWrapper__chart',
splits: chartSplit,
},
- {
- type: 'div',
- class: 'visWrapper__alerts',
- },
{
type: 'div',
class: 'visAxis__splitTitles--x',
diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.js
index 30dc2d82d4890..cc9e48897d053 100644
--- a/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.js
+++ b/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.js
@@ -18,7 +18,6 @@ const DEFAULT_VIS_CONFIG = {
style: {
margin: { top: 10, right: 3, bottom: 5, left: 3 },
},
- alerts: [],
categoryAxes: [],
valueAxes: [],
grid: {},
diff --git a/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx b/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx
index 55955da07ebdd..731fbed7482c4 100644
--- a/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx
+++ b/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import ReactDOM from 'react-dom/server';
+import { EuiIcon } from '@elastic/eui';
interface Props {
wholeBucket: boolean;
@@ -16,7 +17,7 @@ interface Props {
export const touchdownTemplate = ({ wholeBucket }: Props) => {
return ReactDOM.renderToStaticMarkup(
-
+
{wholeBucket ? 'Part of this bucket' : 'This area'} may contain partial data. The selected
time range does not fully cover it.
diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/gauges/meter.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/gauges/meter.js
index ad278847b0780..4073aeeed434b 100644
--- a/src/plugins/vis_types/vislib/public/vislib/visualizations/gauges/meter.js
+++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/gauges/meter.js
@@ -175,7 +175,6 @@ export class MeterGauge {
const marginFactor = 0.95;
const tooltip = this.gaugeChart.tooltip;
const isTooltip = this.gaugeChart.handler.visConfig.get('addTooltip');
- const isDisplayWarning = this.gaugeChart.handler.visConfig.get('isDisplayWarning', false);
const { angleFactor, maxAngle, minAngle } =
this.gaugeConfig.gaugeType === 'Circle' ? circleAngles : arcAngles;
const maxRadius = (Math.min(width, height / angleFactor) / 2) * marginFactor;
@@ -261,7 +260,6 @@ export class MeterGauge {
.style('fill', (d) => this.getColorBucket(Math.max(min, d.y)));
const smallContainer = svg.node().getBBox().height < 70;
- let hiddenLabels = smallContainer;
// If the value label is hidden we later want to hide also all other labels
// since they don't make sense as long as the actual value is hidden.
@@ -286,7 +284,6 @@ export class MeterGauge {
// The text is too long if it's larger than the inner free space minus a couple of random pixels for padding.
const textTooLong = textLength >= getInnerFreeSpace() - 6;
if (textTooLong) {
- hiddenLabels = true;
valueLabelHidden = true;
}
return textTooLong ? 'none' : 'initial';
@@ -302,9 +299,6 @@ export class MeterGauge {
.style('display', function () {
const textLength = this.getBBox().width;
const textTooLong = textLength > maxRadius;
- if (textTooLong) {
- hiddenLabels = true;
- }
return smallContainer || textTooLong ? 'none' : 'initial';
});
@@ -317,9 +311,6 @@ export class MeterGauge {
.style('display', function () {
const textLength = this.getBBox().width;
const textTooLong = textLength > maxRadius;
- if (textTooLong) {
- hiddenLabels = true;
- }
return valueLabelHidden || smallContainer || textTooLong ? 'none' : 'initial';
});
}
@@ -335,10 +326,6 @@ export class MeterGauge {
});
}
- if (hiddenLabels && isDisplayWarning) {
- this.gaugeChart.handler.alerts.show('Some labels were hidden due to size constraints');
- }
-
//center the visualization
const transformX = width / 2;
const transformY = height / 2 > maxRadius ? height / 2 : maxRadius;
diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/heatmap_chart.js
index bef6c939f864a..ecab91103d614 100644
--- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/heatmap_chart.js
+++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/heatmap_chart.js
@@ -248,7 +248,6 @@ export class HeatmapChart extends PointSeries {
};
}
- let hiddenLabels = false;
squares
.append('text')
.text((d) => zAxisFormatter(d.y))
@@ -257,9 +256,6 @@ export class HeatmapChart extends PointSeries {
const textHeight = this.getBBox().height;
const textTooLong = textLength > maxLength;
const textTooWide = textHeight > maxHeight;
- if (!d.hide && (textTooLong || textTooWide)) {
- hiddenLabels = true;
- }
return d.hide || textTooLong || textTooWide ? 'none' : 'initial';
})
.style('dominant-baseline', 'central')
@@ -278,9 +274,6 @@ export class HeatmapChart extends PointSeries {
const verticalCenter = y(d) + squareHeight / 2;
return `rotate(${rotate},${horizontalCenter},${verticalCenter})`;
});
- if (hiddenLabels) {
- this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints');
- }
}
if (isTooltip) {
diff --git a/src/plugins/vis_types/vislib/tsconfig.json b/src/plugins/vis_types/vislib/tsconfig.json
index 8246b3f30646b..db00cd34203e6 100644
--- a/src/plugins/vis_types/vislib/tsconfig.json
+++ b/src/plugins/vis_types/vislib/tsconfig.json
@@ -17,7 +17,6 @@
{ "path": "../../data/tsconfig.json" },
{ "path": "../../expressions/tsconfig.json" },
{ "path": "../../visualizations/tsconfig.json" },
- { "path": "../../kibana_legacy/tsconfig.json" },
{ "path": "../../kibana_utils/tsconfig.json" },
{ "path": "../../vis_default_editor/tsconfig.json" },
{ "path": "../../vis_types/xy/tsconfig.json" },
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js
index d0ebe00b1a6f0..db6a9f2beb776 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.js
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.js
@@ -136,6 +136,30 @@ function convertSeriesParams(visState) {
];
}
+/**
+ * This function is responsible for updating old TSVB visStates.
+ * Specifically, it identifies if the series and metrics ids are numbers
+ * and convert them to string with an x prefix. Number ids are never been generated
+ * from the editor, only programmatically. See https://github.com/elastic/kibana/issues/113601.
+ */
+function convertNumIdsToStringsForTSVB(visState) {
+ if (visState.params.series) {
+ visState.params.series.forEach((s) => {
+ const seriesId = s.id;
+ const metrics = s.metrics;
+ if (!isNaN(seriesId)) {
+ s.id = `x${seriesId}`;
+ }
+ metrics?.forEach((m) => {
+ const metricId = m.id;
+ if (!isNaN(metricId)) {
+ m.id = `x${metricId}`;
+ }
+ });
+ });
+ }
+}
+
/**
* This function is responsible for updating old visStates - the actual saved object
* object - into the format, that will be required by the current Kibana version.
@@ -155,6 +179,10 @@ export const updateOldState = (visState) => {
convertSeriesParams(newState);
}
+ if (visState.params && visState.type === 'metrics') {
+ convertNumIdsToStringsForTSVB(newState);
+ }
+
if (visState.type === 'gauge' && visState.fontSize) {
delete newState.fontSize;
set(newState, 'gauge.style.fontSize', visState.fontSize);
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.test.js b/src/plugins/visualizations/public/legacy/vis_update_state.test.js
index 3b0d732df2d1a..a7c2df506d313 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.test.js
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.test.js
@@ -93,4 +93,87 @@ describe('updateOldState', () => {
expect(state.params.showMeticsAtAllLevels).toBe(undefined);
});
});
+
+ describe('TSVB ids conversion', () => {
+ it('should update the seriesId from number to string with x prefix', () => {
+ const oldState = {
+ type: 'metrics',
+ params: {
+ series: [
+ {
+ id: '10',
+ },
+ {
+ id: 'ABC',
+ },
+ {
+ id: 1,
+ },
+ ],
+ },
+ };
+ const state = updateOldState(oldState);
+ expect(state.params.series).toEqual([
+ {
+ id: 'x10',
+ },
+ {
+ id: 'ABC',
+ },
+ {
+ id: 'x1',
+ },
+ ]);
+ });
+ it('should update the metrics ids from number to string with x prefix', () => {
+ const oldState = {
+ type: 'metrics',
+ params: {
+ series: [
+ {
+ id: '10',
+ metrics: [
+ {
+ id: '1000',
+ },
+ {
+ id: '74a66e70-ac44-11eb-9865-6b616e971cf8',
+ },
+ ],
+ },
+ {
+ id: 'ABC',
+ metrics: [
+ {
+ id: null,
+ },
+ ],
+ },
+ ],
+ },
+ };
+ const state = updateOldState(oldState);
+ expect(state.params.series).toEqual([
+ {
+ id: 'x10',
+ metrics: [
+ {
+ id: 'x1000',
+ },
+ {
+ id: '74a66e70-ac44-11eb-9865-6b616e971cf8',
+ },
+ ],
+ },
+ {
+ id: 'ABC',
+ metrics: [
+ {
+ id: 'xnull',
+ },
+ ],
+ },
+ ]);
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
index 43a8ab3d507d8..f9fa2a09c47e9 100644
--- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
+++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
@@ -17,6 +17,7 @@ import {
commonAddEmptyValueColorRule,
commonMigrateTagCloud,
commonAddDropLastBucketIntoTSVBModel,
+ commonRemoveMarkdownLessFromTSVB,
} from '../migrations/visualization_common_migrations';
const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableRecord) => {
@@ -68,6 +69,13 @@ const byValueMigrateTagcloud = (state: SerializableRecord) => {
};
};
+const byValueRemoveMarkdownLessFromTSVB = (state: SerializableRecord) => {
+ return {
+ ...state,
+ savedVis: commonRemoveMarkdownLessFromTSVB(state.savedVis),
+ };
+};
+
export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
return {
id: 'visualization',
@@ -86,6 +94,7 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
byValueMigrateTagcloud,
byValueAddDropLastBucketIntoTSVBModel
)(state),
+ '8.0.0': (state) => flow(byValueRemoveMarkdownLessFromTSVB)(state),
},
};
};
diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
index 2503ac2c54b12..34bae3f279f97 100644
--- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
@@ -157,3 +157,32 @@ export const commonMigrateTagCloud = (visState: any) => {
return visState;
};
+
+export const commonRemoveMarkdownLessFromTSVB = (visState: any) => {
+ if (visState && visState.type === 'metrics') {
+ const params: any = get(visState, 'params') || {};
+
+ if (params.type === 'markdown') {
+ // remove less
+ if (params.markdown_less) {
+ delete params.markdown_less;
+ }
+
+ // remove markdown id from css
+ if (params.markdown_css) {
+ params.markdown_css = params.markdown_css
+ .replace(new RegExp(`#markdown-${params.id}`, 'g'), '')
+ .trim();
+ }
+ }
+
+ return {
+ ...visState,
+ params: {
+ ...params,
+ },
+ };
+ }
+
+ return visState;
+};
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
index d9801b8a59504..1ef9018f3472b 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
@@ -2312,4 +2312,36 @@ describe('migration visualization', () => {
expect(palette.name).toEqual('default');
});
});
+
+ describe('8.0.0 removeMarkdownLessFromTSVB', () => {
+ const migrate = (doc: any) =>
+ visualizationSavedObjectTypeMigrations['8.0.0'](
+ doc as Parameters[0],
+ savedObjectMigrationContext
+ );
+ const getTestDoc = () => ({
+ attributes: {
+ title: 'My Vis',
+ description: 'This is my super cool vis.',
+ visState: JSON.stringify({
+ type: 'metrics',
+ title: '[Flights] Delay Type',
+ params: {
+ id: 'test1',
+ type: 'markdown',
+ markdwon_less: 'test { color: red }',
+ markdown_css: '#markdown-test1 test { color: red }',
+ },
+ }),
+ },
+ });
+
+ it('should remove markdown_less and id from markdown_css', () => {
+ const migratedTestDoc = migrate(getTestDoc());
+ const params = JSON.parse(migratedTestDoc.attributes.visState).params;
+
+ expect(params.mardwon_less).toBeUndefined();
+ expect(params.markdown_css).toEqual('test { color: red }');
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
index fd08ecd748668..b598d34943e6c 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
@@ -19,6 +19,7 @@ import {
commonAddEmptyValueColorRule,
commonMigrateTagCloud,
commonAddDropLastBucketIntoTSVBModel,
+ commonRemoveMarkdownLessFromTSVB,
} from './visualization_common_migrations';
const migrateIndexPattern: SavedObjectMigrationFn = (doc) => {
@@ -1068,6 +1069,29 @@ export const replaceIndexPatternReference: SavedObjectMigrationFn = (d
: doc.references,
});
+export const removeMarkdownLessFromTSVB: SavedObjectMigrationFn = (doc) => {
+ const visStateJSON = get(doc, 'attributes.visState');
+ let visState;
+
+ if (visStateJSON) {
+ try {
+ visState = JSON.parse(visStateJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ }
+
+ const newVisState = commonRemoveMarkdownLessFromTSVB(visState);
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ visState: JSON.stringify(newVisState),
+ },
+ };
+ }
+ return doc;
+};
+
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@@ -1121,4 +1145,5 @@ export const visualizationSavedObjectTypeMigrations = {
replaceIndexPatternReference,
addDropLastBucketIntoTSVBModel
),
+ '8.0.0': flow(removeMarkdownLessFromTSVB),
};
diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts
index 2d1d085198bb4..4b1344ee8e84c 100644
--- a/test/api_integration/apis/custom_integration/integrations.ts
+++ b/test/api_integration/apis/custom_integration/integrations.ts
@@ -12,18 +12,32 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
- describe('get list of append integrations', () => {
- it('should return list of custom integrations that can be appended', async () => {
- const resp = await supertest
- .get(`/api/customIntegrations/appendCustomIntegrations`)
- .set('kbn-xsrf', 'kibana')
- .expect(200);
+ describe('customIntegrations', () => {
+ describe('get list of append integrations', () => {
+ it('should return list of custom integrations that can be appended', async () => {
+ const resp = await supertest
+ .get(`/internal/customIntegrations/appendCustomIntegrations`)
+ .set('kbn-xsrf', 'kibana')
+ .expect(200);
- expect(resp.body).to.be.an('array');
- expect(resp.body.length).to.be.above(2); // Should at least have registered the three sample data-sets
+ expect(resp.body).to.be.an('array');
+ expect(resp.body.length).to.be.above(2); // Should at least have registered the three sample data-sets
- ['flights', 'logs', 'ecommerce'].forEach((sampleData) => {
- expect(resp.body.findIndex((c: { id: string }) => c.id === sampleData)).to.be.above(-1);
+ ['flights', 'logs', 'ecommerce'].forEach((sampleData) => {
+ expect(resp.body.findIndex((c: { id: string }) => c.id === sampleData)).to.be.above(-1);
+ });
+ });
+ });
+
+ describe('get list of replacement integrations', () => {
+ it('should return list of custom integrations that can be used to replace EPR packages', async () => {
+ const resp = await supertest
+ .get(`/internal/customIntegrations/replacementCustomIntegrations`)
+ .set('kbn-xsrf', 'kibana')
+ .expect(200);
+
+ expect(resp.body).to.be.an('array');
+ expect(resp.body.length).to.be.above(2); // Should have at least a few beats registered
});
});
});
diff --git a/test/common/services/saved_object_info/README.md b/test/common/services/saved_object_info/README.md
index 5f081e48e2639..c5e36f2596ddc 100644
--- a/test/common/services/saved_object_info/README.md
+++ b/test/common/services/saved_object_info/README.md
@@ -1,6 +1,70 @@
-# Tips for using the SO INFO SVC CLI with JQ
+# Tips for using the SO INFO SVC
-## Myriad ways to use jq to discern discrete info from the svc
+## From an FTR test
+```
+ ...
+ const soInfo = getService('savedObjectInfo');
+ const log = getService('log');
+
+ describe('some test suite', function () {
+ ...
+
+ after(async () => {
+ // "Normal" logging, without JQ
+ await soInfo.logSoTypes(log);
+ // Without a title, using JQ
+ await soInfo.filterSoTypes(log, '.[] | .key');
+ // With a title, using JQ
+ await soInfo.filterSoTypes(
+ log,
+ 'reduce .[].doc_count as $item (0; . + $item)',
+ 'TOTAL count of ALL Saved Object types'
+ );
+ // With a title, using JQ
+ await soInfo.filterSoTypes(
+ log,
+ '.[] | select(.key =="canvas-workpad-template") | .doc_count',
+ 'TOTAL count of canvas-workpad-template'
+ );
+ });
+```
+
+## From the CLI
+
+Run the cli
+> the **--esUrl** arg is required; tells the svc which elastic search endpoint to use
+
+```shell
+ λ node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes
+```
+
+Result
+
+```shell
+ ### types:
+
+ [
+ {
+ doc_count: 5,
+ key: 'canvas-workpad-template'
+ },
+ {
+ doc_count: 1,
+ key: 'apm-telemetry'
+ },
+ {
+ doc_count: 1,
+ key: 'config'
+ },
+ {
+ doc_count: 1,
+ key: 'space'
+ }
+ ]
+```
+
+
+### Myriad ways to use JQ to discern discrete info from the svc
Below, I will leave out the so types call, which is:
`node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes --json`
diff --git a/test/common/services/saved_object_info/index.ts b/test/common/services/saved_object_info/index.ts
index 41367694373f3..799c9964fde7f 100644
--- a/test/common/services/saved_object_info/index.ts
+++ b/test/common/services/saved_object_info/index.ts
@@ -33,7 +33,7 @@ Show information pertaining to the saved objects in the .kibana index
Examples:
-See 'saved_objects_info_svc.md'
+See 'README.md'
`,
flags: expectedFlags(),
diff --git a/test/common/services/saved_object_info/saved_object_info.ts b/test/common/services/saved_object_info/saved_object_info.ts
index 61472ea98d879..a75dfd8f3b5aa 100644
--- a/test/common/services/saved_object_info/saved_object_info.ts
+++ b/test/common/services/saved_object_info/saved_object_info.ts
@@ -13,6 +13,7 @@ import { flow, pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/lib/TaskEither';
import * as T from 'fp-ts/lib/Task';
import { ToolingLog } from '@kbn/dev-utils';
+import { run as jq } from 'node-jq';
import { FtrService } from '../../ftr_provider_context';
import { print } from './utils';
@@ -60,8 +61,22 @@ export const types =
export class SavedObjectInfoService extends FtrService {
private readonly config = this.ctx.getService('config');
+ private readonly typesF = async () =>
+ await types(url.format(this.config.get('servers.elasticsearch')))();
+
public async logSoTypes(log: ToolingLog, msg: string | null = null) {
// @ts-ignore
- pipe(await types(url.format(this.config.get('servers.elasticsearch'))), print(log)(msg));
+ pipe(await this.typesF(), print(log)(msg));
+ }
+
+ /**
+ * See test/common/services/saved_object_info/README.md for "jq filtering" ideas.
+ */
+ public async filterSoTypes(log: ToolingLog, jqFilter: string, title: string | null = null) {
+ pipe(await this.typesF(), filterAndLog);
+
+ async function filterAndLog(payload: any) {
+ log.info(`${title ? title + '\n' : ''}${await jq(jqFilter, payload, { input: 'json' })}`);
+ }
}
}
diff --git a/test/common/services/saved_object_info/saved_objects_info_svc.md b/test/common/services/saved_object_info/saved_objects_info_svc.md
deleted file mode 100644
index 2d623129e2906..0000000000000
--- a/test/common/services/saved_object_info/saved_objects_info_svc.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Saved Objects Info Svc w/ CLI
-
-## Used via the cli
-
-Run the cli
-> the **--esUrl** arg is required; tells the svc which elastic search endpoint to use
-
-```shell
- λ node scripts/saved_objs_info.js --esUrl http://elastic:changeme@localhost:9220 --soTypes
-```
-
-Result
-
-```shell
- ### types:
-
- [
- {
- doc_count: 5,
- key: 'canvas-workpad-template'
- },
- {
- doc_count: 1,
- key: 'apm-telemetry'
- },
- {
- doc_count: 1,
- key: 'config'
- },
- {
- doc_count: 1,
- key: 'space'
- }
- ]
-```
diff --git a/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts b/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts
index 566e6f033d2fd..29c914d76a8c5 100644
--- a/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts
+++ b/test/functional/apps/dashboard_elements/input_control_vis/input_control_range.ts
@@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']);
- describe('input control range', () => {
+ // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113744
+ describe.skip('input control range', () => {
before(async () => {
await PageObjects.visualize.initTests();
await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']);
diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js
index 4aa06f4cd9ad7..2e965c275d6dd 100644
--- a/test/functional/apps/management/_scripted_fields.js
+++ b/test/functional/apps/management/_scripted_fields.js
@@ -367,7 +367,8 @@ export default function ({ getService, getPageObjects }) {
});
});
- describe('creating and using Painless date scripted fields', function describeIndexTests() {
+ // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113745
+ describe.skip('creating and using Painless date scripted fields', function describeIndexTests() {
const scriptedPainlessFieldName2 = 'painDate';
it('should create scripted field', async function () {
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index 853a926f4f6e8..8fe2e4139e6ca 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -217,8 +217,9 @@ export class CommonPageObject extends FtrService {
{
basePath = '',
shouldLoginIfPrompted = true,
- disableWelcomePrompt = true,
hash = '',
+ search = '',
+ disableWelcomePrompt = true,
insertTimestamp = true,
} = {}
) {
@@ -229,11 +230,13 @@ export class CommonPageObject extends FtrService {
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
pathname: `${basePath}${appConfig.pathname}`,
hash: hash || appConfig.hash,
+ search,
});
} else {
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
pathname: `${basePath}/app/${appName}`,
hash,
+ search,
});
}
diff --git a/vars/tasks.groovy b/vars/tasks.groovy
index 1842e278282b1..5c8f133331e55 100644
--- a/vars/tasks.groovy
+++ b/vars/tasks.groovy
@@ -146,13 +146,14 @@ def functionalXpack(Map params = [:]) {
}
}
- whenChanged([
- 'x-pack/plugins/apm/',
- ]) {
- if (githubPr.isPr()) {
- task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh'))
- }
- }
+ //temporarily disable apm e2e test since it's breaking due to a version upgrade.
+ // whenChanged([
+ // 'x-pack/plugins/apm/',
+ // ]) {
+ // if (githubPr.isPr()) {
+ // task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh'))
+ // }
+ // }
whenChanged([
'x-pack/plugins/uptime/',
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
index af518f0cebc07..2300143925b1e 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
@@ -316,6 +316,57 @@ describe('Jira service', () => {
});
});
+ test('removes newline characters and trialing spaces from summary', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { summary: 'test', description: 'description' } },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } },
+ }));
+
+ await service.createIncident({
+ incident: {
+ summary: 'title \n \n \n howdy \r \r \n \r test',
+ description: 'desc',
+ labels: [],
+ priority: 'High',
+ issueType: null,
+ parent: null,
+ },
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
+ logger,
+ method: 'post',
+ configurationUtilities,
+ data: {
+ fields: {
+ summary: 'title, howdy, test',
+ description: 'desc',
+ project: { key: 'CK' },
+ issuetype: { id: '10006' },
+ labels: [],
+ priority: { name: 'High' },
+ },
+ },
+ });
+ });
+
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
index 063895c7eb5cd..be0240e705a65 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
@@ -6,6 +6,7 @@
*/
import axios from 'axios';
+import { isEmpty } from 'lodash';
import { Logger } from '../../../../../../src/core/server';
import {
@@ -76,7 +77,7 @@ export const createExternalService = (
const createFields = (key: string, incident: Incident): Fields => {
let fields: Fields = {
- summary: incident.summary,
+ summary: trimAndRemoveNewlines(incident.summary),
project: { key },
};
@@ -103,6 +104,13 @@ export const createExternalService = (
return fields;
};
+ const trimAndRemoveNewlines = (str: string) =>
+ str
+ .split(/[\n\r]/gm)
+ .map((item) => item.trim())
+ .filter((item) => !isEmpty(item))
+ .join(', ');
+
const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => {
if (errorResponse == null) {
return '';
diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts
index 76e038fb77e7f..544d6a411ccdc 100644
--- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts
+++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts
@@ -78,7 +78,7 @@ export async function getTotalCount(
}
return {
countTotal:
- Object.keys(aggs).reduce((total: number, key: string) => parseInt(aggs[key], 0) + total, 0) +
+ Object.keys(aggs).reduce((total: number, key: string) => parseInt(aggs[key], 10) + total, 0) +
(preconfiguredActions?.length ?? 0),
countByType,
};
diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts
index e88a2dbd742cb..3494a48fc8ab9 100644
--- a/x-pack/plugins/alerting/common/parse_duration.ts
+++ b/x-pack/plugins/alerting/common/parse_duration.ts
@@ -28,7 +28,7 @@ export function parseDuration(duration: string): number {
}
export function getDurationNumberInItsUnit(duration: string): number {
- return parseInt(duration.replace(/[^0-9.]/g, ''), 0);
+ return parseInt(duration.replace(/[^0-9.]/g, ''), 10);
}
export function getDurationUnitValue(duration: string): string {
diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts
index 46ac3e53895eb..7d8c1593f533d 100644
--- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts
+++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts
@@ -259,7 +259,7 @@ export async function getTotalCountAggregations(
const totalAlertsCount = Object.keys(aggregations.byAlertTypeId.value.types).reduce(
(total: number, key: string) =>
- parseInt(aggregations.byAlertTypeId.value.types[key], 0) + total,
+ parseInt(aggregations.byAlertTypeId.value.types[key], 10) + total,
0
);
@@ -325,7 +325,7 @@ export async function getTotalCountInUse(esClient: ElasticsearchClient, kibanaIn
return {
countTotal: Object.keys(aggregations.byAlertTypeId.value.types).reduce(
(total: number, key: string) =>
- parseInt(aggregations.byAlertTypeId.value.types[key], 0) + total,
+ parseInt(aggregations.byAlertTypeId.value.types[key], 10) + total,
0
),
countByType: Object.keys(aggregations.byAlertTypeId.value.types).reduce(
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
index 170e3a2fdad1e..593de7c3a6f70 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
@@ -11,12 +11,12 @@ import { i18n } from '@kbn/i18n';
import {
createExploratoryViewUrl,
HeaderMenuPortal,
- SeriesUrl,
} from '../../../../../../observability/public';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { AppMountParameters } from '../../../../../../../../src/core/public';
import { InspectorHeaderLink } from '../../../shared/apm_header_action_menu/inspector_header_link';
+import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames';
const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', {
defaultMessage: 'Analyze data',
@@ -39,15 +39,22 @@ export function UXActionMenu({
services: { http },
} = useKibana();
const { urlParams } = useUrlParams();
- const { rangeTo, rangeFrom } = urlParams;
+ const { rangeTo, rangeFrom, serviceName } = urlParams;
const uxExploratoryViewLink = createExploratoryViewUrl(
{
- 'ux-series': {
- dataType: 'ux',
- isNew: true,
- time: { from: rangeFrom, to: rangeTo },
- } as unknown as SeriesUrl,
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ dataType: 'ux',
+ name: `${serviceName}-page-views`,
+ time: { from: rangeFrom!, to: rangeTo! },
+ reportDefinitions: {
+ [SERVICE_NAME]: serviceName ? [serviceName] : [],
+ },
+ selectedMetricField: 'Records',
+ },
+ ],
},
http?.basePath.get()
);
@@ -61,6 +68,7 @@ export function UXActionMenu({
{ANALYZE_MESSAGE}
}>
{
return (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index 5c63cc24b6fdf..a9dac0a37c353 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -87,15 +87,18 @@ export function PageLoadDistribution() {
const exploratoryViewLink = createExploratoryViewUrl(
{
- [`${serviceName}-page-views`]: {
- dataType: 'ux',
- reportType: 'data-distribution',
- time: { from: rangeFrom!, to: rangeTo! },
- reportDefinitions: {
- 'service.name': serviceName as string[],
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${serviceName}-page-views`,
+ dataType: 'ux',
+ time: { from: rangeFrom!, to: rangeTo! },
+ reportDefinitions: {
+ 'service.name': serviceName as string[],
+ },
+ ...(breakdown ? { breakdown: breakdown.fieldName } : {}),
},
- ...(breakdown ? { breakdown: breakdown.fieldName } : {}),
- },
+ ],
},
http?.basePath.get()
);
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
index 667d0b5e4b4db..5f7c0738e5642 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -62,15 +62,18 @@ export function PageViewsTrend() {
const exploratoryViewLink = createExploratoryViewUrl(
{
- [`${serviceName}-page-views`]: {
- dataType: 'ux',
- reportType: 'kpi-over-time',
- time: { from: rangeFrom!, to: rangeTo! },
- reportDefinitions: {
- 'service.name': serviceName as string[],
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${serviceName}-page-views`,
+ dataType: 'ux',
+ time: { from: rangeFrom!, to: rangeTo! },
+ reportDefinitions: {
+ 'service.name': serviceName as string[],
+ },
+ ...(breakdown ? { breakdown: breakdown.fieldName } : {}),
},
- ...(breakdown ? { breakdown: breakdown.fieldName } : {}),
- },
+ ],
},
http?.basePath.get()
);
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx
index c5828dea2c920..359dcdfda0a14 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx
@@ -15,7 +15,7 @@ import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_det
import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers';
import { Environment } from '../../../../../common/environment_rt';
-export const MaybeViewTraceLink = ({
+export function MaybeViewTraceLink({
transaction,
waterfall,
environment,
@@ -23,7 +23,7 @@ export const MaybeViewTraceLink = ({
transaction: ITransaction;
waterfall: IWaterfall;
environment: Environment;
-}) => {
+}) {
const {
urlParams: { latencyAggregationType },
} = useUrlParams();
@@ -102,4 +102,4 @@ export const MaybeViewTraceLink = ({
);
}
-};
+}
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx
index 08cebbe1880e8..f499cf88ecdb5 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx
@@ -38,7 +38,7 @@ describe('AnalyzeDataButton', () => {
render(
);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => {
render(
);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => {
render(
);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => {
render(
);
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
index 068d7bb1c242f..a4fc964a444c9 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
@@ -9,10 +9,7 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
-import {
- createExploratoryViewUrl,
- SeriesUrl,
-} from '../../../../../../observability/public';
+import { createExploratoryViewUrl } from '../../../../../../observability/public';
import { ALL_VALUES_SELECTED } from '../../../../../../observability/public';
import {
isIosAgentName,
@@ -21,6 +18,7 @@ import {
import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
+ TRANSACTION_DURATION,
} from '../../../../../common/elasticsearch_fieldnames';
import {
ENVIRONMENT_ALL,
@@ -29,13 +27,11 @@ import {
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
-function getEnvironmentDefinition(environment?: string) {
+function getEnvironmentDefinition(environment: string) {
switch (environment) {
case ENVIRONMENT_ALL.value:
return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] };
case ENVIRONMENT_NOT_DEFINED.value:
- case undefined:
- return {};
default:
return { [SERVICE_ENVIRONMENT]: [environment] };
}
@@ -54,21 +50,26 @@ export function AnalyzeDataButton() {
if (
(isRumAgentName(agentName) || isIosAgentName(agentName)) &&
- canShowDashboard
+ rangeFrom &&
+ canShowDashboard &&
+ rangeTo
) {
const href = createExploratoryViewUrl(
{
- 'apm-series': {
- dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
- time: { from: rangeFrom, to: rangeTo },
- reportType: 'kpi-over-time',
- reportDefinitions: {
- [SERVICE_NAME]: [serviceName],
- ...getEnvironmentDefinition(environment),
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${serviceName}-response-latency`,
+ selectedMetricField: TRANSACTION_DURATION,
+ dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
+ time: { from: rangeFrom, to: rangeTo },
+ reportDefinitions: {
+ [SERVICE_NAME]: [serviceName],
+ ...(environment ? getEnvironmentDefinition(environment) : {}),
+ },
+ operationType: 'average',
},
- operationType: 'average',
- isNew: true,
- } as SeriesUrl,
+ ],
},
basepath
);
diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts
new file mode 100644
index 0000000000000..d706146faf212
--- /dev/null
+++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { GetDeprecationsContext } from '../../../../../src/core/server';
+import { CloudSetup } from '../../../cloud/server';
+import { getDeprecations } from './';
+import { APMRouteHandlerResources } from '../';
+import { AgentPolicy } from '../../../fleet/common';
+
+const deprecationContext = {
+ esClient: {},
+ savedObjectsClient: {},
+} as GetDeprecationsContext;
+
+describe('getDeprecations', () => {
+ describe('when fleet is disabled', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({});
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+
+ describe('when running on cloud with legacy apm-server', () => {
+ it('returns deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({
+ agentPolicyService: { get: () => undefined },
+ }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).not.toEqual([]);
+ });
+ });
+
+ describe('when running on cloud with fleet', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({
+ agentPolicyService: { get: () => ({ id: 'foo' } as AgentPolicy) },
+ }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+
+ describe('when running on prem', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: false } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({ agentPolicyService: { get: () => undefined } }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts
new file mode 100644
index 0000000000000..b592a2bf13268
--- /dev/null
+++ b/x-pack/plugins/apm/server/deprecations/index.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { GetDeprecationsContext, DeprecationsDetails } from 'src/core/server';
+import { i18n } from '@kbn/i18n';
+import { isEmpty } from 'lodash';
+import { CloudSetup } from '../../../cloud/server';
+import { getCloudAgentPolicy } from '../lib/fleet/get_cloud_apm_package_policy';
+import { APMRouteHandlerResources } from '../';
+
+export function getDeprecations({
+ cloudSetup,
+ fleet,
+}: {
+ cloudSetup?: CloudSetup;
+ fleet?: APMRouteHandlerResources['plugins']['fleet'];
+}) {
+ return async ({
+ savedObjectsClient,
+ }: GetDeprecationsContext): Promise
=> {
+ const deprecations: DeprecationsDetails[] = [];
+ if (!fleet) {
+ return deprecations;
+ }
+
+ const fleetPluginStart = await fleet.start();
+ const cloudAgentPolicy = await getCloudAgentPolicy({
+ fleetPluginStart,
+ savedObjectsClient,
+ });
+
+ const isCloudEnabled = !!cloudSetup?.isCloudEnabled;
+
+ const hasCloudAgentPolicy = !isEmpty(cloudAgentPolicy);
+
+ if (isCloudEnabled && !hasCloudAgentPolicy) {
+ deprecations.push({
+ title: i18n.translate('xpack.apm.deprecations.legacyModeTitle', {
+ defaultMessage: 'APM Server running in legacy mode',
+ }),
+ message: i18n.translate('xpack.apm.deprecations.message', {
+ defaultMessage:
+ 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.',
+ }),
+ documentationUrl:
+ 'https://www.elastic.co/guide/en/apm/server/current/apm-integration.html',
+ level: 'warning',
+ correctiveActions: {
+ manualSteps: [
+ i18n.translate('xpack.apm.deprecations.steps.apm', {
+ defaultMessage: 'Navigate to Observability/APM',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.settings', {
+ defaultMessage: 'Click on "Settings"',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.schema', {
+ defaultMessage: 'Select "Schema" tab',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.switch', {
+ defaultMessage:
+ 'Click "Switch to data streams". You will be guided through the process',
+ }),
+ ],
+ },
+ });
+ }
+
+ return deprecations;
+ };
+}
diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts
deleted file mode 100644
index 89d9147610d69..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts
+++ /dev/null
@@ -1,157 +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 { isEmpty, omit } from 'lodash';
-import { EventOutcome } from '../../../../common/event_outcome';
-import {
- processSignificantTermAggs,
- TopSigTerm,
-} from '../process_significant_term_aggs';
-import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch';
-import { ESFilter } from '../../../../../../../src/core/types/elasticsearch';
-import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { Setup } from '../../helpers/setup_request';
-import { getBucketSize } from '../../helpers/get_bucket_size';
-import {
- getTimeseriesAggregation,
- getFailedTransactionRateTimeSeries,
-} from '../../helpers/transaction_error_rate';
-import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
-
-interface Options extends CorrelationsOptions {
- fieldNames: string[];
- setup: Setup;
-}
-export async function getCorrelationsForFailedTransactions(options: Options) {
- const { fieldNames, setup, start, end } = options;
- const { apmEventClient } = setup;
- const filters = getCorrelationsFilters(options);
-
- const params = {
- apm: { events: [ProcessorEvent.transaction] },
- track_total_hits: true,
- body: {
- size: 0,
- query: {
- bool: { filter: filters },
- },
- aggs: {
- failed_transactions: {
- filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
-
- // significant term aggs
- aggs: fieldNames.reduce((acc, fieldName) => {
- return {
- ...acc,
- [fieldName]: {
- significant_terms: {
- size: 10,
- field: fieldName,
- background_filter: {
- bool: {
- filter: filters,
- must_not: {
- term: { [EVENT_OUTCOME]: EventOutcome.failure },
- },
- },
- },
- },
- },
- };
- }, {} as Record),
- },
- },
- },
- };
-
- const response = await apmEventClient.search(
- 'get_correlations_for_failed_transactions',
- params
- );
- if (!response.aggregations) {
- return { significantTerms: [] };
- }
-
- const sigTermAggs = omit(
- response.aggregations?.failed_transactions,
- 'doc_count'
- );
-
- const topSigTerms = processSignificantTermAggs({ sigTermAggs });
- return getErrorRateTimeSeries({ setup, filters, topSigTerms, start, end });
-}
-
-export async function getErrorRateTimeSeries({
- setup,
- filters,
- topSigTerms,
- start,
- end,
-}: {
- setup: Setup;
- filters: ESFilter[];
- topSigTerms: TopSigTerm[];
- start: number;
- end: number;
-}) {
- const { apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
-
- if (isEmpty(topSigTerms)) {
- return { significantTerms: [] };
- }
-
- const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString);
-
- const perTermAggs = topSigTerms.reduce(
- (acc, term, index) => {
- acc[`term_${index}`] = {
- filter: { term: { [term.fieldName]: term.fieldValue } },
- aggs: { timeseries: timeseriesAgg },
- };
- return acc;
- },
- {} as {
- [key: string]: {
- filter: AggregationOptionsByType['filter'];
- aggs: { timeseries: typeof timeseriesAgg };
- };
- }
- );
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: perTermAggs,
- },
- };
-
- const response = await apmEventClient.search(
- 'get_error_rate_timeseries',
- params
- );
- const { aggregations } = response;
-
- if (!aggregations) {
- return { significantTerms: [] };
- }
-
- return {
- significantTerms: topSigTerms.map((topSig, index) => {
- const agg = aggregations[`term_${index}`]!;
-
- return {
- ...topSig,
- timeseries: getFailedTransactionRateTimeSeries(agg.timeseries.buckets),
- };
- }),
- };
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts
deleted file mode 100644
index 14399a935aa52..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.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 { ProcessorEvent } from '../../../../common/processor_event';
-import { getBucketSize } from '../../helpers/get_bucket_size';
-import {
- getTimeseriesAggregation,
- getFailedTransactionRateTimeSeries,
-} from '../../helpers/transaction_error_rate';
-import { Setup } from '../../helpers/setup_request';
-import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
-
-interface Options extends CorrelationsOptions {
- setup: Setup;
-}
-
-export async function getOverallErrorTimeseries(options: Options) {
- const { setup, start, end } = options;
- const filters = getCorrelationsFilters(options);
- const { apmEventClient } = setup;
- const { intervalString } = getBucketSize({ start, end, numBuckets: 15 });
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: {
- timeseries: getTimeseriesAggregation(start, end, intervalString),
- },
- },
- };
-
- const response = await apmEventClient.search(
- 'get_error_rate_timeseries',
- params
- );
- const { aggregations } = response;
-
- if (!aggregations) {
- return { overall: null };
- }
-
- return {
- overall: {
- timeseries: getFailedTransactionRateTimeSeries(
- aggregations.timeseries.buckets
- ),
- },
- };
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts
deleted file mode 100644
index 77c6fb5b1c1c6..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts
+++ /dev/null
@@ -1,121 +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 { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch';
-import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { getDurationForPercentile } from './get_duration_for_percentile';
-import { processSignificantTermAggs } from '../process_significant_term_aggs';
-import { getLatencyDistribution } from './get_latency_distribution';
-import { withApmSpan } from '../../../utils/with_apm_span';
-import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
-import { Setup } from '../../helpers/setup_request';
-
-interface Options extends CorrelationsOptions {
- durationPercentile: number;
- fieldNames: string[];
- maxLatency: number;
- distributionInterval: number;
- setup: Setup;
-}
-export async function getCorrelationsForSlowTransactions(options: Options) {
- return withApmSpan('get_correlations_for_slow_transactions', async () => {
- const {
- durationPercentile,
- fieldNames,
- setup,
- maxLatency,
- distributionInterval,
- } = options;
- const { apmEventClient } = setup;
- const filters = getCorrelationsFilters(options);
- const durationForPercentile = await getDurationForPercentile({
- durationPercentile,
- filters,
- setup,
- });
-
- if (!durationForPercentile) {
- return { significantTerms: [] };
- }
-
- const params = {
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: {
- bool: {
- // foreground filters
- filter: filters,
- must: {
- function_score: {
- query: {
- range: {
- [TRANSACTION_DURATION]: { gte: durationForPercentile },
- },
- },
- script_score: {
- script: {
- source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`,
- },
- },
- },
- },
- },
- },
- aggs: fieldNames.reduce((acc, fieldName) => {
- return {
- ...acc,
- [fieldName]: {
- significant_terms: {
- size: 10,
- field: fieldName,
- background_filter: {
- bool: {
- filter: [
- ...filters,
- {
- range: {
- [TRANSACTION_DURATION]: {
- lt: durationForPercentile,
- },
- },
- },
- ],
- },
- },
- },
- },
- };
- }, {} as Record),
- },
- };
-
- const response = await apmEventClient.search(
- 'get_significant_terms',
- params
- );
-
- if (!response.aggregations) {
- return { significantTerms: [] };
- }
-
- const topSigTerms = processSignificantTermAggs({
- sigTermAggs: response.aggregations,
- });
-
- const significantTerms = await getLatencyDistribution({
- setup,
- filters,
- topSigTerms,
- maxLatency,
- distributionInterval,
- });
-
- return { significantTerms };
- });
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts
deleted file mode 100644
index e7346d15f5aae..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.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 { ESFilter } from '../../../../../../../src/core/types/elasticsearch';
-import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { Setup } from '../../helpers/setup_request';
-
-export async function getDurationForPercentile({
- durationPercentile,
- filters,
- setup,
-}: {
- durationPercentile: number;
- filters: ESFilter[];
- setup: Setup;
-}) {
- const { apmEventClient } = setup;
- const res = await apmEventClient.search('get_duration_for_percentiles', {
- apm: {
- events: [ProcessorEvent.transaction],
- },
- body: {
- size: 0,
- query: {
- bool: { filter: filters },
- },
- aggs: {
- percentile: {
- percentiles: {
- field: TRANSACTION_DURATION,
- percents: [durationPercentile],
- },
- },
- },
- },
- });
-
- const duration = Object.values(res.aggregations?.percentile.values || {})[0];
- return duration || 0;
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts
deleted file mode 100644
index 14ba7ecd4a0b9..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch';
-import { ESFilter } from '../../../../../../../src/core/types/elasticsearch';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { Setup } from '../../helpers/setup_request';
-import { TopSigTerm } from '../process_significant_term_aggs';
-
-import {
- getDistributionAggregation,
- trimBuckets,
-} from './get_overall_latency_distribution';
-
-export async function getLatencyDistribution({
- setup,
- filters,
- topSigTerms,
- maxLatency,
- distributionInterval,
-}: {
- setup: Setup;
- filters: ESFilter[];
- topSigTerms: TopSigTerm[];
- maxLatency: number;
- distributionInterval: number;
-}) {
- const { apmEventClient } = setup;
-
- const distributionAgg = getDistributionAggregation(
- maxLatency,
- distributionInterval
- );
-
- const perTermAggs = topSigTerms.reduce(
- (acc, term, index) => {
- acc[`term_${index}`] = {
- filter: { term: { [term.fieldName]: term.fieldValue } },
- aggs: {
- distribution: distributionAgg,
- },
- };
- return acc;
- },
- {} as Record<
- string,
- {
- filter: AggregationOptionsByType['filter'];
- aggs: {
- distribution: typeof distributionAgg;
- };
- }
- >
- );
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: perTermAggs,
- },
- };
-
- const response = await apmEventClient.search(
- 'get_latency_distribution',
- params
- );
-
- type Agg = NonNullable;
-
- if (!response.aggregations) {
- return [];
- }
-
- return topSigTerms.map((topSig, index) => {
- // ignore the typescript error since existence of response.aggregations is already checked:
- // @ts-expect-error
- const agg = response.aggregations[`term_${index}`] as Agg[string];
- const total = agg.distribution.doc_count;
- const buckets = trimBuckets(
- agg.distribution.dist_filtered_by_latency.buckets
- );
-
- return {
- ...topSig,
- distribution: buckets.map((bucket) => ({
- x: bucket.key,
- y: (bucket.doc_count / total) * 100,
- })),
- };
- });
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts
deleted file mode 100644
index 8838b5ff7a862..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.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 { ESFilter } from '../../../../../../../src/core/types/elasticsearch';
-import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { Setup } from '../../helpers/setup_request';
-import { TopSigTerm } from '../process_significant_term_aggs';
-
-export async function getMaxLatency({
- setup,
- filters,
- topSigTerms = [],
-}: {
- setup: Setup;
- filters: ESFilter[];
- topSigTerms?: TopSigTerm[];
-}) {
- const { apmEventClient } = setup;
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: {
- bool: {
- filter: filters,
-
- ...(topSigTerms.length
- ? {
- // only include docs containing the significant terms
- should: topSigTerms.map((term) => ({
- term: { [term.fieldName]: term.fieldValue },
- })),
- minimum_should_match: 1,
- }
- : null),
- },
- },
- aggs: {
- // TODO: add support for metrics
- // max_latency: { max: { field: TRANSACTION_DURATION } },
- max_latency: {
- percentiles: { field: TRANSACTION_DURATION, percents: [99] },
- },
- },
- },
- };
-
- const response = await apmEventClient.search('get_max_latency', params);
- // return response.aggregations?.max_latency.value;
- return Object.values(response.aggregations?.max_latency.values ?? {})[0];
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts
deleted file mode 100644
index cab1496849d8c..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts
+++ /dev/null
@@ -1,110 +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 { dropRightWhile } from 'lodash';
-import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../../common/processor_event';
-import { getMaxLatency } from './get_max_latency';
-import { withApmSpan } from '../../../utils/with_apm_span';
-import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters';
-import { Setup } from '../../helpers/setup_request';
-
-export const INTERVAL_BUCKETS = 15;
-interface Options extends CorrelationsOptions {
- setup: Setup;
-}
-
-export function getDistributionAggregation(
- maxLatency: number,
- distributionInterval: number
-) {
- return {
- filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } },
- aggs: {
- dist_filtered_by_latency: {
- histogram: {
- // TODO: add support for metrics
- field: TRANSACTION_DURATION,
- interval: distributionInterval,
- min_doc_count: 0,
- extended_bounds: {
- min: 0,
- max: maxLatency,
- },
- },
- },
- },
- };
-}
-
-export async function getOverallLatencyDistribution(options: Options) {
- const { setup } = options;
- const filters = getCorrelationsFilters(options);
-
- return withApmSpan('get_overall_latency_distribution', async () => {
- const { apmEventClient } = setup;
- const maxLatency = await getMaxLatency({ setup, filters });
- if (!maxLatency) {
- return {
- maxLatency: null,
- distributionInterval: null,
- overallDistribution: null,
- };
- }
- const distributionInterval = Math.floor(maxLatency / INTERVAL_BUCKETS);
-
- const params = {
- // TODO: add support for metrics
- apm: { events: [ProcessorEvent.transaction] },
- body: {
- size: 0,
- query: { bool: { filter: filters } },
- aggs: {
- // overall distribution agg
- distribution: getDistributionAggregation(
- maxLatency,
- distributionInterval
- ),
- },
- },
- };
-
- const response = await apmEventClient.search(
- 'get_terms_distribution',
- params
- );
-
- if (!response.aggregations) {
- return {
- maxLatency,
- distributionInterval,
- overallDistribution: null,
- };
- }
-
- const { distribution } = response.aggregations;
- const total = distribution.doc_count;
- const buckets = trimBuckets(distribution.dist_filtered_by_latency.buckets);
-
- return {
- maxLatency,
- distributionInterval,
- overallDistribution: buckets.map((bucket) => ({
- x: bucket.key,
- y: (bucket.doc_count / total) * 100,
- })),
- };
- });
-}
-
-// remove trailing buckets that are empty and out of bounds of the desired number of buckets
-export function trimBuckets(buckets: T[]) {
- return dropRightWhile(
- buckets,
- (bucket, index) => bucket.doc_count === 0 && index > INTERVAL_BUCKETS - 1
- );
-}
diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts
deleted file mode 100644
index ecb751cad5a3f..0000000000000
--- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts
+++ /dev/null
@@ -1,80 +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 { orderBy } from 'lodash';
-import {
- AggregationOptionsByType,
- AggregationResultOf,
-} from '../../../../../../src/core/types/elasticsearch';
-
-export interface TopSigTerm {
- fieldName: string;
- fieldValue: string | number;
- score: number;
- impact: number;
- fieldCount: number;
- valueCount: number;
-}
-
-type SigTermAgg = AggregationResultOf<
- { significant_terms: AggregationOptionsByType['significant_terms'] },
- {}
->;
-
-function getMaxImpactScore(scores: number[]) {
- if (scores.length === 0) {
- return 0;
- }
-
- const sortedScores = scores.sort((a, b) => b - a);
- const maxScore = sortedScores[0];
-
- // calculate median
- const halfSize = scores.length / 2;
- const medianIndex = Math.floor(halfSize);
- const medianScore =
- medianIndex < halfSize
- ? sortedScores[medianIndex]
- : (sortedScores[medianIndex - 1] + sortedScores[medianIndex]) / 2;
-
- return Math.max(maxScore, medianScore * 2);
-}
-
-export function processSignificantTermAggs({
- sigTermAggs,
-}: {
- sigTermAggs: Record;
-}) {
- const significantTerms = Object.entries(sigTermAggs)
- // filter entries with buckets, i.e. Significant terms aggs
- .filter((entry): entry is [string, SigTermAgg] => {
- const [, agg] = entry;
- return 'buckets' in agg;
- })
- .flatMap(([fieldName, agg]) => {
- return agg.buckets.map((bucket) => ({
- fieldName,
- fieldValue: bucket.key,
- fieldCount: agg.doc_count,
- valueCount: bucket.doc_count,
- score: bucket.score,
- }));
- });
-
- const maxImpactScore = getMaxImpactScore(
- significantTerms.map(({ score }) => score)
- );
-
- // get top 10 terms ordered by score
- const topSigTerms = orderBy(significantTerms, 'score', 'desc')
- .map((significantTerm) => ({
- ...significantTerm,
- impact: significantTerm.score / maxImpactScore,
- }))
- .slice(0, 10);
- return topSigTerms;
-}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
index ebb5c7655806a..9409e94fa9ba9 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import moment from 'moment';
import { SetupUX } from '../../routes/rum_client';
import {
SERVICE_NAME,
@@ -16,8 +17,8 @@ import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
export async function hasRumData({
setup,
- start,
- end,
+ start = moment().subtract(24, 'h').valueOf(),
+ end = moment().valueOf(),
}: {
setup: SetupUX;
start?: number;
diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts
similarity index 78%
rename from x-pack/plugins/apm/server/lib/correlations/get_filters.ts
rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts
index e735c79aa0cde..8537a367b99eb 100644
--- a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts
@@ -5,16 +5,16 @@
* 2.0.
*/
-import { ESFilter } from '../../../../../../src/core/types/elasticsearch';
-import { rangeQuery, kqlQuery } from '../../../../observability/server';
-import { environmentQuery } from '../../../common/utils/environment_query';
+import { ESFilter } from '../../../../../../../src/core/types/elasticsearch';
+import { rangeQuery, kqlQuery } from '../../../../../observability/server';
+import { environmentQuery } from '../../../../common/utils/environment_query';
import {
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
PROCESSOR_EVENT,
-} from '../../../common/elasticsearch_fieldnames';
-import { ProcessorEvent } from '../../../common/processor_event';
+} from '../../../../common/elasticsearch_fieldnames';
+import { ProcessorEvent } from '../../../../common/processor_event';
export interface CorrelationsOptions {
environment: string;
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
index d544bbc9e00f5..f00c89503f103 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts
@@ -15,7 +15,7 @@ import type {
SearchStrategyParams,
} from '../../../../common/search_strategies/types';
import { rangeRt } from '../../../routes/default_api_types';
-import { getCorrelationsFilters } from '../../correlations/get_filters';
+import { getCorrelationsFilters } from './get_filters';
export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => {
return { term: { [fieldName]: fieldValue } };
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index 56185d846562f..2296227de2a33 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -51,6 +51,7 @@ import {
TRANSACTION_TYPE,
} from '../common/elasticsearch_fieldnames';
import { tutorialProvider } from './tutorial';
+import { getDeprecations } from './deprecations';
export class APMPlugin
implements
@@ -222,6 +223,12 @@ export class APMPlugin
);
})();
});
+ core.deprecations.registerDeprecations({
+ getDeprecations: getDeprecations({
+ cloudSetup: plugins.cloud,
+ fleet: resourcePlugins.fleet,
+ }),
+ });
return {
config$: mergedConfig$,
diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
index a7efaa3b00f34..c62e42f222194 100644
--- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
+++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
@@ -57,10 +57,10 @@ function getApmServerInstructionSet(
id: INSTRUCTION_VARIANT.ESC,
instructions: [
{
- title: 'Enable the APM Server in the ESS console',
+ title: 'Enable the APM Server in the Elastic Cloud user console',
textPre: i18n.translate('xpack.apm.tutorial.elasticCloud.textPre', {
defaultMessage:
- 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments/{deploymentId}/edit) and enable APM in the deployment settings. Once enabled, refresh this page.',
+ 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments/{deploymentId}/edit) and enable APM and Fleet in the deployment edit page by clicking on add capacity, and then click on save. Once enabled, refresh this page.',
values: { deploymentId },
}),
},
diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts
index 4c99cce241170..66e6ffaed95a8 100644
--- a/x-pack/plugins/apm/server/tutorial/index.ts
+++ b/x-pack/plugins/apm/server/tutorial/index.ts
@@ -103,6 +103,8 @@ It allows you to monitor the performance of thousands of applications in real ti
}
),
euiIconType: 'apmApp',
+ eprPackageOverlap: 'apm',
+ integrationBrowserCategories: ['web'],
artifacts,
customStatusCheckName: 'apm_fleet_server_status_check',
onPrem: onPremInstructions({ apmConfig, isFleetPluginEnabled }),
diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js b/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
index b4d33627d734a..dcee475557d98 100644
--- a/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
+++ b/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js
@@ -580,15 +580,6 @@ export const applyLocalTransforms = (shapes, transformIntents) => {
return shapes.map(shapeApplyLocalTransforms(transformIntents));
};
-// eslint-disable-next-line
-const getUpstreamTransforms = (shapes, shape) =>
- shape.parent
- ? getUpstreamTransforms(
- shapes,
- shapes.find((s) => s.id === shape.parent)
- ).concat([shape.localTransformMatrix])
- : [shape.localTransformMatrix];
-
const getUpstreams = (shapes, shape) =>
shape.parent
? getUpstreams(
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
index 5c63e79374c73..5707f9fa5fdcd 100644
--- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import sinon from 'sinon';
+import { fakeServer } from 'sinon';
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server) => {
@@ -124,7 +124,7 @@ const registerHttpRequestMockHelpers = (server) => {
};
export const init = () => {
- const server = sinon.fakeServer.create();
+ const server = fakeServer.create();
server.respondImmediately = true;
// We make requests to APIs which don't impact the UX, e.g. UI metric telemetry,
diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts
index 55ebdf9a196d6..f7bea807c3e61 100644
--- a/x-pack/plugins/data_visualizer/common/constants.ts
+++ b/x-pack/plugins/data_visualizer/common/constants.ts
@@ -33,19 +33,6 @@ export const JOB_FIELD_TYPES = {
UNKNOWN: 'unknown',
} as const;
-export const JOB_FIELD_TYPES_OPTIONS = {
- [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' },
- [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' },
- [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' },
- [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' },
- [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' },
- [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' },
- [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' },
- [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' },
- [JOB_FIELD_TYPES.HISTOGRAM]: { name: 'Histogram', icon: 'tokenNumber' },
- [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' },
-};
-
export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
export const NON_AGGREGATABLE_FIELD_TYPES = new Set([
diff --git a/x-pack/plugins/data_visualizer/common/types/index.ts b/x-pack/plugins/data_visualizer/common/types/index.ts
index 8b51142e19129..1153b45e1cce2 100644
--- a/x-pack/plugins/data_visualizer/common/types/index.ts
+++ b/x-pack/plugins/data_visualizer/common/types/index.ts
@@ -6,6 +6,7 @@
*/
import type { SimpleSavedObject } from 'kibana/public';
+import { isPopulatedObject } from '../utils/object_utils';
export type { JobFieldType } from './job_field_type';
export type {
FieldRequestConfig,
@@ -27,3 +28,7 @@ export interface DataVisualizerTableState {
}
export type SavedSearchSavedObject = SimpleSavedObject;
+
+export function isSavedSearchSavedObject(arg: unknown): arg is SavedSearchSavedObject {
+ return isPopulatedObject(arg, ['id', 'type', 'attributes']);
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/_embedded_map.scss b/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/_embedded_map.scss
index 99ee60f62bb21..a3682bfd7d96c 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/_embedded_map.scss
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/_embedded_map.scss
@@ -1,4 +1,4 @@
-.embeddedMapContent {
+.embeddedMap__content {
width: 100%;
height: 100%;
display: flex;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/embedded_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/embedded_map.tsx
index 13aab06640bd5..cf357a462d9b3 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/embedded_map.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/embedded_map/embedded_map.tsx
@@ -39,7 +39,7 @@ export function EmbeddedMapComponent({
const baseLayers = useRef();
const {
- services: { embeddable: embeddablePlugin, maps: mapsPlugin },
+ services: { embeddable: embeddablePlugin, maps: mapsPlugin, data },
} = useDataVisualizerKibana();
const factory:
@@ -73,7 +73,7 @@ export function EmbeddedMapComponent({
const input: MapEmbeddableInput = {
id: htmlIdGenerator()(),
attributes: { title: '' },
- filters: [],
+ filters: data.query.filterManager.getFilters() ?? [],
hidePanelTitles: true,
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
@@ -143,7 +143,7 @@ export function EmbeddedMapComponent({
return (
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/examples_list/examples_list.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/examples_list/examples_list.tsx
index 296820479437c..1aa14a88a5248 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/examples_list/examples_list.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/examples_list/examples_list.tsx
@@ -11,6 +11,7 @@ import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
+import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel';
interface Props {
examples: Array;
}
@@ -31,8 +32,7 @@ export const ExamplesList: FC = ({ examples }) => {
examplesContent = examples.map((example, i) => {
return (
@@ -41,7 +41,10 @@ export const ExamplesList: FC = ({ examples }) => {
}
return (
-
+
= ({ examples }) => {
{examplesContent}
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
index 4517d62ec2aa1..8a9f9a25c16fa 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
@@ -52,10 +52,7 @@ export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFi
}
return (
-
+
{getCardContent()}
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content/geo_point_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content/geo_point_content.tsx
index b732e542658b5..542500df71a6f 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content/geo_point_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content/geo_point_content.tsx
@@ -6,8 +6,6 @@
*/
import React, { FC, useMemo } from 'react';
-
-import { EuiFlexItem } from '@elastic/eui';
import { Feature, Point } from 'geojson';
import type { FieldDataRowProps } from '../../stats_table/types/field_data_row';
import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats';
@@ -15,6 +13,7 @@ import { EmbeddedMapComponent } from '../../embedded_map';
import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { ExamplesList } from '../../examples_list';
+import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
export const DEFAULT_GEO_REGEX = RegExp('(?
.+) (?.+)');
@@ -63,17 +62,12 @@ export const GeoPointContent: FC = ({ config }) => {
{formattedResults && Array.isArray(formattedResults.examples) && (
-
-
-
+
)}
{formattedResults && Array.isArray(formattedResults.layerList) && (
-
+
-
+
)}
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx
index 082083fa92ff2..5da44262d29da 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/geo_point_content_with_map/geo_point_content_with_map.tsx
@@ -6,7 +6,6 @@
*/
import React, { FC, useEffect, useState } from 'react';
-import { EuiFlexItem } from '@elastic/eui';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
@@ -17,6 +16,7 @@ import { useDataVisualizerKibana } from '../../../../kibana_context';
import { JOB_FIELD_TYPES } from '../../../../../../common';
import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '../../../../../../../maps/common';
import { EmbeddedMapComponent } from '../../embedded_map';
+import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
export const GeoPointContentWithMap: FC<{
config: FieldVisConfig;
@@ -26,7 +26,7 @@ export const GeoPointContentWithMap: FC<{
const { stats } = config;
const [layerList, setLayerList] = useState([]);
const {
- services: { maps: mapsPlugin },
+ services: { maps: mapsPlugin, data },
} = useDataVisualizerKibana();
// Update the layer list with updated geo points upon refresh
@@ -42,6 +42,7 @@ export const GeoPointContentWithMap: FC<{
indexPatternId: indexPattern.id,
geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE,
+ filters: data.query.filterManager.getFilters() ?? [],
query: {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
@@ -57,19 +58,16 @@ export const GeoPointContentWithMap: FC<{
}
updateIndexPatternSearchLayer();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [indexPattern, combinedQuery, config, mapsPlugin]);
+ }, [indexPattern, combinedQuery, config, mapsPlugin, data.query]);
if (stats?.examples === undefined) return null;
return (
-
-
-
-
-
+
+
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
index ca9c8301bcfba..79af35f1c8005 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
@@ -22,15 +22,21 @@ import { FieldVisConfig } from '../stats_table/types';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
import { LoadingIndicator } from '../loading_indicator';
+import { IndexPatternField } from '../../../../../../../../src/plugins/data/common';
export const IndexBasedDataVisualizerExpandedRow = ({
item,
indexPattern,
combinedQuery,
+ onAddFilter,
}: {
item: FieldVisConfig;
indexPattern: IndexPattern | undefined;
combinedQuery: CombinedQuery;
+ /**
+ * Callback to add a filter to filter bar
+ */
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}) => {
const config = item;
const { loading, type, existsInDocs, fieldName } = config;
@@ -42,7 +48,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
switch (type) {
case JOB_FIELD_TYPES.NUMBER:
- return ;
+ return ;
case JOB_FIELD_TYPES.BOOLEAN:
return ;
@@ -61,10 +67,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({
);
case JOB_FIELD_TYPES.IP:
- return ;
+ return ;
case JOB_FIELD_TYPES.KEYWORD:
- return ;
+ return ;
case JOB_FIELD_TYPES.TEXT:
return ;
@@ -75,10 +81,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
}
return (
-
+
{loading === true ? : getCardContent()}
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_count_panel/field_count_panel.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_count_panel/field_count_panel.tsx
index c79ed4ade7092..ce98ecd2fa5c7 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_count_panel/field_count_panel.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_count_panel/field_count_panel.tsx
@@ -28,12 +28,13 @@ export const FieldCountPanel: FC
= ({
-
+
,
combinedQuery: CombinedQuery,
actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined>
): Array> {
- const { lens: lensPlugin, indexPatternFieldEditor } = services;
+ const { lens: lensPlugin, data } = services;
const actions: Array> = [];
+ const filters = data?.query.filterManager.getFilters() ?? [];
const refreshPage = () => {
const refresh: Refresh = {
@@ -49,7 +50,7 @@ export function getActions(
available: (item: FieldVisConfig) =>
getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor,
onClick: (item: FieldVisConfig) => {
- const lensAttributes = getLensAttributes(indexPattern, combinedQuery, item);
+ const lensAttributes = getLensAttributes(indexPattern, combinedQuery, filters, item);
if (lensAttributes) {
lensPlugin.navigateToPrefilledEditor({
id: `dataVisualizer-${item.fieldName}`,
@@ -62,7 +63,7 @@ export function getActions(
}
// Allow to edit index pattern field
- if (indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
+ if (services.indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
actions.push({
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', {
defaultMessage: 'Edit index pattern field',
@@ -76,7 +77,7 @@ export function getActions(
type: 'icon',
icon: 'indexEdit',
onClick: (item: FieldVisConfig) => {
- actionFlyoutRef.current = indexPatternFieldEditor?.openEditor({
+ actionFlyoutRef.current = services.indexPatternFieldEditor?.openEditor({
ctx: { indexPattern },
fieldName: item.fieldName,
onSave: refreshPage,
@@ -100,7 +101,7 @@ export function getActions(
return item.deletable === true;
},
onClick: (item: FieldVisConfig) => {
- actionFlyoutRef.current = indexPatternFieldEditor?.openDeleteModal({
+ actionFlyoutRef.current = services.indexPatternFieldEditor?.openDeleteModal({
ctx: { indexPattern },
fieldName: item.fieldName!,
onDelete: refreshPage,
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
index 3f80bbefcc259..615ba84afb5b7 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/lens_utils.ts
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import type { Filter } from '@kbn/es-query';
import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import type {
@@ -15,6 +16,7 @@ import type {
} from '../../../../../../../lens/public';
import { FieldVisConfig } from '../../stats_table/types';
import { JOB_FIELD_TYPES } from '../../../../../../common';
+
interface ColumnsAndLayer {
columns: Record;
layer: XYLayerConfig;
@@ -241,6 +243,7 @@ function getColumnsAndLayer(
export function getLensAttributes(
defaultIndexPattern: IndexPattern | undefined,
combinedQuery: CombinedQuery,
+ filters: Filter[],
item: FieldVisConfig
): TypedLensByValueInput['attributes'] | undefined {
if (defaultIndexPattern === undefined || item.type === undefined || item.fieldName === undefined)
@@ -279,7 +282,7 @@ export function getLensAttributes(
},
},
},
- filters: [],
+ filters,
query: { language: combinedQuery.searchQueryLanguage, query: combinedQuery.searchString },
visualization: {
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/number_content_preview.tsx
index 08d2d42c6c027..0e38be72b20a4 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/number_content_preview.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/number_content_preview.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FileBasedFieldVisConfig } from '../stats_table/types';
export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => {
@@ -23,28 +23,34 @@ export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFie
-
+
-
+
-
+
-
+
-
+
-
+
- {stats.min}
- {stats.median}
- {stats.max}
+
+ {stats.min}
+
+
+ {stats.median}
+
+
+ {stats.max}
+
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
index e69e2e7626718..927d8ddb7a851 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
@@ -2,6 +2,7 @@
exports[`FieldTypeIcon render component when type matches a field type 1`] = `
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_field_type_icon.scss b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_field_type_icon.scss
new file mode 100644
index 0000000000000..67023a4421636
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_field_type_icon.scss
@@ -0,0 +1,4 @@
+.dvFieldTypeIcon__anchor {
+ display: flex;
+ align-items: center;
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_index.scss b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_index.scss
new file mode 100644
index 0000000000000..e57d750df95ec
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/_index.scss
@@ -0,0 +1 @@
+@import 'field_type_icon';
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
index 6b7c9eafc8c3e..b6a5ff3e5dbed 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
@@ -26,11 +26,10 @@ describe('FieldTypeIcon', () => {
const typeIconComponent = mount(
);
- const container = typeIconComponent.find({ 'data-test-subj': 'fieldTypeIcon' });
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
- container.simulate('mouseover');
+ typeIconComponent.simulate('mouseover');
// Run the timers so the EuiTooltip will be visible
jest.runAllTimers();
@@ -38,7 +37,7 @@ describe('FieldTypeIcon', () => {
typeIconComponent.update();
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
- container.simulate('mouseout');
+ typeIconComponent.simulate('mouseout');
// Run the timers so the EuiTooltip will be hidden again
jest.runAllTimers();
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
index ee4b4f8171d7d..3f84950c1345b 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
@@ -6,91 +6,62 @@
*/
import React, { FC } from 'react';
-
import { EuiToken, EuiToolTip } from '@elastic/eui';
-
import { i18n } from '@kbn/i18n';
-
import { getJobTypeAriaLabel } from '../../util/field_types_utils';
-import { JOB_FIELD_TYPES } from '../../../../../common';
import type { JobFieldType } from '../../../../../common';
+import './_index.scss';
interface FieldTypeIconProps {
tooltipEnabled: boolean;
type: JobFieldType;
- fieldName?: string;
needsAria: boolean;
}
interface FieldTypeIconContainerProps {
ariaLabel: string | null;
iconType: string;
- color: string;
+ color?: string;
needsAria: boolean;
[key: string]: any;
}
+// defaultIcon => a unknown datatype
+const defaultIcon = { iconType: 'questionInCircle', color: 'gray' };
+
+// Extended & modified version of src/plugins/kibana_react/public/field_icon/field_icon.tsx
+export const typeToEuiIconMap: Record = {
+ boolean: { iconType: 'tokenBoolean' },
+ // icon for an index pattern mapping conflict in discover
+ conflict: { iconType: 'alert', color: 'euiColorVis9' },
+ date: { iconType: 'tokenDate' },
+ date_range: { iconType: 'tokenDate' },
+ geo_point: { iconType: 'tokenGeo' },
+ geo_shape: { iconType: 'tokenGeo' },
+ ip: { iconType: 'tokenIP' },
+ ip_range: { iconType: 'tokenIP' },
+ // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html
+ murmur3: { iconType: 'tokenFile' },
+ number: { iconType: 'tokenNumber' },
+ number_range: { iconType: 'tokenNumber' },
+ histogram: { iconType: 'tokenHistogram' },
+ _source: { iconType: 'editorCodeBlock', color: 'gray' },
+ string: { iconType: 'tokenString' },
+ text: { iconType: 'tokenString' },
+ keyword: { iconType: 'tokenKeyword' },
+ nested: { iconType: 'tokenNested' },
+};
+
export const FieldTypeIcon: FC = ({
tooltipEnabled = false,
type,
- fieldName,
needsAria = true,
}) => {
const ariaLabel = getJobTypeAriaLabel(type);
-
- let iconType = 'questionInCircle';
- let color = 'euiColorVis6';
-
- switch (type) {
- // Set icon types and colors
- case JOB_FIELD_TYPES.BOOLEAN:
- iconType = 'tokenBoolean';
- color = 'euiColorVis5';
- break;
- case JOB_FIELD_TYPES.DATE:
- iconType = 'tokenDate';
- color = 'euiColorVis7';
- break;
- case JOB_FIELD_TYPES.GEO_POINT:
- case JOB_FIELD_TYPES.GEO_SHAPE:
- iconType = 'tokenGeo';
- color = 'euiColorVis8';
- break;
- case JOB_FIELD_TYPES.TEXT:
- iconType = 'document';
- color = 'euiColorVis9';
- break;
- case JOB_FIELD_TYPES.IP:
- iconType = 'tokenIP';
- color = 'euiColorVis3';
- break;
- case JOB_FIELD_TYPES.KEYWORD:
- iconType = 'tokenText';
- color = 'euiColorVis0';
- break;
- case JOB_FIELD_TYPES.NUMBER:
- iconType = 'tokenNumber';
- color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2';
- break;
- case JOB_FIELD_TYPES.HISTOGRAM:
- iconType = 'tokenHistogram';
- color = 'euiColorVis7';
- case JOB_FIELD_TYPES.UNKNOWN:
- // Use defaults
- break;
- }
-
- const containerProps = {
- ariaLabel,
- iconType,
- color,
- needsAria,
- };
+ const token = typeToEuiIconMap[type] || defaultIcon;
+ const containerProps = { ...token, ariaLabel, needsAria };
if (tooltipEnabled === true) {
- // wrap the inner component inside because EuiToolTip doesn't seem
- // to support having another component directly inside the tooltip anchor
- // see https://github.com/elastic/eui/issues/839
return (
= ({
defaultMessage: '{type} type',
values: { type },
})}
+ anchorClassName="dvFieldTypeIcon__anchor"
>
@@ -122,12 +94,15 @@ const FieldTypeIconContainer: FC = ({
if (needsAria && ariaLabel) {
wrapperProps['aria-label'] = ariaLabel;
}
-
return (
-
-
-
-
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
index 511a068f305f9..97dc2077d5931 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
@@ -14,7 +14,7 @@ import type {
FileBasedUnknownFieldVisConfig,
} from '../stats_table/types/field_vis_config';
import { FieldTypeIcon } from '../field_type_icon';
-import { JOB_FIELD_TYPES_OPTIONS } from '../../../../../common';
+import { jobTypeLabels } from '../../util/field_types_utils';
interface Props {
fields: Array;
@@ -39,27 +39,18 @@ export const DataVisualizerFieldTypesFilter: FC = ({
const fieldTypesTracker = new Set();
const fieldTypes: Option[] = [];
fields.forEach(({ type }) => {
- if (
- type !== undefined &&
- !fieldTypesTracker.has(type) &&
- JOB_FIELD_TYPES_OPTIONS[type] !== undefined
- ) {
- const item = JOB_FIELD_TYPES_OPTIONS[type];
+ if (type !== undefined && !fieldTypesTracker.has(type) && jobTypeLabels[type] !== undefined) {
+ const label = jobTypeLabels[type];
fieldTypesTracker.add(type);
fieldTypes.push({
value: type,
name: (
- {item.name}
+ {label}
{type && (
-
+
)}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
index 8e59345c9bd63..b57072eed2944 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
@@ -25,7 +25,7 @@ interface Props {
export const getDefaultDataVisualizerListState = (): DataVisualizerTableState => ({
pageIndex: 0,
- pageSize: 10,
+ pageSize: 25,
sortField: 'fieldName',
sortDirection: 'asc',
visibleFieldTypes: [],
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx
index caa58009fda5d..ff4701e22953f 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/multi_select_picker/multi_select_picker.tsx
@@ -98,7 +98,7 @@ export const MultiSelectPicker: FC<{
);
return (
-
+
(
-
-
-
-
+
.euiTableRowCell {
- border-bottom: 0;
- border-top: $euiBorderThin;
+@include euiBreakpoint('m', 'l', 'xl') {
+ .dvTable {
+ .columnHeader__title {
+ display: flex;
+ align-items: center;
+ }
- }
- .euiTableRow-isExpandedRow {
+ .columnHeader__icon {
+ padding-right: $euiSizeXS;
+ }
+
+ .euiTableRow > .euiTableRowCell {
+ border-bottom: 0;
+ border-top: $euiBorderThin;
- .euiTableRowCell {
- background-color: $euiColorEmptyShade !important;
- border-top: 0;
- border-bottom: $euiBorderThin;
- &:hover {
+ }
+
+ .euiTableCellContent {
+ padding: $euiSizeXS;
+ }
+
+ .euiTableRow-isExpandedRow {
+
+ .euiTableRowCell {
background-color: $euiColorEmptyShade !important;
+ border-top: 0;
+ border-bottom: $euiBorderThin;
+ &:hover {
+ background-color: $euiColorEmptyShade !important;
+ }
}
}
- }
- .dataVisualizerSummaryTable {
- max-width: 350px;
- min-width: 250px;
- .euiTableRow > .euiTableRowCell {
- border-bottom: 0;
+
+ .dvSummaryTable {
+ .euiTableRow > .euiTableRowCell {
+ border-bottom: 0;
+ }
+ .euiTableHeaderCell {
+ display: none;
+ }
+ }
+
+ .dvSummaryTable__wrapper {
+ min-width: $panelWidthS;
+ max-width: $panelWidthS;
}
- .euiTableHeaderCell {
- display: none;
+
+ .dvTopValues__wrapper {
+ min-width: fit-content;
+ }
+
+ .dvPanel__wrapper {
+ margin: $euiSizeXS $euiSizeM $euiSizeM 0;
+ &.dvPanel--compressed {
+ width: $panelWidthS;
+ }
+ &.dvPanel--uniform {
+ min-width: $panelWidthS;
+ max-width: $panelWidthS;
+ }
+ }
+
+ .dvMap__wrapper {
+ height: $euiSize * 15; //240px
+ }
+
+ .dvText__wrapper {
+ min-width: $panelWidthS;
}
- }
- .dataVisualizerSummaryTableWrapper {
- max-width: 300px;
- }
- .dataVisualizerMapWrapper {
- min-height: 300px;
- min-width: 600px;
}
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx
index 7279bceb8be93..8fdb68c6efa4c 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx
@@ -9,7 +9,12 @@ import { EuiText } from '@elastic/eui';
import React from 'react';
export const ExpandedRowFieldHeader = ({ children }: { children: React.ReactNode }) => (
-
+
{children}
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/_index.scss b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/_index.scss
index e44082c90ba32..0774cb198ea90 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/_index.scss
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/_index.scss
@@ -1,3 +1,12 @@
-.dataVisualizerFieldCountContainer {
+.dvFieldCount__panel {
+ margin-left: $euiSizeXS;
+ @include euiBreakpoint('xs', 's') {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+.dvFieldCount__item {
max-width: 300px;
+ min-width: 300px;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/metric_fields_count.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/metric_fields_count.tsx
index 7996e6366c497..3b1dbf0c6376d 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/metric_fields_count.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/metric_fields_count.tsx
@@ -30,8 +30,9 @@ export const MetricFieldsCount: FC = ({ metricsStats })
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/total_fields_count.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/total_fields_count.tsx
index 8e9e3e59f1281..53aa84c09d3a7 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/total_fields_count.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_count_stats/total_fields_count.tsx
@@ -30,8 +30,9 @@ export const TotalFieldsCount: FC = ({ fieldsCountStats }
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
index 2869b5030f81b..754d0e470fe40 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, ReactNode, useMemo } from 'react';
-import { EuiBasicTable, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { EuiBasicTable, EuiSpacer, RIGHT_ALIGNMENT, HorizontalAlignment } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -18,6 +18,7 @@ import { roundToDecimalPlace } from '../../../utils';
import { useDataVizChartTheme } from '../../hooks';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
+import { ExpandedRowPanel } from './expanded_row_panel';
function getPercentLabel(value: number): string {
if (value === 0) {
@@ -35,7 +36,7 @@ function getFormattedValue(value: number, totalCount: number): string {
return `${value} (${getPercentLabel(percentage)})`;
}
-const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 100;
+const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 70;
export const BooleanContent: FC = ({ config }) => {
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
@@ -68,9 +69,11 @@ export const BooleanContent: FC = ({ config }) => {
];
const summaryTableColumns = [
{
+ field: 'function',
name: '',
- render: (summaryItem: { display: ReactNode }) => summaryItem.display,
- width: '75px',
+ render: (_: string, summaryItem: { display: ReactNode }) => summaryItem.display,
+ width: '25px',
+ align: RIGHT_ALIGNMENT as HorizontalAlignment,
},
{
field: 'value',
@@ -90,18 +93,18 @@ export const BooleanContent: FC = ({ config }) => {
-
+
{summaryTableTitle}
-
+
-
+
= ({ config }) => {
yScaleType="linear"
/>
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx
index 318ff655abb21..9192ea85d5868 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, useMemo } from 'react';
-import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui';
+import { EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -20,6 +20,7 @@ import {
import { EMSTermJoinConfig } from '../../../../../../../../maps/public';
import { EmbeddedMapComponent } from '../../../embedded_map';
import { FieldVisStats } from '../../../../../../../common/types';
+import { ExpandedRowPanel } from './expanded_row_panel';
export const getChoroplethTopValuesLayer = (
fieldName: string,
@@ -104,14 +105,19 @@ export const ChoroplethMap: FC = ({ stats, suggestion }) => {
);
return (
-
-
+
+
+
{isTopValuesSampled === true && (
- <>
-
-
+
+
+
= ({ stats, suggestion }) => {
}}
/>
- >
+
)}
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
index 4adb76575dd48..8d5704fc16fd5 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
@@ -6,16 +6,18 @@
*/
import React, { FC, ReactNode } from 'react';
-import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
+import { EuiBasicTable, HorizontalAlignment } from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { RIGHT_ALIGNMENT } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
+import { ExpandedRowPanel } from './expanded_row_panel';
const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS';
interface SummaryTableItem {
function: string;
@@ -60,8 +62,10 @@ export const DateContent: FC
= ({ config }) => {
const summaryTableColumns = [
{
name: '',
- render: (summaryItem: { display: ReactNode }) => summaryItem.display,
- width: '75px',
+ field: 'function',
+ render: (func: string, summaryItem: { display: ReactNode }) => summaryItem.display,
+ width: '70px',
+ align: RIGHT_ALIGNMENT as HorizontalAlignment,
},
{
field: 'value',
@@ -73,10 +77,10 @@ export const DateContent: FC = ({ config }) => {
return (
-
+
{summaryTableTitle}
- className={'dataVisualizerSummaryTable'}
+ className={'dvSummaryTable'}
data-test-subj={'dataVisualizerDateSummaryTable'}
compressed
items={summaryTableItems}
@@ -84,7 +88,7 @@ export const DateContent: FC = ({ config }) => {
tableCaption={summaryTableTitle}
tableLayout="auto"
/>
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx
index f4ed74193d90a..5995b81555f9b 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx
@@ -8,16 +8,19 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC, ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
+import { EuiBasicTable, HorizontalAlignment, RIGHT_ALIGNMENT } from '@elastic/eui';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { FieldDataRowProps } from '../../types';
import { roundToDecimalPlace } from '../../../utils';
+import { ExpandedRowPanel } from './expanded_row_panel';
const metaTableColumns = [
{
+ field: 'function',
name: '',
- render: (metaItem: { display: ReactNode }) => metaItem.display,
- width: '75px',
+ render: (_: string, metaItem: { display: ReactNode }) => metaItem.display,
+ width: '25px',
+ align: RIGHT_ALIGNMENT as HorizontalAlignment,
},
{
field: 'value',
@@ -76,18 +79,18 @@ export const DocumentStatsTable: FC = ({ config }) => {
];
return (
-
{metaTableTitle}
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx
index a9f5dc6eaab1d..87caa0386da94 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, ReactNode } from 'react';
-import { EuiFlexGroup } from '@elastic/eui';
+import { EuiFlexGrid } from '@elastic/eui';
interface Props {
children: ReactNode;
@@ -14,12 +14,8 @@ interface Props {
}
export const ExpandedRowContent: FC = ({ children, dataTestSubj }) => {
return (
-
+
{children}
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx
new file mode 100644
index 0000000000000..b738dbdf67178
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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, { FC, ReactNode } from 'react';
+import { EuiPanel } from '@elastic/eui';
+import { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item';
+
+interface Props {
+ children: ReactNode;
+ dataTestSubj?: string;
+ grow?: EuiFlexItemProps['grow'];
+ className?: string;
+}
+export const ExpandedRowPanel: FC = ({ children, dataTestSubj, grow, className }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
index 77cf5fad5cca8..a5db86e0c30a0 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
@@ -11,7 +11,7 @@ import { TopValues } from '../../../top_values';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
-export const IpContent: FC = ({ config }) => {
+export const IpContent: FC = ({ config, onAddFilter }) => {
const { stats } = config;
if (stats === undefined) return null;
const { count, sampleCount, cardinality } = stats;
@@ -21,7 +21,12 @@ export const IpContent: FC = ({ config }) => {
return (
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
index 1baea4b3f2f7c..2bae49323a6bb 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
@@ -14,7 +14,7 @@ import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
import { ChoroplethMap } from './choropleth_map';
-export const KeywordContent: FC = ({ config }) => {
+export const KeywordContent: FC = ({ config, onAddFilter }) => {
const [EMSSuggestion, setEMSSuggestion] = useState();
const { stats, fieldName } = config;
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
@@ -44,7 +44,12 @@ export const KeywordContent: FC = ({ config }) => {
return (
-
+
{EMSSuggestion && stats && }
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/number_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/number_content.tsx
index ef3ac5a267346..d22638af1a2eb 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/number_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/number_content.tsx
@@ -6,7 +6,13 @@
*/
import React, { FC, ReactNode, useEffect, useState } from 'react';
-import { EuiBasicTable, EuiFlexItem, EuiText } from '@elastic/eui';
+import {
+ EuiBasicTable,
+ EuiFlexItem,
+ EuiText,
+ HorizontalAlignment,
+ RIGHT_ALIGNMENT,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -21,8 +27,9 @@ import { TopValues } from '../../../top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
+import { ExpandedRowPanel } from './expanded_row_panel';
-const METRIC_DISTRIBUTION_CHART_WIDTH = 325;
+const METRIC_DISTRIBUTION_CHART_WIDTH = 260;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 200;
interface SummaryTableItem {
@@ -31,7 +38,7 @@ interface SummaryTableItem {
value: number | string | undefined | null;
}
-export const NumberContent: FC = ({ config }) => {
+export const NumberContent: FC = ({ config, onAddFilter }) => {
const { stats } = config;
useEffect(() => {
@@ -83,7 +90,8 @@ export const NumberContent: FC = ({ config }) => {
{
name: '',
render: (summaryItem: { display: ReactNode }) => summaryItem.display,
- width: '75px',
+ width: '25px',
+ align: RIGHT_ALIGNMENT as HorizontalAlignment,
},
{
field: 'value',
@@ -101,23 +109,33 @@ export const NumberContent: FC = ({ config }) => {
return (
-
+
{summaryTableTitle}
- className={'dataVisualizerSummaryTable'}
+ className={'dvSummaryTable'}
compressed
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
data-test-subj={'dataVisualizerNumberSummaryTable'}
/>
-
+
{stats && (
-
+
)}
{distribution && (
-
+
= ({ config }) => {
/>
-
+
= ({ config }) => {
/>
-
+
)}
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/other_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/other_content.tsx
index 98d5cb2ec0fc9..4307da33523ed 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/other_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/other_content.tsx
@@ -6,7 +6,6 @@
*/
import React, { FC } from 'react';
-import { EuiFlexItem } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExamplesList } from '../../../examples_list';
import { DocumentStatsTable } from './document_stats';
@@ -15,14 +14,12 @@ import { ExpandedRowContent } from './expanded_row_content';
export const OtherContent: FC = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
- return (
+ return stats.count === undefined ? (
+ <>{Array.isArray(stats.examples) && }>
+ ) : (
- {Array.isArray(stats.examples) && (
-
-
-
- )}
+ {Array.isArray(stats.examples) && }
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
index 700a715a33396..6f946fc1025ed 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, Fragment } from 'react';
-import { EuiCallOut, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { EuiCallOut, EuiSpacer, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -26,7 +26,7 @@ export const TextContent: FC = ({ config }) => {
return (
-
+
{numExamples > 0 && }
{numExamples === 0 && (
@@ -44,7 +44,7 @@ export const TextContent: FC = ({ config }) => {
id="xpack.dataVisualizer.dataGrid.fieldText.fieldNotPresentDescription"
defaultMessage="This field was not present in the {sourceParam} field of documents queried."
values={{
- sourceParam: _source ,
+ sourceParam: _source ,
}}
/>
@@ -54,10 +54,10 @@ export const TextContent: FC = ({ config }) => {
id="xpack.dataVisualizer.dataGrid.fieldText.fieldMayBePopulatedDescription"
defaultMessage="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters."
values={{
- copyToParam: copy_to ,
- sourceParam: _source ,
- includesParam: includes ,
- excludesParam: excludes ,
+ copyToParam: copy_to ,
+ sourceParam: _source ,
+ includesParam: includes ,
+ excludesParam: excludes ,
}}
/>
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.scss b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.scss
index 63603ee9bd2ec..8a0b9cc992c3e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.scss
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.scss
@@ -1,19 +1,22 @@
.dataGridChart__histogram {
width: 100%;
- height: $euiSizeXL + $euiSizeXXL;
+}
+
+.dataGridChart__column-chart {
+ width: 100%;
}
.dataGridChart__legend {
@include euiTextTruncate;
- @include euiFontSizeXS;
color: $euiColorMediumShade;
display: block;
overflow-x: hidden;
- margin: $euiSizeXS 0 0 0;
font-style: italic;
font-weight: normal;
text-align: left;
+ line-height: 1.1;
+ font-size: #{$euiFontSizeL / 2}; // 10px
}
.dataGridChart__legend--numeric {
@@ -21,9 +24,7 @@
}
.dataGridChart__legendBoolean {
- width: 100%;
- min-width: $euiButtonMinWidth;
- td { text-align: center }
+ width: #{$euiSizeXS * 2.5} // 10px
}
/* Override to align column header to bottom of cell when no chart is available */
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
index ed4b82005db29..453754d4d6bd4 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/column_chart.tsx
@@ -8,7 +8,7 @@
import React, { FC } from 'react';
import classNames from 'classnames';
-import { BarSeries, Chart, Settings } from '@elastic/charts';
+import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/charts';
import { EuiDataGridColumn } from '@elastic/eui';
import './column_chart.scss';
@@ -25,22 +25,9 @@ interface Props {
maxChartColumns?: number;
}
-const columnChartTheme = {
- background: { color: 'transparent' },
- chartMargins: {
- left: 0,
- right: 0,
- top: 0,
- bottom: 1,
- },
- chartPaddings: {
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- },
- scales: { barsPadding: 0.1 },
-};
+const zeroSize = { bottom: 0, left: 0, right: 0, top: 0 };
+const size = { width: 100, height: 10 };
+
export const ColumnChart: FC = ({
chartData,
columnType,
@@ -48,26 +35,34 @@ export const ColumnChart: FC = ({
hideLabel,
maxChartColumns,
}) => {
- const { data, legendText, xScaleType } = useColumnChart(chartData, columnType, maxChartColumns);
+ const { data, legendText } = useColumnChart(chartData, columnType, maxChartColumns);
return (
{!isUnsupportedChartData(chartData) && data.length > 0 && (
-
-
-
- d.datum.color}
- data={data}
- />
-
-
+
+
+ {
+ return `${data[idx]?.key_as_string ?? ''}`;
+ }}
+ hide
+ />
+ d.datum.color}
+ />
+
)}
{
+interface Props {
+ cardinality?: number;
+ showIcon?: boolean;
+}
+
+export const DistinctValues = ({ cardinality, showIcon }: Props) => {
if (cardinality === undefined) return null;
return (
-
-
-
-
-
- {cardinality}
-
-
+ <>
+ {showIcon ?
: null}
+
{cardinality}
+ >
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx
index 7d0bda6ac47ea..01b8f0af9538d 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx
@@ -5,29 +5,36 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
+import { EuiIcon, EuiText } from '@elastic/eui';
import React from 'react';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { roundToDecimalPlace } from '../../../utils';
+import { isIndexBasedFieldVisConfig } from '../../types';
-export const DocumentStat = ({ config }: FieldDataRowProps) => {
+interface Props extends FieldDataRowProps {
+ showIcon?: boolean;
+}
+export const DocumentStat = ({ config, showIcon }: Props) => {
const { stats } = config;
if (stats === undefined) return null;
-
const { count, sampleCount } = stats;
- if (count === undefined || sampleCount === undefined) return null;
- const docsPercent = roundToDecimalPlace((count / sampleCount) * 100);
+ // If field exists is docs but we don't have count stats then don't show
+ // Otherwise if field doesn't appear in docs at all, show 0%
+ const docsCount =
+ count ?? (isIndexBasedFieldVisConfig(config) && config.existsInDocs === true ? undefined : 0);
+ const docsPercent =
+ docsCount !== undefined && sampleCount !== undefined
+ ? roundToDecimalPlace((docsCount / sampleCount) * 100)
+ : 0;
- return (
-
-
-
-
-
- {count} ({docsPercent}%)
+ return docsCount !== undefined ? (
+ <>
+ {showIcon ? : null}
+
+ {docsCount} ({docsPercent}%)
-
- );
+ >
+ ) : null;
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/number_content_preview.tsx
index 651e41b0cbea8..dd8685fdb9380 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/number_content_preview.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/number_content_preview.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, useEffect, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames';
import {
MetricDistributionChart,
@@ -16,8 +16,8 @@ import {
import { FieldVisConfig } from '../../types';
import { kibanaFieldFormat, formatSingleValue } from '../../../utils';
-const METRIC_DISTRIBUTION_CHART_WIDTH = 150;
-const METRIC_DISTRIBUTION_CHART_HEIGHT = 80;
+const METRIC_DISTRIBUTION_CHART_WIDTH = 100;
+const METRIC_DISTRIBUTION_CHART_HEIGHT = 10;
export interface NumberContentPreviewProps {
config: FieldVisConfig;
@@ -59,8 +59,11 @@ export const IndexBasedNumberContentPreview: FC
= ({
{legendText && (
<>
-
-
+
{kibanaFieldFormat(legendText.min, fieldFormat)}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.test.tsx
index aff4d6d62c6c8..228719552da9e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.test.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.test.tsx
@@ -122,8 +122,8 @@ describe('getLegendText()', () => {
})}
>
);
- expect(getByText('true')).toBeInTheDocument();
- expect(getByText('false')).toBeInTheDocument();
+ expect(getByText('t')).toBeInTheDocument();
+ expect(getByText('f')).toBeInTheDocument();
});
it('should return the chart legend text for ordinal chart data with less than max categories', () => {
expect(getLegendText({ ...validOrdinalChartData, data: [{ key: 'cat', doc_count: 10 }] })).toBe(
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
index 2bcf1854235d2..2c0817228655e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
@@ -94,11 +94,19 @@ export const getLegendText = (
if (chartData.type === 'boolean') {
return (
-
+
- {chartData.data[0] !== undefined && {chartData.data[0].key_as_string} }
- {chartData.data[1] !== undefined && {chartData.data[1].key_as_string} }
+ {chartData.data[0] !== undefined && (
+
+ {chartData.data[0].key_as_string?.slice(0, 1) ?? ''}
+
+ )}
+ {chartData.data[1] !== undefined && (
+
+ {chartData.data[1].key_as_string?.slice(0, 1) ?? ''}
+
+ )}
@@ -185,14 +193,16 @@ export const useColumnChart = (
// The if/else if/else is a work-around because `.map()` doesn't work with union types.
// See TS Caveats for details: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
if (isOrdinalChartData(chartData)) {
- data = chartData.data.map((d: OrdinalDataItem) => ({
+ data = chartData.data.map((d: OrdinalDataItem, idx) => ({
...d,
+ x: idx,
key_as_string: d.key_as_string ?? d.key,
color: getColor(d),
}));
} else if (isNumericChartData(chartData)) {
- data = chartData.data.map((d: NumericDataItem) => ({
+ data = chartData.data.map((d: NumericDataItem, idx) => ({
...d,
+ x: idx,
key_as_string: d.key_as_string || d.key,
color: getColor(d),
}));
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx
index 2c4739206d47f..627c206e87fb0 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/metric_distribution_chart/metric_distribution_chart.tsx
@@ -75,14 +75,17 @@ export const MetricDistributionChart: FC = ({
return (
);
};
return (
-
+
{
updatePageState: (update: DataVisualizerTableState) => void;
getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap;
extendedColumns?: Array>;
+ showPreviewByDefault?: boolean;
+ /** Callback to receive any updates when table or page state is changed **/
+ onChange?: (update: Partial) => void;
}
export const DataVisualizerTable = ({
@@ -57,23 +62,52 @@ export const DataVisualizerTable = ({
updatePageState,
getItemIdToExpandedRowMap,
extendedColumns,
+ showPreviewByDefault,
+ onChange,
}: DataVisualizerTableProps) => {
const [expandedRowItemIds, setExpandedRowItemIds] = useState([]);
- const [expandAll, toggleExpandAll] = useState(false);
+ const [expandAll, setExpandAll] = useState(false);
const { onTableChange, pagination, sorting } = useTableSettings(
items,
pageState,
updatePageState
);
- const showDistributions: boolean =
- ('showDistributions' in pageState && pageState.showDistributions) ?? true;
- const toggleShowDistribution = () => {
- updatePageState({
- ...pageState,
- showDistributions: !showDistributions,
- });
- };
+ const [showDistributions, setShowDistributions] = useState(showPreviewByDefault ?? true);
+ const [dimensions, setDimensions] = useState(calculateTableColumnsDimensions());
+ const [tableWidth, setTableWidth] = useState(1400);
+
+ const toggleExpandAll = useCallback(
+ (shouldExpandAll: boolean) => {
+ setExpandedRowItemIds(
+ shouldExpandAll
+ ? // Update list of ids in expandedRowIds to include all
+ (items.map((item) => item.fieldName).filter((id) => id !== undefined) as string[])
+ : // Otherwise, reset list of ids in expandedRowIds
+ []
+ );
+ setExpandAll(shouldExpandAll);
+ },
+ [items]
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const resizeHandler = useCallback(
+ throttle((e: { width: number; height: number }) => {
+ // When window or table is resized,
+ // update the column widths and other settings accordingly
+ setTableWidth(e.width);
+ setDimensions(calculateTableColumnsDimensions(e.width));
+ }, 500),
+ [tableWidth]
+ );
+
+ const toggleShowDistribution = useCallback(() => {
+ setShowDistributions(!showDistributions);
+ if (onChange) {
+ onChange({ showDistributions: !showDistributions });
+ }
+ }, [onChange, showDistributions]);
function toggleDetails(item: DataVisualizerTableItem) {
if (item.fieldName === undefined) return;
@@ -90,31 +124,32 @@ export const DataVisualizerTable = ({
const columns = useMemo(() => {
const expanderColumn: EuiTableComputedColumnType = {
- name: (
- toggleExpandAll(!expandAll)}
- aria-label={
- !expandAll
- ? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', {
- defaultMessage: 'Expand details for all fields',
- })
- : i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', {
- defaultMessage: 'Collapse details for all fields',
- })
- }
- iconType={expandAll ? 'arrowUp' : 'arrowDown'}
- />
- ),
+ name:
+ dimensions.breakPoint !== 'xs' && dimensions.breakPoint !== 's' ? (
+ toggleExpandAll(!expandAll)}
+ aria-label={
+ !expandAll
+ ? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', {
+ defaultMessage: 'Expand details for all fields',
+ })
+ : i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', {
+ defaultMessage: 'Collapse details for all fields',
+ })
+ }
+ iconType={expandAll ? 'arrowDown' : 'arrowRight'}
+ />
+ ) : null,
align: RIGHT_ALIGNMENT,
- width: '40px',
+ width: dimensions.expander,
isExpander: true,
render: (item: DataVisualizerTableItem) => {
const displayName = item.displayName ?? item.fieldName;
if (item.fieldName === undefined) return null;
- const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown';
+ const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowDown' : 'arrowRight';
return (
({
render: (fieldType: JobFieldType) => {
return ;
},
- width: '75px',
+ width: dimensions.type,
sortable: true,
align: CENTER_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnType',
@@ -163,8 +198,8 @@ export const DataVisualizerTable = ({
const displayName = item.displayName ?? item.fieldName;
return (
-
- {displayName}
+
+ {displayName}
);
},
@@ -177,56 +212,65 @@ export const DataVisualizerTable = ({
defaultMessage: 'Documents (%)',
}),
render: (value: number | undefined, item: DataVisualizerTableItem) => (
-
+
),
sortable: (item: DataVisualizerTableItem) => item?.stats?.count,
align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDocumentsCount',
+ width: dimensions.docCount,
},
{
field: 'stats.cardinality',
name: i18n.translate('xpack.dataVisualizer.dataGrid.distinctValuesColumnName', {
defaultMessage: 'Distinct values',
}),
- render: (cardinality?: number) => ,
+ render: (cardinality: number | undefined) => (
+
+ ),
+
sortable: true,
align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDistinctValues',
+ width: dimensions.distinctValues,
},
{
name: (
-
-
+
+ {dimensions.showIcon ? (
+
+ ) : null}
{i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', {
defaultMessage: 'Distributions',
})}
-
- toggleShowDistribution()}
- aria-label={
+ {
+
-
+ >
+ toggleShowDistribution()}
+ aria-label={
+ showDistributions
+ ? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', {
+ defaultMessage: 'Show distributions',
+ })
+ : i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', {
+ defaultMessage: 'Hide distributions',
+ })
+ }
+ />
+
+ }
),
render: (item: DataVisualizerTableItem) => {
@@ -252,41 +296,49 @@ export const DataVisualizerTable =
({
return null;
},
+ width: dimensions.distributions,
align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDistribution',
},
];
return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [expandAll, showDistributions, updatePageState, extendedColumns]);
+ }, [
+ expandAll,
+ showDistributions,
+ updatePageState,
+ extendedColumns,
+ dimensions.breakPoint,
+ toggleExpandAll,
+ ]);
const itemIdToExpandedRowMap = useMemo(() => {
- let itemIds = expandedRowItemIds;
- if (expandAll) {
- itemIds = items.map((i) => i[FIELD_NAME]).filter((f) => f !== undefined) as string[];
- }
+ const itemIds = expandedRowItemIds;
return getItemIdToExpandedRowMap(itemIds, items);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [expandAll, items, expandedRowItemIds]);
+ }, [items, expandedRowItemIds, getItemIdToExpandedRowMap]);
return (
-
-
- className={'dataVisualizer'}
- items={items}
- itemId={FIELD_NAME}
- columns={columns}
- pagination={pagination}
- sorting={sorting}
- isExpandable={true}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
- isSelectable={false}
- onTableChange={onTableChange}
- data-test-subj={'dataVisualizerTable'}
- rowProps={(item) => ({
- 'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
- })}
- />
-
+
+ {(resizeRef) => (
+
+
+ className={'dvTable'}
+ items={items}
+ itemId={FIELD_NAME}
+ columns={columns}
+ pagination={pagination}
+ sorting={sorting}
+ isExpandable={true}
+ itemIdToExpandedRowMap={itemIdToExpandedRowMap}
+ isSelectable={false}
+ onTableChange={onTableChange}
+ data-test-subj={'dataVisualizerTable'}
+ rowProps={(item) => ({
+ 'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
+ })}
+ />
+
+ )}
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
index 24209af23ceb4..94b704764c93b 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
@@ -6,7 +6,9 @@
*/
import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config';
+import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common';
export interface FieldDataRowProps {
config: FieldVisConfig | FileBasedFieldVisConfig;
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
index 3fbf333bdc876..87d936edc2957 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
@@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
import { DataVisualizerTableState } from '../../../../../common';
-const PAGE_SIZE_OPTIONS = [10, 25, 50];
+const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
interface UseTableSettingsReturnValue {
onTableChange: EuiBasicTableProps['onChange'];
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/utils.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/utils.ts
index 27da91153b3ba..d30a33a96c590 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/utils.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { getBreakpoint } from '@elastic/eui';
import { FileBasedFieldVisConfig } from './types';
export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
@@ -36,3 +37,45 @@ export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
falseCount,
};
};
+
+export const calculateTableColumnsDimensions = (width?: number) => {
+ const defaultSettings = {
+ expander: '40px',
+ type: '75px',
+ docCount: '225px',
+ distinctValues: '225px',
+ distributions: '225px',
+ showIcon: true,
+ breakPoint: 'xl',
+ };
+ if (width === undefined) return defaultSettings;
+ const breakPoint = getBreakpoint(width);
+ switch (breakPoint) {
+ case 'xs':
+ case 's':
+ return {
+ expander: '25px',
+ type: '40px',
+ docCount: 'auto',
+ distinctValues: 'auto',
+ distributions: 'auto',
+ showIcon: false,
+ breakPoint,
+ };
+
+ case 'm':
+ case 'l':
+ return {
+ expander: '25px',
+ type: '40px',
+ docCount: 'auto',
+ distinctValues: 'auto',
+ distributions: 'auto',
+ showIcon: false,
+ breakPoint,
+ };
+
+ default:
+ return defaultSettings;
+ }
+};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/_top_values.scss b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/_top_values.scss
index 05fa1bfa94b2d..bb227dd24d48a 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/_top_values.scss
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/_top_values.scss
@@ -4,16 +4,4 @@
.topValuesValueLabelContainer {
margin-right: $euiSizeM;
- &.topValuesValueLabelContainer--small {
- width:70px;
- }
-
- &.topValuesValueLabelContainer--large {
- width: 200px;
- }
-}
-
-.topValuesPercentLabelContainer {
- margin-left: $euiSizeM;
- width:70px;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
index a019f7fb0976c..45e8944c7c667 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
@@ -12,21 +12,25 @@ import {
EuiProgress,
EuiSpacer,
EuiText,
- EuiToolTip,
+ EuiButtonIcon,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import classNames from 'classnames';
+import { i18n } from '@kbn/i18n';
import { roundToDecimalPlace, kibanaFieldFormat } from '../utils';
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
import { FieldVisStats } from '../../../../../common/types';
+import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel';
+import { IndexPatternField } from '../../../../../../../../src/plugins/data/common/data_views/fields';
interface Props {
stats: FieldVisStats | undefined;
fieldFormat?: any;
barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent';
compressed?: boolean;
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
function getPercentLabel(docCount: number, topValuesSampleSize: number): string {
@@ -38,13 +42,23 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string
}
}
-export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed }) => {
+export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, onAddFilter }) => {
if (stats === undefined) return null;
- const { topValues, topValuesSampleSize, topValuesSamplerShardSize, count, isTopValuesSampled } =
- stats;
+ const {
+ topValues,
+ topValuesSampleSize,
+ topValuesSamplerShardSize,
+ count,
+ isTopValuesSampled,
+ fieldName,
+ } = stats;
+
const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count;
return (
-
+
= ({ stats, fieldFormat, barColor, compressed
{Array.isArray(topValues) &&
topValues.map((value) => (
-
-
-
- {kibanaFieldFormat(value.key, fieldFormat)}
-
-
-
- {progressBarMax !== undefined && (
-
-
- {getPercentLabel(value.doc_count, progressBarMax)}
-
-
- )}
+ {fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? (
+ <>
+
+ onAddFilter(
+ fieldName,
+ typeof value.key === 'number' ? value.key.toString() : value.key,
+ '+'
+ )
+ }
+ aria-label={i18n.translate(
+ 'xpack.dataVisualizer.dataGrid.field.addFilterAriaLabel',
+ {
+ defaultMessage: 'Filter for {fieldName}: "{value}"',
+ values: { fieldName, value: value.key },
+ }
+ )}
+ data-test-subj={`dvFieldDataTopValuesAddFilterButton-${value.key}-${value.key}`}
+ style={{
+ minHeight: 'auto',
+ minWidth: 'auto',
+ paddingRight: 2,
+ paddingLeft: 2,
+ paddingTop: 0,
+ paddingBottom: 0,
+ }}
+ />
+
+ onAddFilter(
+ fieldName,
+ typeof value.key === 'number' ? value.key.toString() : value.key,
+ '-'
+ )
+ }
+ aria-label={i18n.translate(
+ 'xpack.dataVisualizer.dataGrid.field.removeFilterAriaLabel',
+ {
+ defaultMessage: 'Filter out {fieldName}: "{value}"',
+ values: { fieldName, value: value.key },
+ }
+ )}
+ data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${value.key}-${value.key}`}
+ style={{
+ minHeight: 'auto',
+ minWidth: 'auto',
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingRight: 2,
+ paddingLeft: 2,
+ }}
+ />
+ >
+ ) : null}
))}
{isTopValuesSampled === true && (
-
+
= ({ stats, fieldFormat, barColor, compressed
)}
-
+
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
index 54e0b2d5310f3..3e459cd2b079b 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
@@ -23,6 +23,9 @@ export const jobTypeAriaLabels = {
geoPointParam: 'geo point',
},
}),
+ GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', {
+ defaultMessage: 'geo shape type',
+ }),
IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', {
defaultMessage: 'ip type',
}),
@@ -32,6 +35,9 @@ export const jobTypeAriaLabels = {
NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', {
defaultMessage: 'number type',
}),
+ HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', {
+ defaultMessage: 'histogram type',
+ }),
TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', {
defaultMessage: 'text type',
}),
@@ -40,6 +46,48 @@ export const jobTypeAriaLabels = {
}),
};
+export const jobTypeLabels = {
+ [JOB_FIELD_TYPES.BOOLEAN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeLabel', {
+ defaultMessage: 'Boolean',
+ }),
+ [JOB_FIELD_TYPES.DATE]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeLabel', {
+ defaultMessage: 'Date',
+ }),
+ [JOB_FIELD_TYPES.GEO_POINT]: i18n.translate(
+ 'xpack.dataVisualizer.fieldTypeIcon.geoPointTypeLabel',
+ {
+ defaultMessage: 'Geo point',
+ }
+ ),
+ [JOB_FIELD_TYPES.GEO_SHAPE]: i18n.translate(
+ 'xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeLabel',
+ {
+ defaultMessage: 'Geo shape',
+ }
+ ),
+ [JOB_FIELD_TYPES.IP]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeLabel', {
+ defaultMessage: 'IP',
+ }),
+ [JOB_FIELD_TYPES.KEYWORD]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeLabel', {
+ defaultMessage: 'Keyword',
+ }),
+ [JOB_FIELD_TYPES.NUMBER]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeLabel', {
+ defaultMessage: 'Number',
+ }),
+ [JOB_FIELD_TYPES.HISTOGRAM]: i18n.translate(
+ 'xpack.dataVisualizer.fieldTypeIcon.histogramTypeLabel',
+ {
+ defaultMessage: 'Histogram',
+ }
+ ),
+ [JOB_FIELD_TYPES.TEXT]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeLabel', {
+ defaultMessage: 'Text',
+ }),
+ [JOB_FIELD_TYPES.UNKNOWN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeLabel', {
+ defaultMessage: 'Unknown',
+ }),
+};
+
export const getJobTypeAriaLabel = (type: string) => {
const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find(
(k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx
index bc68bdf4b6ce0..186d3ef840c21 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx
@@ -40,6 +40,7 @@ export const ActionsPanel: FC = ({
const {
services: {
+ data,
application: { capabilities },
share: {
urlGenerators: { getUrlGenerator },
@@ -60,6 +61,9 @@ export const ActionsPanel: FC = ({
const state: DiscoverUrlGeneratorState = {
indexPatternId,
};
+
+ state.filters = data.query.filterManager.getFilters() ?? [];
+
if (searchString && searchQueryLanguage !== undefined) {
state.query = { query: searchString, language: searchQueryLanguage };
}
@@ -113,6 +117,7 @@ export const ActionsPanel: FC = ({
capabilities,
getUrlGenerator,
additionalLinks,
+ data.query,
]);
// Note we use display:none for the DataRecognizer section as it needs to be
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index.scss b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index.scss
new file mode 100644
index 0000000000000..c9b1d78320aee
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index.scss
@@ -0,0 +1 @@
+@import 'index_data_visualizer_view';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index_data_visualizer_view.scss b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index_data_visualizer_view.scss
new file mode 100644
index 0000000000000..f49cb73454178
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/_index_data_visualizer_view.scss
@@ -0,0 +1,13 @@
+.dataViewTitleHeader {
+ min-width: 300px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+@include euiBreakpoint('xs', 's', 'm', 'l') {
+ .dataVisualizerPageHeader {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
index 0eb8e6363d607..fdd723dea3487 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
@@ -23,12 +23,12 @@ import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_tab
import { FormattedMessage } from '@kbn/i18n/react';
import { Required } from 'utility-types';
import { i18n } from '@kbn/i18n';
+import { Filter } from '@kbn/es-query';
import {
- IndexPatternField,
KBN_FIELD_TYPES,
UI_SETTINGS,
Query,
- IndexPattern,
+ generateFilters,
} from '../../../../../../../../src/plugins/data/public';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { usePageUrlState, useUrlState } from '../../../common/util/url_state';
@@ -65,10 +65,12 @@ import { DatePickerWrapper } from '../../../common/components/date_picker_wrappe
import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
import { HelpMenu } from '../../../common/components/help_menu';
import { TimeBuckets } from '../../services/time_buckets';
-import { extractSearchData } from '../../utils/saved_search_utils';
+import { createMergedEsQuery, getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
import { ResultLink } from '../../../common/components/results_links';
import { extractErrorProperties } from '../../utils/error_utils';
+import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common';
+import './_index.scss';
interface DataVisualizerPageState {
overallStats: OverallStats;
@@ -85,7 +87,7 @@ const defaultSearchQuery = {
match_all: {},
};
-function getDefaultPageState(): DataVisualizerPageState {
+export function getDefaultPageState(): DataVisualizerPageState {
return {
overallStats: {
totalCount: 0,
@@ -103,22 +105,25 @@ function getDefaultPageState(): DataVisualizerPageState {
documentCountStats: undefined,
};
}
-export const getDefaultDataVisualizerListState =
- (): Required => ({
- pageIndex: 0,
- pageSize: 10,
- sortField: 'fieldName',
- sortDirection: 'asc',
- visibleFieldTypes: [],
- visibleFieldNames: [],
- samplerShardSize: 5000,
- searchString: '',
- searchQuery: defaultSearchQuery,
- searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
- showDistributions: true,
- showAllFields: false,
- showEmptyFields: false,
- });
+export const getDefaultDataVisualizerListState = (
+ overrides?: Partial
+): Required => ({
+ pageIndex: 0,
+ pageSize: 25,
+ sortField: 'fieldName',
+ sortDirection: 'asc',
+ visibleFieldTypes: [],
+ visibleFieldNames: [],
+ samplerShardSize: 5000,
+ searchString: '',
+ searchQuery: defaultSearchQuery,
+ searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
+ filters: [],
+ showDistributions: true,
+ showAllFields: false,
+ showEmptyFields: false,
+ ...overrides,
+});
export interface IndexDataVisualizerViewProps {
currentIndexPattern: IndexPattern;
@@ -129,7 +134,7 @@ const restorableDefaults = getDefaultDataVisualizerListState();
export const IndexDataVisualizerView: FC = (dataVisualizerProps) => {
const { services } = useDataVisualizerKibana();
- const { docLinks, notifications, uiSettings } = services;
+ const { docLinks, notifications, uiSettings, data } = services;
const { toasts } = notifications;
const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState(
@@ -150,6 +155,15 @@ export const IndexDataVisualizerView: FC = (dataVi
}
}, [dataVisualizerProps?.currentSavedSearch]);
+ useEffect(() => {
+ return () => {
+ // When navigating away from the index pattern
+ // Reset all previously set filters
+ // to make sure new page doesn't have unrelated filters
+ data.query.filterManager.removeAll();
+ };
+ }, [currentIndexPattern.id, data.query.filterManager]);
+
const getTimeBuckets = useCallback(() => {
return new TimeBuckets({
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
@@ -227,13 +241,17 @@ export const IndexDataVisualizerView: FC = (dataVi
const defaults = getDefaultPageState();
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
- const searchData = extractSearchData(
- currentSavedSearch,
- currentIndexPattern,
- uiSettings.get(UI_SETTINGS.QUERY_STRING_OPTIONS)
- );
+ const searchData = getEsQueryFromSavedSearch({
+ indexPattern: currentIndexPattern,
+ uiSettings,
+ savedSearch: currentSavedSearch,
+ filterManager: data.query.filterManager,
+ });
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
+ if (dataVisualizerListState.filters) {
+ data.query.filterManager.setFilters(dataVisualizerListState.filters);
+ }
return {
searchQuery: dataVisualizerListState.searchQuery,
searchString: dataVisualizerListState.searchString,
@@ -247,26 +265,31 @@ export const IndexDataVisualizerView: FC = (dataVi
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState]);
-
- const setSearchParams = (searchParams: {
- searchQuery: Query['query'];
- searchString: Query['query'];
- queryLanguage: SearchQueryLanguage;
- }) => {
- // When the user loads saved search and then clear or modify the query
- // we should remove the saved search and replace it with the index pattern id
- if (currentSavedSearch !== null) {
- setCurrentSavedSearch(null);
- }
+ }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]);
+
+ const setSearchParams = useCallback(
+ (searchParams: {
+ searchQuery: Query['query'];
+ searchString: Query['query'];
+ queryLanguage: SearchQueryLanguage;
+ filters: Filter[];
+ }) => {
+ // When the user loads saved search and then clear or modify the query
+ // we should remove the saved search and replace it with the index pattern id
+ if (currentSavedSearch !== null) {
+ setCurrentSavedSearch(null);
+ }
- setDataVisualizerListState({
- ...dataVisualizerListState,
- searchQuery: searchParams.searchQuery,
- searchString: searchParams.searchString,
- searchQueryLanguage: searchParams.queryLanguage,
- });
- };
+ setDataVisualizerListState({
+ ...dataVisualizerListState,
+ searchQuery: searchParams.searchQuery,
+ searchString: searchParams.searchString,
+ searchQueryLanguage: searchParams.queryLanguage,
+ filters: searchParams.filters,
+ });
+ },
+ [currentSavedSearch, dataVisualizerListState, setDataVisualizerListState]
+ );
const samplerShardSize =
dataVisualizerListState.samplerShardSize ?? restorableDefaults.samplerShardSize;
@@ -305,6 +328,52 @@ export const IndexDataVisualizerView: FC = (dataVi
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
+ const onAddFilter = useCallback(
+ (field: IndexPatternField | string, values: string, operation: '+' | '-') => {
+ const newFilters = generateFilters(
+ data.query.filterManager,
+ field,
+ values,
+ operation,
+ String(currentIndexPattern.id)
+ );
+ if (newFilters) {
+ data.query.filterManager.addFilters(newFilters);
+ }
+
+ // Merge current query with new filters
+ const mergedQuery = {
+ query: searchString || '',
+ language: searchQueryLanguage,
+ };
+
+ const combinedQuery = createMergedEsQuery(
+ {
+ query: searchString || '',
+ language: searchQueryLanguage,
+ },
+ data.query.filterManager.getFilters() ?? [],
+ currentIndexPattern,
+ uiSettings
+ );
+
+ setSearchParams({
+ searchQuery: combinedQuery,
+ searchString: mergedQuery.query,
+ queryLanguage: mergedQuery.language as SearchQueryLanguage,
+ filters: data.query.filterManager.getFilters(),
+ });
+ },
+ [
+ currentIndexPattern,
+ data.query.filterManager,
+ searchQueryLanguage,
+ searchString,
+ setSearchParams,
+ uiSettings,
+ ]
+ );
+
useEffect(() => {
const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(),
@@ -666,11 +735,11 @@ export const IndexDataVisualizerView: FC = (dataVi
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
const nonMetricConfig = {
- ...fieldData,
+ ...(fieldData ? fieldData : {}),
fieldFormat: currentIndexPattern.getFormatterForField(field),
aggregatable: field.aggregatable,
scripted: field.scripted,
- loading: fieldData.existsInDocs,
+ loading: fieldData?.existsInDocs,
deletable: field.runtimeField !== undefined,
};
@@ -751,13 +820,14 @@ export const IndexDataVisualizerView: FC = (dataVi
item={item}
indexPattern={currentIndexPattern}
combinedQuery={{ searchQueryLanguage, searchString }}
+ onAddFilter={onAddFilter}
/>
);
}
return m;
}, {} as ItemIdToExpandedRowMap);
},
- [currentIndexPattern, searchQueryLanguage, searchString]
+ [currentIndexPattern, searchQueryLanguage, searchString, onAddFilter]
);
// Some actions open up fly-out or popup
@@ -809,17 +879,10 @@ export const IndexDataVisualizerView: FC = (dataVi
-
+
-
-
+
+
{currentIndexPattern.title}
= (dataVi
-
-
- {currentIndexPattern.timeFieldName !== undefined && (
-
-
-
- )}
+
+ {currentIndexPattern.timeFieldName !== undefined && (
-
+
-
-
+ )}
+
+
+
+
@@ -862,8 +928,6 @@ export const IndexDataVisualizerView: FC = (dataVi
/>
)}
-
-
= (dataVi
visibleFieldNames={visibleFieldNames}
setVisibleFieldNames={setVisibleFieldNames}
showEmptyFields={showEmptyFields}
+ onAddFilter={onAddFilter}
/>
-
+
= ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => {
const options: Option[] = useMemo(() => {
return indexedFieldTypes.map((indexedFieldName) => {
- const item = JOB_FIELD_TYPES_OPTIONS[indexedFieldName];
+ const label = jobTypeLabels[indexedFieldName] ?? '';
return {
value: indexedFieldName,
name: (
- {item.name}
+ {label}
{indexedFieldName && (
-
+
)}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss
new file mode 100644
index 0000000000000..6f274921d5ebf
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss
@@ -0,0 +1,20 @@
+.dvSearchPanel__controls {
+ flex-direction: row;
+ padding: $euiSizeS;
+}
+
+@include euiBreakpoint('xs', 's', 'm', 'l') {
+ .dvSearchPanel__container {
+ flex-direction: column;
+ }
+ .dvSearchBar {
+ min-width: #{'max(100%, 500px)'};
+ }
+ .dvSearchPanel__controls {
+ padding: 0;
+ }
+ // prevent margin -16 which scrunches the filter bar
+ .globalFilterGroup__wrapper-isVisible {
+ margin: 0 !important;
+ }
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
index 91ec1e449bb38..f55114ca36d78 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
@@ -6,21 +6,22 @@
*/
import React, { FC, useEffect, useState } from 'react';
-import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiInputPopover } from '@elastic/eui';
+import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { Query, fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query';
-import { QueryStringInput } from '../../../../../../../../src/plugins/data/public';
+import { Query, Filter } from '@kbn/es-query';
import { ShardSizeFilter } from './shard_size_select';
import { DataVisualizerFieldNamesFilter } from './field_name_filter';
-import { DatavisualizerFieldTypeFilter } from './field_type_filter';
-import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
-import { JobFieldType } from '../../../../../common/types';
+import { DataVisualizerFieldTypeFilter } from './field_type_filter';
import {
- ErrorMessage,
- SEARCH_QUERY_LANGUAGE,
- SearchQueryLanguage,
-} from '../../types/combined_query';
-
+ IndexPattern,
+ IndexPatternField,
+ TimeRange,
+} from '../../../../../../../../src/plugins/data/common';
+import { JobFieldType } from '../../../../../common/types';
+import { SearchQueryLanguage } from '../../types/combined_query';
+import { useDataVisualizerKibana } from '../../../kibana_context';
+import './_index.scss';
+import { createMergedEsQuery } from '../../utils/saved_search_utils';
interface Props {
indexPattern: IndexPattern;
searchString: Query['query'];
@@ -38,12 +39,15 @@ interface Props {
searchQuery,
searchString,
queryLanguage,
+ filters,
}: {
searchQuery: Query['query'];
searchString: Query['query'];
queryLanguage: SearchQueryLanguage;
+ filters: Filter[];
}): void;
showEmptyFields: boolean;
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
export const SearchPanel: FC = ({
@@ -61,98 +65,109 @@ export const SearchPanel: FC = ({
setSearchParams,
showEmptyFields,
}) => {
+ const {
+ services: {
+ uiSettings,
+ notifications: { toasts },
+ data: {
+ query: queryManager,
+ ui: { SearchBar },
+ },
+ },
+ } = useDataVisualizerKibana();
// The internal state of the input query bar updated on every key stroke.
const [searchInput, setSearchInput] = useState({
query: searchString || '',
language: searchQueryLanguage,
});
- const [errorMessage, setErrorMessage] = useState(undefined);
useEffect(() => {
setSearchInput({
query: searchString || '',
language: searchQueryLanguage,
});
- }, [searchQueryLanguage, searchString]);
+ }, [searchQueryLanguage, searchString, queryManager.filterManager]);
- const searchHandler = (query: Query) => {
- let filterQuery;
+ const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => {
+ const mergedQuery = query ?? searchInput;
+ const mergedFilters = filters ?? queryManager.filterManager.getFilters();
try {
- if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
- filterQuery = toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
- } else if (query.language === SEARCH_QUERY_LANGUAGE.LUCENE) {
- filterQuery = luceneStringToDsl(query.query);
- } else {
- filterQuery = {};
+ if (mergedFilters) {
+ queryManager.filterManager.setFilters(mergedFilters);
}
+
+ const combinedQuery = createMergedEsQuery(
+ mergedQuery,
+ queryManager.filterManager.getFilters() ?? [],
+ indexPattern,
+ uiSettings
+ );
+
setSearchParams({
- searchQuery: filterQuery,
- searchString: query.query,
- queryLanguage: query.language as SearchQueryLanguage,
+ searchQuery: combinedQuery,
+ searchString: mergedQuery.query,
+ queryLanguage: mergedQuery.language as SearchQueryLanguage,
+ filters: mergedFilters,
});
} catch (e) {
console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console
- setErrorMessage({ query: query.query as string, message: e.message });
+ toasts.addError(e, {
+ title: i18n.translate('xpack.dataVisualizer.searchPanel.invalidSyntax', {
+ defaultMessage: 'Invalid syntax',
+ }),
+ });
}
};
- const searchChangeHandler = (query: Query) => setSearchInput(query);
return (
-
-
- setErrorMessage(undefined)}
- input={
-
+
+
+
+ searchHandler({ query: params.query })
}
- isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''}
- >
-
- {i18n.translate(
- 'xpack.dataVisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar',
- {
- defaultMessage: 'Invalid query',
- }
- )}
- {': '}
- {errorMessage?.message.split('\n')[0]}
-
-
+ // @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
+ onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
+ indexPatterns={[indexPattern]}
+ placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', {
+ defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
+ })}
+ displayStyle={'inPage'}
+ isClearable={true}
+ customSubmitButton={
}
+ />
-
+
+
+
+
-
-
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
index f9748da51a22d..c3f3d744a3978 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
@@ -49,6 +49,7 @@ export const DataVisualizerUrlStateContextProvider: FC(
undefined
@@ -56,7 +57,6 @@ export const DataVisualizerUrlStateContextProvider: FC | null>(
null
);
- const { search: searchString } = useLocation();
useEffect(() => {
const prevSearchString = searchString;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/index.ts
similarity index 72%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx
rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/index.ts
index 4bff7f3b2ef5e..fb3e0100bbf75 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/index.ts
@@ -5,8 +5,4 @@
* 2.0.
*/
-import React from 'react';
-
-export const CurationsSettings: React.FC = () => {
- return null;
-};
+export * from './locator';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.test.ts
new file mode 100644
index 0000000000000..c8762aa79bbd5
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { IndexDataVisualizerLocatorDefinition } from './locator';
+
+describe('Index data visualizer locator', () => {
+ const definition = new IndexDataVisualizerLocatorDefinition();
+
+ it('should generate valid URL for the Index Data Visualizer Viewer page with global settings', async () => {
+ const location = await definition.getLocation({
+ indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
+ timeRange: {
+ from: 'now-30m',
+ to: 'now',
+ },
+ refreshInterval: { pause: false, value: 300 },
+ });
+
+ expect(location).toMatchObject({
+ app: 'ml',
+ path: '/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:())&_g=(refreshInterval:(pause:!f,value:300),time:(from:now-30m,to:now))',
+ state: {},
+ });
+ });
+
+ it('should prioritize savedSearchId even when index pattern id is available', async () => {
+ const location = await definition.getLocation({
+ indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
+ savedSearchId: '45014020-dffa-11eb-b120-a105fbbe93b3',
+ });
+
+ expect(location).toMatchObject({
+ app: 'ml',
+ path: '/jobs/new_job/datavisualizer?savedSearchId=45014020-dffa-11eb-b120-a105fbbe93b3&_a=(DATA_VISUALIZER_INDEX_VIEWER:())&_g=()',
+ state: {},
+ });
+ });
+
+ it('should generate valid URL with field names and field types', async () => {
+ const location = await definition.getLocation({
+ indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
+ visibleFieldNames: ['@timestamp', 'responsetime'],
+ visibleFieldTypes: ['number'],
+ });
+
+ expect(location).toMatchObject({
+ app: 'ml',
+ path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(visibleFieldNames:!('@timestamp',responsetime),visibleFieldTypes:!(number)))&_g=()",
+ });
+ });
+
+ it('should generate valid URL with KQL query', async () => {
+ const location = await definition.getLocation({
+ indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
+ query: {
+ searchQuery: {
+ bool: {
+ should: [
+ {
+ match: {
+ region: 'ap-northwest-1',
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ searchString: 'region : ap-northwest-1',
+ searchQueryLanguage: 'kuery',
+ },
+ });
+
+ expect(location).toMatchObject({
+ app: 'ml',
+ path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(searchQuery:(bool:(minimum_should_match:1,should:!((match:(region:ap-northwest-1))))),searchQueryLanguage:kuery,searchString:'region : ap-northwest-1'))&_g=()",
+ state: {},
+ });
+ });
+
+ it('should generate valid URL with Lucene query', async () => {
+ const location = await definition.getLocation({
+ indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
+ query: {
+ searchQuery: {
+ query_string: {
+ query: 'region: ap-northwest-1',
+ analyze_wildcard: true,
+ },
+ },
+ searchString: 'region : ap-northwest-1',
+ searchQueryLanguage: 'lucene',
+ },
+ });
+
+ expect(location).toMatchObject({
+ app: 'ml',
+ path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(searchQuery:(query_string:(analyze_wildcard:!t,query:'region: ap-northwest-1')),searchQueryLanguage:lucene,searchString:'region : ap-northwest-1'))&_g=()",
+ state: {},
+ });
+ });
+});
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts
new file mode 100644
index 0000000000000..c26a668bd04ab
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+// @ts-ignore
+import { encode } from 'rison-node';
+import { stringify } from 'query-string';
+import { SerializableRecord } from '@kbn/utility-types';
+import { RefreshInterval, TimeRange } from '../../../../../../../src/plugins/data/common';
+import { LocatorDefinition, LocatorPublic } from '../../../../../../../src/plugins/share/common';
+import { QueryState } from '../../../../../../../src/plugins/data/public';
+import { Dictionary, isRisonSerializationRequired } from '../../common/util/url_state';
+import { SearchQueryLanguage } from '../types/combined_query';
+
+export const DATA_VISUALIZER_APP_LOCATOR = 'DATA_VISUALIZER_APP_LOCATOR';
+
+export interface IndexDataVisualizerLocatorParams extends SerializableRecord {
+ /**
+ * Optionally set saved search ID.
+ */
+ savedSearchId?: string;
+
+ /**
+ * Optionally set index pattern ID.
+ */
+ indexPatternId?: string;
+
+ /**
+ * Optionally set the time range in the time picker.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Optionally set the refresh interval.
+ */
+ refreshInterval?: RefreshInterval & SerializableRecord;
+
+ /**
+ * Optionally set a query.
+ */
+ query?: {
+ searchQuery: SerializableRecord;
+ searchString: string | SerializableRecord;
+ searchQueryLanguage: SearchQueryLanguage;
+ };
+
+ /**
+ * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
+ * whether to hash the data in the url to avoid url length issues.
+ */
+ useHash?: boolean;
+ /**
+ * Optionally set visible field names.
+ */
+ visibleFieldNames?: string[];
+ /**
+ * Optionally set visible field types.
+ */
+ visibleFieldTypes?: string[];
+}
+
+export type IndexDataVisualizerLocator = LocatorPublic;
+
+export class IndexDataVisualizerLocatorDefinition
+ implements LocatorDefinition
+{
+ public readonly id = DATA_VISUALIZER_APP_LOCATOR;
+
+ constructor() {}
+
+ public readonly getLocation = async (params: IndexDataVisualizerLocatorParams) => {
+ const {
+ indexPatternId,
+ query,
+ refreshInterval,
+ savedSearchId,
+ timeRange,
+ visibleFieldNames,
+ visibleFieldTypes,
+ } = params;
+
+ const appState: {
+ searchQuery?: { [key: string]: any };
+ searchQueryLanguage?: string;
+ searchString?: string | SerializableRecord;
+ visibleFieldNames?: string[];
+ visibleFieldTypes?: string[];
+ } = {};
+ const queryState: QueryState = {};
+
+ if (query) {
+ appState.searchQuery = query.searchQuery;
+ appState.searchString = query.searchString;
+ appState.searchQueryLanguage = query.searchQueryLanguage;
+ }
+ if (visibleFieldNames) appState.visibleFieldNames = visibleFieldNames;
+ if (visibleFieldTypes) appState.visibleFieldTypes = visibleFieldTypes;
+
+ if (timeRange) queryState.time = timeRange;
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+
+ const urlState: Dictionary = {
+ ...(savedSearchId ? { savedSearchId } : { index: indexPatternId }),
+ _a: { DATA_VISUALIZER_INDEX_VIEWER: appState },
+ _g: queryState,
+ };
+
+ const parsedQueryString: Dictionary = {};
+ Object.keys(urlState).forEach((a) => {
+ if (isRisonSerializationRequired(a)) {
+ parsedQueryString[a] = encode(urlState[a]);
+ } else {
+ parsedQueryString[a] = urlState[a];
+ }
+ });
+ const newLocationSearchString = stringify(parsedQueryString, {
+ sort: false,
+ encode: false,
+ });
+
+ const path = `/jobs/new_job/datavisualizer?${newLocationSearchString}`;
+ return {
+ app: 'ml',
+ path,
+ state: {},
+ };
+ };
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts
index 7cd1c2bb3ce09..81c5a9097e0ef 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { Filter } from '@kbn/es-query';
import { Query } from '../../../../../../../src/plugins/data/common/query';
import { SearchQueryLanguage } from './combined_query';
@@ -25,4 +26,5 @@ export interface DataVisualizerIndexBasedAppState extends Omit50","language":"lucene"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
+ },
+ },
+ id: '93fc4d60-1c80-11ec-b1d7-f7e5cf21b9e0',
+ type: 'search',
+};
+
+// @ts-expect-error We don't need the full object here
+const luceneInvalidSavedSearchObj: SavedSearchSavedObject = {
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: null,
+ },
+ },
+ id: '93fc4d60-1c80-11ec-b1d7-f7e5cf21b9e0',
+ type: 'search',
+};
+
+const kqlSavedSearch: SavedSearch = {
+ title: 'farequote_filter_and_kuery',
+ description: '',
+ columns: ['_source'],
+ // @ts-expect-error We don't need the full object here
+ kibanaSavedObjectMeta: {
+ searchSourceJSON:
+ '{"highlightAll":true,"version":true,"query":{"query":"responsetime > 49","language":"kuery"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
+ },
+};
+
+describe('getQueryFromSavedSearch()', () => {
+ it('should return parsed searchSourceJSON with query and filter', () => {
+ expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({
+ filter: [
+ {
+ $state: { store: 'appState' },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
+ key: 'airline',
+ negate: false,
+ params: { query: 'ASA', type: 'phrase' },
+ type: 'phrase',
+ value: 'ASA',
+ },
+ query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
+ },
+ ],
+ highlightAll: true,
+ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ query: { language: 'lucene', query: 'responsetime:>50' },
+ version: true,
+ });
+ expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({
+ filter: [
+ {
+ $state: { store: 'appState' },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
+ key: 'airline',
+ negate: false,
+ params: { query: 'ASA', type: 'phrase' },
+ type: 'phrase',
+ value: 'ASA',
+ },
+ query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
+ },
+ ],
+ highlightAll: true,
+ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ query: { language: 'kuery', query: 'responsetime > 49' },
+ version: true,
+ });
+ });
+ it('should return undefined if invalid searchSourceJSON', () => {
+ expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined);
+ });
+});
+
+describe('createMergedEsQuery()', () => {
+ const luceneQuery = {
+ query: 'responsetime:>50',
+ language: 'lucene',
+ };
+ const kqlQuery = {
+ query: 'responsetime > 49',
+ language: 'kuery',
+ };
+ const mockFilters: Filter[] = [
+ {
+ meta: {
+ index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
+ negate: false,
+ disabled: false,
+ alias: null,
+ type: 'phrase',
+ key: 'airline',
+ params: {
+ query: 'ASA',
+ },
+ },
+ query: {
+ match: {
+ airline: {
+ query: 'ASA',
+ type: 'phrase',
+ },
+ },
+ },
+ $state: {
+ store: 'appState' as FilterStateStore,
+ },
+ },
+ ];
+
+ it('return formatted ES bool query with both the original query and filters combined', () => {
+ expect(createMergedEsQuery(luceneQuery, mockFilters)).toEqual({
+ bool: {
+ filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
+ must: [{ query_string: { query: 'responsetime:>50' } }],
+ must_not: [],
+ should: [],
+ },
+ });
+ expect(createMergedEsQuery(kqlQuery, mockFilters)).toEqual({
+ bool: {
+ filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
+ minimum_should_match: 1,
+ must_not: [],
+ should: [{ range: { responsetime: { gt: '49' } } }],
+ },
+ });
+ });
+ it('return formatted ES bool query without filters ', () => {
+ expect(createMergedEsQuery(luceneQuery)).toEqual({
+ bool: {
+ filter: [],
+ must: [{ query_string: { query: 'responsetime:>50' } }],
+ must_not: [],
+ should: [],
+ },
+ });
+ expect(createMergedEsQuery(kqlQuery)).toEqual({
+ bool: {
+ filter: [],
+ minimum_should_match: 1,
+ must_not: [],
+ should: [{ range: { responsetime: { gt: '49' } } }],
+ },
+ });
+ });
+});
+
+describe('getEsQueryFromSavedSearch()', () => {
+ it('return undefined if saved search is not provided', () => {
+ expect(
+ getEsQueryFromSavedSearch({
+ indexPattern: mockDataView,
+ savedSearch: undefined,
+ uiSettings: mockUiSettings,
+ })
+ ).toEqual(undefined);
+ });
+ it('return search data from saved search if neither query nor filter is provided ', () => {
+ expect(
+ getEsQueryFromSavedSearch({
+ indexPattern: mockDataView,
+ savedSearch: luceneSavedSearchObj,
+ uiSettings: mockUiSettings,
+ })
+ ).toEqual({
+ queryLanguage: 'lucene',
+ searchQuery: {
+ bool: {
+ filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
+ must: [{ query_string: { query: 'responsetime:>50' } }],
+ must_not: [],
+ should: [],
+ },
+ },
+ searchString: 'responsetime:>50',
+ });
+ });
+ it('should override original saved search with the provided query ', () => {
+ expect(
+ getEsQueryFromSavedSearch({
+ indexPattern: mockDataView,
+ savedSearch: luceneSavedSearchObj,
+ uiSettings: mockUiSettings,
+ query: {
+ query: 'responsetime:>100',
+ language: 'lucene',
+ },
+ })
+ ).toEqual({
+ queryLanguage: 'lucene',
+ searchQuery: {
+ bool: {
+ filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
+ must: [{ query_string: { query: 'responsetime:>100' } }],
+ must_not: [],
+ should: [],
+ },
+ },
+ searchString: 'responsetime:>100',
+ });
+ });
+
+ it('should override original saved search with the provided filters ', () => {
+ expect(
+ getEsQueryFromSavedSearch({
+ indexPattern: mockDataView,
+ savedSearch: luceneSavedSearchObj,
+ uiSettings: mockUiSettings,
+ query: {
+ query: 'responsetime:>100',
+ language: 'lucene',
+ },
+ filters: [
+ {
+ meta: {
+ index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
+ alias: null,
+ negate: true,
+ disabled: false,
+ type: 'phrase',
+ key: 'airline',
+ params: {
+ query: 'JZA',
+ },
+ },
+ query: {
+ match_phrase: {
+ airline: 'JZA',
+ },
+ },
+ $state: {
+ store: 'appState' as FilterStateStore,
+ },
+ },
+ ],
+ })
+ ).toEqual({
+ queryLanguage: 'lucene',
+ searchQuery: {
+ bool: {
+ filter: [],
+ must: [{ query_string: { query: 'responsetime:>100' } }],
+ must_not: [{ match_phrase: { airline: 'JZA' } }],
+ should: [],
+ },
+ },
+ searchString: 'responsetime:>100',
+ });
+ });
+});
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
index cb80e491fc7e5..80a2069aab1a8 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
@@ -8,55 +8,155 @@
import { cloneDeep } from 'lodash';
import { IUiSettingsClient } from 'kibana/public';
import {
- buildEsQuery,
- buildQueryFromFilters,
- decorateQuery,
fromKueryExpression,
- luceneStringToDsl,
toElasticsearchQuery,
+ buildQueryFromFilters,
+ buildEsQuery,
+ Query,
+ Filter,
} from '@kbn/es-query';
-import { estypes } from '@elastic/elasticsearch';
-import { SavedSearchSavedObject } from '../../../../common/types';
+import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
-import { getEsQueryConfig, Query } from '../../../../../../../src/plugins/data/public';
-
-export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) {
- const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string };
- return JSON.parse(search.searchSourceJSON) as {
- query: Query;
- filter: any[];
- };
+import { SavedSearch } from '../../../../../../../src/plugins/discover/public';
+import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
+import { FilterManager } from '../../../../../../../src/plugins/data/public';
+
+/**
+ * Parse the stringified searchSourceJSON
+ * from a saved search or saved search object
+ */
+export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) {
+ const search = isSavedSearchSavedObject(savedSearch)
+ ? savedSearch?.attributes?.kibanaSavedObjectMeta
+ : // @ts-expect-error kibanaSavedObjectMeta does exist
+ savedSearch?.kibanaSavedObjectMeta;
+
+ const parsed =
+ typeof search?.searchSourceJSON === 'string'
+ ? (JSON.parse(search.searchSourceJSON) as {
+ query: Query;
+ filter: Filter[];
+ })
+ : undefined;
+
+ // Remove indexRefName because saved search might no longer be relevant
+ // if user modifies the query or filter
+ // after opening a saved search
+ if (parsed && Array.isArray(parsed.filter)) {
+ parsed.filter.forEach((f) => {
+ // @ts-expect-error indexRefName does appear in meta for newly created saved search
+ f.meta.indexRefName = undefined;
+ });
+ }
+ return parsed;
}
/**
- * Extract query data from the saved search object.
+ * Create an Elasticsearch query that combines both lucene/kql query string and filters
+ * Should also form a valid query if only the query or filters is provided
*/
-export function extractSearchData(
- savedSearch: SavedSearchSavedObject | null,
- currentIndexPattern: IndexPattern,
- queryStringOptions: Record | string
+export function createMergedEsQuery(
+ query?: Query,
+ filters?: Filter[],
+ indexPattern?: IndexPattern,
+ uiSettings?: IUiSettingsClient
) {
- if (!savedSearch) {
- return undefined;
- }
+ let combinedQuery: any = getDefaultQuery();
+
+ if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
+ const ast = fromKueryExpression(query.query);
+ if (query.query !== '') {
+ combinedQuery = toElasticsearchQuery(ast, indexPattern);
+ }
+ const filterQuery = buildQueryFromFilters(filters, indexPattern);
+
+ if (Array.isArray(combinedQuery.bool.filter) === false) {
+ combinedQuery.bool.filter =
+ combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
+ }
+
+ if (Array.isArray(combinedQuery.bool.must_not) === false) {
+ combinedQuery.bool.must_not =
+ combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
+ }
- const { query: extractedQuery } = getQueryFromSavedSearch(savedSearch);
- const queryLanguage = extractedQuery.language as SearchQueryLanguage;
- const qryString = extractedQuery.query;
- let qry;
- if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {
- const ast = fromKueryExpression(qryString);
- qry = toElasticsearchQuery(ast, currentIndexPattern);
+ combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
+ combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
} else {
- qry = luceneStringToDsl(qryString);
- decorateQuery(qry, queryStringOptions);
+ combinedQuery = buildEsQuery(
+ indexPattern,
+ query ? [query] : [],
+ filters ? filters : [],
+ uiSettings ? getEsQueryConfig(uiSettings) : undefined
+ );
+ }
+ return combinedQuery;
+}
+
+/**
+ * Extract query data from the saved search object
+ * with overrides from the provided query data and/or filters
+ */
+export function getEsQueryFromSavedSearch({
+ indexPattern,
+ uiSettings,
+ savedSearch,
+ query,
+ filters,
+ filterManager,
+}: {
+ indexPattern: IndexPattern;
+ uiSettings: IUiSettingsClient;
+ savedSearch: SavedSearchSavedObject | SavedSearch | null | undefined;
+ query?: Query;
+ filters?: Filter[];
+ filterManager?: FilterManager;
+}) {
+ if (!indexPattern || !savedSearch) return;
+
+ const savedSearchData = getQueryFromSavedSearch(savedSearch);
+ const userQuery = query;
+ const userFilters = filters;
+
+ // If no saved search available, use user's query and filters
+ if (!savedSearchData && userQuery) {
+ if (filterManager && userFilters) filterManager.setFilters(userFilters);
+
+ const combinedQuery = createMergedEsQuery(
+ userQuery,
+ Array.isArray(userFilters) ? userFilters : [],
+ indexPattern,
+ uiSettings
+ );
+
+ return {
+ searchQuery: combinedQuery,
+ searchString: userQuery.query,
+ queryLanguage: userQuery.language as SearchQueryLanguage,
+ };
+ }
+
+ // If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
+ if (savedSearchData) {
+ const currentQuery = userQuery ?? savedSearchData?.query;
+ const currentFilters = userFilters ?? savedSearchData?.filter;
+
+ if (filterManager) filterManager.setFilters(currentFilters);
+
+ const combinedQuery = createMergedEsQuery(
+ currentQuery,
+ Array.isArray(currentFilters) ? currentFilters : [],
+ indexPattern,
+ uiSettings
+ );
+
+ return {
+ searchQuery: combinedQuery,
+ searchString: currentQuery.query,
+ queryLanguage: currentQuery.language as SearchQueryLanguage,
+ };
}
- return {
- searchQuery: qry,
- searchString: qryString,
- queryLanguage,
- };
}
const DEFAULT_QUERY = {
@@ -69,64 +169,6 @@ const DEFAULT_QUERY = {
},
};
-export function getDefaultDatafeedQuery() {
+export function getDefaultQuery() {
return cloneDeep(DEFAULT_QUERY);
}
-
-export function createSearchItems(
- kibanaConfig: IUiSettingsClient,
- indexPattern: IndexPattern | undefined,
- savedSearch: SavedSearchSavedObject | null
-) {
- // query is only used by the data visualizer as it needs
- // a lucene query_string.
- // Using a blank query will cause match_all:{} to be used
- // when passed through luceneStringToDsl
- let query: Query = {
- query: '',
- language: 'lucene',
- };
-
- let combinedQuery: estypes.QueryDslQueryContainer = getDefaultDatafeedQuery();
- if (savedSearch !== null) {
- const data = getQueryFromSavedSearch(savedSearch);
-
- query = data.query;
- const filter = data.filter;
-
- const filters = Array.isArray(filter) ? filter : [];
-
- if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
- const ast = fromKueryExpression(query.query);
- if (query.query !== '') {
- combinedQuery = toElasticsearchQuery(ast, indexPattern);
- }
- const filterQuery = buildQueryFromFilters(filters, indexPattern);
-
- if (!combinedQuery.bool) {
- throw new Error('Missing bool on query');
- }
-
- if (!Array.isArray(combinedQuery.bool.filter)) {
- combinedQuery.bool.filter =
- combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
- }
-
- if (!Array.isArray(combinedQuery.bool.must_not)) {
- combinedQuery.bool.must_not =
- combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
- }
-
- combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
- combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
- } else {
- const esQueryConfigs = getEsQueryConfig(kibanaConfig);
- combinedQuery = buildEsQuery(indexPattern, [query], filters, esQueryConfigs);
- }
- }
-
- return {
- query,
- combinedQuery,
- };
-}
diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts
index 54f27a2e9d72e..112294f4b246f 100644
--- a/x-pack/plugins/data_visualizer/public/plugin.ts
+++ b/x-pack/plugins/data_visualizer/public/plugin.ts
@@ -48,7 +48,10 @@ export class DataVisualizerPlugin
DataVisualizerStartDependencies
>
{
- public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) {
+ public setup(
+ core: CoreSetup,
+ plugins: DataVisualizerSetupDependencies
+ ) {
if (plugins.home) {
registerHomeAddData(plugins.home);
registerHomeFeatureCatalogue(plugins.home);
diff --git a/x-pack/plugins/enterprise_search/jest.config.js b/x-pack/plugins/enterprise_search/jest.config.js
index 263713697b7e0..b5e6105ff41f2 100644
--- a/x-pack/plugins/enterprise_search/jest.config.js
+++ b/x-pack/plugins/enterprise_search/jest.config.js
@@ -18,4 +18,8 @@ module.exports = {
'!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}',
],
coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search',
+ modulePathIgnorePatterns: [
+ '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress',
+ '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress',
+ ],
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.tsx
new file mode 100644
index 0000000000000..c9be6e609f37c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AutomatedIcon } from './automated_icon';
+
+describe('AutomatedIcon', () => {
+ it('renders', () => {
+ expect(shallow( ).is('svg')).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx
new file mode 100644
index 0000000000000..d50cf101e6059
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/automated_icon.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+export const AutomatedIcon: React.FC = ({ ...props }) => (
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
index a241edb8020a4..9598212d3e0c9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
@@ -19,6 +19,6 @@ describe('CurationsRouter', () => {
const wrapper = shallow( );
expect(wrapper.find(Switch)).toHaveLength(1);
- expect(wrapper.find(Route)).toHaveLength(3);
+ expect(wrapper.find(Route)).toHaveLength(4);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
index 40f2d07ab61ab..693e5406b714b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
@@ -12,10 +12,11 @@ import {
ENGINE_CURATIONS_PATH,
ENGINE_CURATIONS_NEW_PATH,
ENGINE_CURATION_PATH,
+ ENGINE_CURATION_SUGGESTION_PATH,
} from '../../routes';
import { Curation } from './curation';
-import { Curations, CurationCreation } from './views';
+import { Curations, CurationCreation, CurationSuggestion } from './views';
export const CurationsRouter: React.FC = () => {
return (
@@ -26,6 +27,9 @@ export const CurationsRouter: React.FC = () => {
+
+
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.test.tsx
new file mode 100644
index 0000000000000..4bd586d9d2e91
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.test.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { CurationActionBar } from './curation_action_bar';
+
+describe('CurationActionBar', () => {
+ const handleAcceptClick = jest.fn();
+ const handleRejectClick = jest.fn();
+
+ it('renders', () => {
+ const wrapper = shallow(
+
+ );
+
+ wrapper.find('[data-test-subj="rejectButton"]').simulate('click');
+ expect(handleRejectClick).toHaveBeenCalled();
+
+ wrapper.find('[data-test-subj="acceptButton"]').simulate('click');
+ expect(handleAcceptClick).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.tsx
new file mode 100644
index 0000000000000..42f4cbbb7d7a9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_action_bar.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { CurationActionsPopover } from './curation_actions_popover';
+
+interface Props {
+ onAcceptClick: (event: React.MouseEvent) => void;
+ onRejectClick: (event: React.MouseEvent) => void;
+}
+
+export const CurationActionBar: React.FC = ({ onAcceptClick, onRejectClick }) => {
+ return (
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.title',
+ { defaultMessage: 'Manage suggestion' }
+ )}
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.rejectButtonLabel',
+ { defaultMessage: 'Reject' }
+ )}
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.acceptButtonLabel',
+ { defaultMessage: 'Accept' }
+ )}
+
+
+
+ {}}
+ onAutomate={() => {}}
+ onReject={() => {}}
+ onTurnOff={() => {}}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.test.tsx
new file mode 100644
index 0000000000000..33d00ca2b7899
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { CurationActionsPopover } from './curation_actions_popover';
+
+describe('CurationActionsPopover', () => {
+ const handleAccept = jest.fn();
+ const handleAutomate = jest.fn();
+ const handleReject = jest.fn();
+ const handleTurnOff = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.isEmptyRender()).toBe(false);
+
+ wrapper.find('[data-test-subj="acceptButton"]').simulate('click');
+ expect(handleAccept).toHaveBeenCalled();
+
+ wrapper.find('[data-test-subj="automateButton"]').simulate('click');
+ expect(handleAutomate).toHaveBeenCalled();
+
+ wrapper.find('[data-test-subj="rejectButton"]').simulate('click');
+ expect(handleReject).toHaveBeenCalled();
+
+ wrapper.find('[data-test-subj="turnoffButton"]').simulate('click');
+ expect(handleTurnOff).toHaveBeenCalled();
+ });
+
+ it('can open and close', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.prop('isOpen')).toBe(false);
+
+ const button = shallow(wrapper.prop('button'));
+ button.simulate('click');
+
+ expect(wrapper.prop('isOpen')).toBe(true);
+
+ wrapper.prop('closePopover')();
+
+ expect(wrapper.prop('isOpen')).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.tsx
new file mode 100644
index 0000000000000..ef7b42fb705f1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_actions_popover.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+
+import {
+ EuiButtonIcon,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiPopover,
+ EuiPopoverTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface Props {
+ onAccept: () => void;
+ onAutomate: () => void;
+ onReject: () => void;
+ onTurnOff: () => void;
+}
+
+export const CurationActionsPopover: React.FC = ({
+ onAccept,
+ onAutomate,
+ onReject,
+ onTurnOff,
+}) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen);
+ const closePopover = () => setIsPopoverOpen(false);
+
+ const button = (
+
+ );
+ return (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.actionsPopoverTitle',
+ {
+ defaultMessage: 'Manage suggestion',
+ }
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.scss
new file mode 100644
index 0000000000000..1fa988014edb7
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.scss
@@ -0,0 +1,24 @@
+.curationResultPanel {
+ border-radius: $euiSizeM;
+ margin-top: $euiSizeS;
+ padding: $euiSizeXS;
+}
+
+.curationResultPanel--current, .curationResultPanel--promoted {
+ border: $euiBorderWidthThick solid $euiColorPrimary;
+ background-color: tintOrShade($euiColorPrimary, 90%, 70%); // Copied from @elastit/eui/src/global_styling/variables/_panels.scss
+}
+
+.curationResultPanel--suggested {
+ border: $euiBorderWidthThick solid $euiColorSecondary;
+ background-color: tintOrShade($euiColorSuccess, 90%, 70%); // Copied from @elastit/eui/src/global_styling/variables/_panels.scss
+}
+
+.curationResultPanel--hidden {
+ border: $euiBorderWidthThick solid $euiColorAccent;
+ background-color: tintOrShade($euiColorAccent, 90%, 70%); // Copied from @elastit/eui/src/global_styling/variables/_panels.scss
+}
+
+.curationResultPanel__header {
+ flex-grow: 0;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx
new file mode 100644
index 0000000000000..fad4e54721bb3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.test.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { Result } from '../../../result';
+
+import { CurationResultPanel } from './curation_result_panel';
+
+describe('CurationResultPanel', () => {
+ const results = [
+ {
+ id: { raw: 'foo' },
+ _meta: { engine: 'some-engine', id: 'foo' },
+ },
+ {
+ id: { raw: 'bar' },
+ _meta: { engine: 'some-engine', id: 'bar' },
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders results', () => {
+ const wrapper = shallow( );
+ expect(wrapper.find('[data-test-subj="suggestedText"]').exists()).toBe(false);
+ expect(wrapper.find(Result).length).toBe(2);
+ });
+
+ it('renders a no results message when there are no results', () => {
+ const wrapper = shallow( );
+ expect(wrapper.find('[data-test-subj="noResults"]').exists()).toBe(true);
+ expect(wrapper.find(Result).length).toBe(0);
+ });
+
+ it('shows text about automation when variant is "suggested"', () => {
+ const wrapper = shallow( );
+ expect(wrapper.find('[data-test-subj="suggestedText"]').exists()).toBe(true);
+ });
+
+ it('renders the right class name for the provided variant', () => {
+ const wrapper = shallow( );
+ expect(wrapper.find('.curationResultPanel--promoted').exists()).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx
new file mode 100644
index 0000000000000..12bbf07f97bb3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_result_panel.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiNotificationBadge,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { Result } from '../../../result';
+import { Result as ResultType } from '../../../result/types';
+import './curation_result_panel.scss';
+
+interface Props {
+ variant: 'current' | 'promoted' | 'suggested' | 'hidden';
+ results: ResultType[];
+}
+
+export const CurationResultPanel: React.FC = ({ variant, results }) => {
+ // TODO wire up
+ const count = 3;
+
+ return (
+ <>
+
+
+ {count}
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.resultPanelTitle',
+ { defaultMessage: 'Promoted results' }
+ )}
+
+
+
+ {variant === 'suggested' && (
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.resultPanelDescription',
+ { defaultMessage: 'This curation can be automated by App Search' }
+ )}
+
+
+
+ )}
+
+
+ 0 ? 'flexStart' : 'center'}
+ gutterSize="s"
+ direction="column"
+ className={`curationResultPanel curationResultPanel--${variant}`}
+ >
+ {results.length > 0 ? (
+ results.map((result) => (
+
+
+
+ ))
+ ) : (
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.suggestedCuration.noResultsMessage',
+ { defaultMessage: 'There are currently no promoted documents for this query' }
+ )}
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx
new file mode 100644
index 0000000000000..9bfc12dfe7cc2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.test.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mockUseParams } from '../../../../../__mocks__/react_router';
+import '../../../../__mocks__/engine_logic.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AppSearchPageTemplate } from '../../../layout';
+
+import { CurationSuggestion } from './curation_suggestion';
+
+describe('CurationSuggestion', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseParams.mockReturnValue({ query: 'some%20query' });
+ });
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.is(AppSearchPageTemplate)).toBe(true);
+ });
+
+ it('displays the decoded query in the title', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageHeader').pageTitle).toEqual('some query');
+ });
+
+ // TODO This will need to come from somewhere else when wired up
+ it('displays an empty query if "" is encoded in as the qery', () => {
+ mockUseParams.mockReturnValue({ query: '%22%22' });
+
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageHeader').pageTitle).toEqual('""');
+ });
+
+ it('displays has a button to display organic results', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find('[data-test-subj="organicResults"]').exists()).toBe(false);
+ wrapper.find('[data-test-subj="showOrganicResults"]').simulate('click');
+ expect(wrapper.find('[data-test-subj="organicResults"]').exists()).toBe(true);
+ wrapper.find('[data-test-subj="showOrganicResults"]').simulate('click');
+ expect(wrapper.find('[data-test-subj="organicResults"]').exists()).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx
new file mode 100644
index 0000000000000..4fab9db47af90
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx
@@ -0,0 +1,120 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+
+import {
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiPanel,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { useDecodedParams } from '../../../../utils/encode_path_params';
+import { AppSearchPageTemplate } from '../../../layout';
+import { Result } from '../../../result';
+import { Result as ResultType } from '../../../result/types';
+import { getCurationsBreadcrumbs } from '../../utils';
+
+import { CurationActionBar } from './curation_action_bar';
+import { CurationResultPanel } from './curation_result_panel';
+
+import { DATA } from './temp_data';
+
+export const CurationSuggestion: React.FC = () => {
+ const { query } = useDecodedParams();
+ const [showOrganicResults, setShowOrganicResults] = useState(false);
+ const currentOrganicResults = [...DATA].splice(5, 4);
+ const proposedOrganicResults = [...DATA].splice(2, 4);
+
+ const queryTitle = query === '""' ? query : `${query}`;
+
+ return (
+
+ alert('Accepted')}
+ onRejectClick={() => alert('Rejected')}
+ />
+
+
+
+
+ Current
+
+
+
+
+
+
+ Suggested
+
+
+
+
+
+
+
+ setShowOrganicResults(!showOrganicResults)}
+ data-test-subj="showOrganicResults"
+ >
+ {showOrganicResults ? 'Collapse' : 'Expand'} organic search results
+
+ {showOrganicResults && (
+ <>
+
+
+
+
+ {currentOrganicResults.length > 0 && (
+
+ {currentOrganicResults.map((result: ResultType) => (
+
+
+
+ ))}
+
+ )}
+
+
+ {proposedOrganicResults.length > 0 && (
+
+ {proposedOrganicResults.map((result: ResultType) => (
+
+
+
+ ))}
+
+ )}
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/index.ts
new file mode 100644
index 0000000000000..9cb1809e20442
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { CurationSuggestion } from './curation_suggestion';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/temp_data.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/temp_data.ts
new file mode 100644
index 0000000000000..83bbc977427a9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/temp_data.ts
@@ -0,0 +1,470 @@
+/*
+ * 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 { Result } from '../../../result/types';
+
+export const DATA: Result[] = [
+ {
+ visitors: {
+ raw: 5028868.0,
+ },
+ square_km: {
+ raw: 3082.7,
+ },
+ world_heritage_site: {
+ raw: 'true',
+ snippet: 'true',
+ },
+ date_established: {
+ raw: '1890-10-01T05:00:00+00:00',
+ },
+ description: {
+ raw: "Yosemite features sheer granite cliffs, exceptionally tall waterfalls, and old-growth forests at a unique intersection of geology and hydrology. Half Dome and El Capitan rise from the park's centerpiece, the glacier-carved Yosemite Valley, and from its vertical walls drop Yosemite Falls, one of North America's tallest waterfalls at 2,425 feet (739 m) high. Three giant sequoia groves, along with a pristine wilderness in the heart of the Sierra Nevada, are home to a wide variety of rare plant and animal species.",
+ snippet:
+ 'Yosemite features sheer granite cliffs, exceptionally tall waterfalls, and old-growth forests ',
+ },
+ location: {
+ raw: '37.83,-119.5',
+ },
+ acres: {
+ raw: 761747.5,
+ },
+ title: {
+ raw: 'Yosemite',
+ snippet: 'Yosemite',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/yose/index.htm',
+ snippet: 'https://www.nps.gov/yose/index.htm',
+ },
+ states: {
+ raw: ['California'],
+ snippet: 'California',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 7543305.0,
+ id: 'park_yosemite',
+ },
+ id: {
+ raw: 'park_yosemite',
+ },
+ },
+ {
+ visitors: {
+ raw: 4517585.0,
+ },
+ square_km: {
+ raw: 1075.6,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1915-01-26T06:00:00+00:00',
+ },
+ description: {
+ raw: 'Bisected north to south by the Continental Divide, this portion of the Rockies has ecosystems varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra. Wildlife including mule deer, bighorn sheep, black bears, and cougars inhabit its igneous mountains and glacial valleys. Longs Peak, a classic Colorado fourteener, and the scenic Bear Lake are popular destinations, as well as the historic Trail Ridge Road, which reaches an elevation of more than 12,000 feet (3,700 m).',
+ snippet:
+ ' varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra. Wildlife',
+ },
+ location: {
+ raw: '40.4,-105.58',
+ },
+ acres: {
+ raw: 265795.2,
+ },
+ title: {
+ raw: 'Rocky Mountain',
+ snippet: 'Rocky Mountain',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/romo/index.htm',
+ snippet: 'https://www.nps.gov/romo/index.htm',
+ },
+ states: {
+ raw: ['Colorado'],
+ snippet: 'Colorado',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 6776380.0,
+ id: 'park_rocky-mountain',
+ },
+ id: {
+ raw: 'park_rocky-mountain',
+ },
+ },
+ {
+ visitors: {
+ raw: 4295127.0,
+ },
+ square_km: {
+ raw: 595.8,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1919-11-19T06:00:00+00:00',
+ },
+ description: {
+ raw: 'Located at the junction of the Colorado Plateau, Great Basin, and Mojave Desert, this park contains sandstone features such as mesas, rock towers, and canyons, including the Virgin River Narrows. The various sandstone formations and the forks of the Virgin River create a wilderness divided into four ecosystems: desert, riparian, woodland, and coniferous forest.',
+ snippet: ' into four ecosystems: desert, riparian, woodland, and coniferous forest .',
+ },
+ location: {
+ raw: '37.3,-113.05',
+ },
+ acres: {
+ raw: 147237.02,
+ },
+ title: {
+ raw: 'Zion',
+ snippet: 'Zion',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/zion/index.htm',
+ snippet: 'https://www.nps.gov/zion/index.htm',
+ },
+ states: {
+ raw: ['Utah'],
+ snippet: 'Utah',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 6442695.0,
+ id: 'park_zion',
+ },
+ id: {
+ raw: 'park_zion',
+ },
+ },
+ {
+ visitors: {
+ raw: 3303393.0,
+ },
+ square_km: {
+ raw: 198.5,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1919-02-26T06:00:00+00:00',
+ },
+ description: {
+ raw: 'Covering most of Mount Desert Island and other coastal islands, Acadia features the tallest mountain on the Atlantic coast of the United States, granite peaks, ocean shoreline, woodlands, and lakes. There are freshwater, estuary, forest, and intertidal habitats.',
+ snippet:
+ ' mountain on the Atlantic coast of the United States, granite peaks, ocean shoreline, woodlands, and lakes. There are freshwater, estuary, forest , and intertidal habitats.',
+ },
+ location: {
+ raw: '44.35,-68.21',
+ },
+ acres: {
+ raw: 49057.36,
+ },
+ title: {
+ raw: 'Acadia',
+ snippet: 'Acadia',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/acad/index.htm',
+ snippet: 'https://www.nps.gov/acad/index.htm',
+ },
+ states: {
+ raw: ['Maine'],
+ snippet: 'Maine',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 4955094.5,
+ id: 'park_acadia',
+ },
+ id: {
+ raw: 'park_acadia',
+ },
+ },
+ {
+ visitors: {
+ raw: 1887580.0,
+ },
+ square_km: {
+ raw: 1308.9,
+ },
+ world_heritage_site: {
+ raw: 'true',
+ snippet: 'true',
+ },
+ date_established: {
+ raw: '1916-08-01T05:00:00+00:00',
+ },
+ description: {
+ raw: "This park on the Big Island protects the Kīlauea and Mauna Loa volcanoes, two of the world's most active geological features. Diverse ecosystems range from tropical forests at sea level to barren lava beds at more than 13,000 feet (4,000 m).",
+ snippet:
+ ' active geological features. Diverse ecosystems range from tropical forests at sea level to barren lava beds at more than 13,000 feet (4,000 m).',
+ },
+ location: {
+ raw: '19.38,-155.2',
+ },
+ acres: {
+ raw: 323431.38,
+ },
+ title: {
+ raw: 'Hawaii Volcanoes',
+ snippet: 'Hawaii Volcanoes',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/havo/index.htm',
+ snippet: 'https://www.nps.gov/havo/index.htm',
+ },
+ states: {
+ raw: ['Hawaii'],
+ snippet: 'Hawaii',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 2831373.2,
+ id: 'park_hawaii-volcanoes',
+ },
+ id: {
+ raw: 'park_hawaii-volcanoes',
+ },
+ },
+ {
+ visitors: {
+ raw: 1437341.0,
+ },
+ square_km: {
+ raw: 806.1,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1935-12-26T06:00:00+00:00',
+ },
+ description: {
+ raw: "Shenandoah's Blue Ridge Mountains are covered by hardwood forests that teem with a wide variety of wildlife. The Skyline Drive and Appalachian Trail run the entire length of this narrow park, along with more than 500 miles (800 km) of hiking trails passing scenic overlooks and cataracts of the Shenandoah River.",
+ snippet:
+ 'Shenandoah's Blue Ridge Mountains are covered by hardwood forests that teem with a wide variety',
+ },
+ location: {
+ raw: '38.53,-78.35',
+ },
+ acres: {
+ raw: 199195.27,
+ },
+ title: {
+ raw: 'Shenandoah',
+ snippet: 'Shenandoah',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/shen/index.htm',
+ snippet: 'https://www.nps.gov/shen/index.htm',
+ },
+ states: {
+ raw: ['Virginia'],
+ snippet: 'Virginia',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 2156015.5,
+ id: 'park_shenandoah',
+ },
+ id: {
+ raw: 'park_shenandoah',
+ },
+ },
+ {
+ visitors: {
+ raw: 1356913.0,
+ },
+ square_km: {
+ raw: 956.6,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1899-03-02T06:00:00+00:00',
+ },
+ description: {
+ raw: 'Mount Rainier, an active stratovolcano, is the most prominent peak in the Cascades and is covered by 26 named glaciers including Carbon Glacier and Emmons Glacier, the largest in the contiguous United States. The mountain is popular for climbing, and more than half of the park is covered by subalpine and alpine forests and meadows seasonally in bloom with wildflowers. Paradise on the south slope is the snowiest place on Earth where snowfall is measured regularly. The Longmire visitor center is the start of the Wonderland Trail, which encircles the mountain.',
+ snippet:
+ ' by subalpine and alpine forests and meadows seasonally in bloom with wildflowers. Paradise on the south slope',
+ },
+ location: {
+ raw: '46.85,-121.75',
+ },
+ acres: {
+ raw: 236381.64,
+ },
+ title: {
+ raw: 'Mount Rainier',
+ snippet: 'Mount Rainier',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/mora/index.htm',
+ snippet: 'https://www.nps.gov/mora/index.htm',
+ },
+ states: {
+ raw: ['Washington'],
+ snippet: 'Washington',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 2035372.0,
+ id: 'park_mount-rainier',
+ },
+ id: {
+ raw: 'park_mount-rainier',
+ },
+ },
+ {
+ visitors: {
+ raw: 1254688.0,
+ },
+ square_km: {
+ raw: 1635.2,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1890-09-25T05:00:00+00:00',
+ },
+ description: {
+ raw: "This park protects the Giant Forest, which boasts some of the world's largest trees, the General Sherman being the largest measured tree in the park. Other features include over 240 caves, a long segment of the Sierra Nevada including the tallest mountain in the contiguous United States, and Moro Rock, a large granite dome.",
+ snippet:
+ 'This park protects the Giant Forest , which boasts some of the world's largest trees, the General',
+ },
+ location: {
+ raw: '36.43,-118.68',
+ },
+ acres: {
+ raw: 404062.63,
+ },
+ title: {
+ raw: 'Sequoia',
+ snippet: 'Sequoia',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/seki/index.htm',
+ snippet: 'https://www.nps.gov/seki/index.htm',
+ },
+ states: {
+ raw: ['California'],
+ snippet: 'California',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 1882038.0,
+ id: 'park_sequoia',
+ },
+ id: {
+ raw: 'park_sequoia',
+ },
+ },
+ {
+ visitors: {
+ raw: 643274.0,
+ },
+ square_km: {
+ raw: 896.0,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1962-12-09T06:00:00+00:00',
+ },
+ description: {
+ raw: 'This portion of the Chinle Formation has a large concentration of 225-million-year-old petrified wood. The surrounding Painted Desert features eroded cliffs of red-hued volcanic rock called bentonite. Dinosaur fossils and over 350 Native American sites are also protected in this park.',
+ snippet:
+ 'This portion of the Chinle Formation has a large concentration of 225-million-year-old petrified',
+ },
+ location: {
+ raw: '35.07,-109.78',
+ },
+ acres: {
+ raw: 221415.77,
+ },
+ title: {
+ raw: 'Petrified Forest',
+ snippet: 'Petrified Forest ',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/pefo/index.htm',
+ snippet: 'https://www.nps.gov/pefo/index.htm',
+ },
+ states: {
+ raw: ['Arizona'],
+ snippet: 'Arizona',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 964919.94,
+ id: 'park_petrified-forest',
+ },
+ id: {
+ raw: 'park_petrified-forest',
+ },
+ },
+ {
+ visitors: {
+ raw: 617377.0,
+ },
+ square_km: {
+ raw: 137.5,
+ },
+ world_heritage_site: {
+ raw: 'false',
+ snippet: 'false',
+ },
+ date_established: {
+ raw: '1903-01-09T06:00:00+00:00',
+ },
+ description: {
+ raw: "Wind Cave is distinctive for its calcite fin formations called boxwork, a unique formation rarely found elsewhere, and needle-like growths called frostwork. The cave is one of the longest and most complex caves in the world. Above ground is a mixed-grass prairie with animals such as bison, black-footed ferrets, and prairie dogs, and ponderosa pine forests that are home to cougars and elk. The cave is culturally significant to the Lakota people as the site 'from which Wakan Tanka, the Great Mystery, sent the buffalo out into their hunting grounds.'",
+ snippet:
+ '-footed ferrets, and prairie dogs, and ponderosa pine forests that are home to cougars and elk',
+ },
+ location: {
+ raw: '43.57,-103.48',
+ },
+ acres: {
+ raw: 33970.84,
+ },
+ title: {
+ raw: 'Wind Cave',
+ snippet: 'Wind Cave',
+ },
+ nps_link: {
+ raw: 'https://www.nps.gov/wica/index.htm',
+ snippet: 'https://www.nps.gov/wica/index.htm',
+ },
+ states: {
+ raw: ['South Dakota'],
+ snippet: 'South Dakota',
+ },
+ _meta: {
+ engine: 'national-parks-demo',
+ score: 926068.7,
+ id: 'park_wind-cave',
+ },
+ id: {
+ raw: 'park_wind-cave',
+ },
+ },
+];
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx
deleted file mode 100644
index 855570829cce4..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx
+++ /dev/null
@@ -1,27 +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 '../../../../__mocks__/react_router';
-import '../../../__mocks__/engine_logic.mock';
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { CurationsSettings } from './curations_settings';
-
-describe('CurationsSettings', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('renders empty', () => {
- const wrapper = shallow( );
-
- expect(wrapper.isEmptyRender()).toBe(true);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx
new file mode 100644
index 0000000000000..4b4e11c31d4b8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx
@@ -0,0 +1,233 @@
+/*
+ * 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 '../../../../../__mocks__/shallow_useeffect.mock';
+import '../../../../../__mocks__/react_router';
+import '../../../../__mocks__/engine_logic.mock';
+
+import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiButtonEmpty, EuiCallOut, EuiSwitch } from '@elastic/eui';
+
+import { mountWithIntl } from '@kbn/test/jest';
+
+import { Loading } from '../../../../../shared/loading';
+import { EuiButtonTo } from '../../../../../shared/react_router_helpers';
+import { DataPanel } from '../../../data_panel';
+import { LogRetentionOptions } from '../../../log_retention';
+
+import { CurationsSettings } from '.';
+
+const MOCK_VALUES = {
+ // CurationsSettingsLogic
+ dataLoading: false,
+ curationsSettings: {
+ enabled: true,
+ mode: 'automatic',
+ },
+ // LogRetentionLogic
+ isLogRetentionUpdating: false,
+ logRetention: {
+ [LogRetentionOptions.Analytics]: {
+ enabled: true,
+ },
+ },
+ // LicensingLogic
+ hasPlatinumLicense: true,
+};
+
+const MOCK_ACTIONS = {
+ // CurationsSettingsLogic
+ loadCurationsSettings: jest.fn(),
+ onSkipLoadingCurationsSettings: jest.fn(),
+ toggleCurationsEnabled: jest.fn(),
+ toggleCurationsMode: jest.fn(),
+ // LogRetentionLogic
+ fetchLogRetention: jest.fn(),
+};
+
+describe('CurationsSettings', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(MOCK_ACTIONS);
+ });
+
+ it('loads curations and log retention settings on load', () => {
+ setMockValues(MOCK_VALUES);
+ mountWithIntl( );
+
+ expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalled();
+ expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalled();
+ });
+
+ it('contains a switch to toggle curations settings', () => {
+ let wrapper: ShallowWrapper;
+
+ setMockValues({
+ ...MOCK_VALUES,
+ curationsSettings: { ...MOCK_VALUES.curationsSettings, enabled: true },
+ });
+ wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(0).prop('checked')).toBe(true);
+
+ setMockValues({
+ ...MOCK_VALUES,
+ curationsSettings: { ...MOCK_VALUES.curationsSettings, enabled: false },
+ });
+ wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(0).prop('checked')).toBe(false);
+
+ wrapper.find(EuiSwitch).at(0).simulate('change');
+ expect(MOCK_ACTIONS.toggleCurationsEnabled).toHaveBeenCalled();
+ });
+
+ it('contains a switch to toggle the curations mode', () => {
+ let wrapper: ShallowWrapper;
+
+ setMockValues({
+ ...MOCK_VALUES,
+ curationsSettings: { ...MOCK_VALUES.curationsSettings, mode: 'automatic' },
+ });
+ wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(1).prop('checked')).toBe(true);
+
+ setMockValues({
+ ...MOCK_VALUES,
+ curationsSettings: { ...MOCK_VALUES.curationsSettings, mode: 'manual' },
+ });
+ wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(1).prop('checked')).toBe(false);
+
+ wrapper.find(EuiSwitch).at(1).simulate('change');
+ expect(MOCK_ACTIONS.toggleCurationsMode).toHaveBeenCalled();
+ });
+
+ it('enables form elements and hides the callout when analytics retention is enabled', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ logRetention: {
+ [LogRetentionOptions.Analytics]: {
+ enabled: true,
+ },
+ },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(0).prop('disabled')).toBe(false);
+ expect(wrapper.find(EuiSwitch).at(1).prop('disabled')).toBe(false);
+ expect(wrapper.find(EuiCallOut)).toHaveLength(0);
+ });
+
+ it('display a callout and disables form elements when analytics retention is disabled', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ logRetention: {
+ [LogRetentionOptions.Analytics]: {
+ enabled: false,
+ },
+ },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiSwitch).at(0).prop('disabled')).toBe(true);
+ expect(wrapper.find(EuiSwitch).at(1).prop('disabled')).toBe(true);
+ expect(wrapper.find(EuiCallOut).dive().find(EuiButtonTo).prop('to')).toEqual('/settings');
+ });
+
+ it('returns a loading state when curations data is loading', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ dataLoading: true,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.is(Loading)).toBe(true);
+ });
+
+ it('returns a loading state when log retention data is loading', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ isLogRetentionUpdating: true,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.is(Loading)).toBe(true);
+ });
+
+ describe('loading curation settings based on log retention', () => {
+ it('loads curation settings when log retention is enabled', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ logRetention: {
+ [LogRetentionOptions.Analytics]: {
+ enabled: true,
+ },
+ },
+ });
+
+ shallow( );
+
+ expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('skips loading curation settings when log retention is enabled', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ logRetention: {
+ [LogRetentionOptions.Analytics]: {
+ enabled: false,
+ },
+ },
+ });
+
+ shallow( );
+
+ expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('takes no action if log retention has not yet been loaded', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ logRetention: null,
+ });
+
+ shallow( );
+
+ expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(0);
+ expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when the user has no platinum license', () => {
+ beforeEach(() => {
+ setMockValues({
+ ...MOCK_VALUES,
+ hasPlatinumLicense: false,
+ });
+ });
+
+ it('it does not fetch log retention', () => {
+ shallow( );
+ expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalledTimes(0);
+ });
+
+ it('shows a CTA to upgrade your license when the user when the user', () => {
+ const wrapper = shallow( );
+ expect(wrapper.is(DataPanel)).toBe(true);
+ expect(wrapper.prop('action').props.to).toEqual('/app/management/stack/license_management');
+ expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('/license-management.html');
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx
new file mode 100644
index 0000000000000..de669298b11d9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx
@@ -0,0 +1,216 @@
+/*
+ * 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, { useEffect } from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import {
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiSwitch,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { docLinks } from '../../../../../shared/doc_links';
+import { LicensingLogic } from '../../../../../shared/licensing';
+import { Loading } from '../../../../../shared/loading';
+import { EuiButtonTo } from '../../../../../shared/react_router_helpers';
+import { SETTINGS_PATH } from '../../../../routes';
+import { DataPanel } from '../../../data_panel';
+import { LogRetentionLogic, LogRetentionOptions } from '../../../log_retention';
+
+import { AutomatedIcon } from '../../components/automated_icon';
+
+import { CurationsSettingsLogic } from './curations_settings_logic';
+
+export const CurationsSettings: React.FC = () => {
+ const { hasPlatinumLicense } = useValues(LicensingLogic);
+
+ const {
+ curationsSettings: { enabled, mode },
+ dataLoading,
+ } = useValues(CurationsSettingsLogic);
+ const {
+ loadCurationsSettings,
+ onSkipLoadingCurationsSettings,
+ toggleCurationsEnabled,
+ toggleCurationsMode,
+ } = useActions(CurationsSettingsLogic);
+
+ const { isLogRetentionUpdating, logRetention } = useValues(LogRetentionLogic);
+ const { fetchLogRetention } = useActions(LogRetentionLogic);
+
+ const analyticsDisabled = !logRetention?.[LogRetentionOptions.Analytics].enabled;
+
+ useEffect(() => {
+ if (hasPlatinumLicense) {
+ fetchLogRetention();
+ }
+ }, [hasPlatinumLicense]);
+
+ useEffect(() => {
+ if (logRetention) {
+ if (!analyticsDisabled) {
+ loadCurationsSettings();
+ } else {
+ onSkipLoadingCurationsSettings();
+ }
+ }
+ }, [logRetention]);
+
+ if (!hasPlatinumLicense)
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.curations.settings.licenseUpgradeCTATitle',
+ {
+ defaultMessage: 'Introducing automated curations',
+ }
+ )}
+
+ }
+ subtitle={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.curations.settings.platinum', {
+ defaultMessage: 'Platinum',
+ })}
+
+ ),
+ }}
+ />
+ }
+ action={
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel',
+ {
+ defaultMessage: 'Start a 30-day trial',
+ }
+ )}
+
+ }
+ >
+
+ {i18n.translate('xpack.enterpriseSearch.curations.settings.licenseUpgradeLink', {
+ defaultMessage: 'Learn more about license upgrades',
+ })}
+
+
+ );
+ if (dataLoading || isLogRetentionUpdating) return ;
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.curations.settings.automaticCurationsTitle',
+ {
+ defaultMessage: 'Automated Curations',
+ }
+ )}
+
+
+
+
+
+ {analyticsDisabled && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.curations.settings.analyticsDisabledCalloutDescription',
+ {
+ defaultMessage:
+ 'Automated curations require analytics to be enabled on your account.',
+ }
+ )}
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.curations.settings.manageAnalyticsButtonLabel',
+ { defaultMessage: 'Manage analytics' }
+ )}
+
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.curations.settings.automaticCurationsDescription',
+ {
+ defaultMessage:
+ "Suggested curations will monitor your engine's analytics and make automatic suggestions to help you deliver the most relevant results. Each suggested curation can be accepted, rejected, or modified.",
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts
new file mode 100644
index 0000000000000..818fac3d0706e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts
@@ -0,0 +1,221 @@
+/*
+ * 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 {
+ LogicMounter,
+ mockHttpValues,
+ mockFlashMessageHelpers,
+} from '../../../../../__mocks__/kea_logic';
+import '../../../../__mocks__/engine_logic.mock';
+
+import { nextTick } from '@kbn/test/jest';
+
+import { CurationsSettingsLogic } from './curations_settings_logic';
+
+const DEFAULT_VALUES = {
+ dataLoading: true,
+ curationsSettings: {
+ enabled: false,
+ mode: 'manual',
+ },
+};
+
+describe('CurationsSettingsLogic', () => {
+ const { mount } = new LogicMounter(CurationsSettingsLogic);
+ const { http } = mockHttpValues;
+ const { flashAPIErrors } = mockFlashMessageHelpers;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('has correct default values', () => {
+ mount();
+ expect(CurationsSettingsLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('actions', () => {
+ describe('onCurationsSettingsLoad', () => {
+ it('saves curation settings and that data has loaded', () => {
+ mount();
+
+ CurationsSettingsLogic.actions.onCurationsSettingsLoad({
+ enabled: true,
+ mode: 'automatic',
+ });
+
+ expect(CurationsSettingsLogic.values.dataLoading).toEqual(false);
+ expect(CurationsSettingsLogic.values.curationsSettings).toEqual({
+ enabled: true,
+ mode: 'automatic',
+ });
+ });
+ });
+
+ describe('onSkipCurationsSettingsLoad', () => {
+ it('saves that data has loaded', () => {
+ mount();
+
+ CurationsSettingsLogic.actions.onSkipLoadingCurationsSettings();
+
+ expect(CurationsSettingsLogic.values.dataLoading).toEqual(false);
+ });
+ });
+ });
+
+ describe('listeners', () => {
+ describe('loadCurationsSettings', () => {
+ it('calls the curations settings API and saves the returned settings', async () => {
+ http.get.mockReturnValueOnce(
+ Promise.resolve({
+ curation: {
+ enabled: true,
+ mode: 'automatic',
+ },
+ })
+ );
+ mount();
+ jest.spyOn(CurationsSettingsLogic.actions, 'onCurationsSettingsLoad');
+
+ CurationsSettingsLogic.actions.loadCurationsSettings();
+ await nextTick();
+
+ expect(http.get).toHaveBeenCalledWith(
+ '/internal/app_search/engines/some-engine/search_relevance_suggestions/settings'
+ );
+ expect(CurationsSettingsLogic.actions.onCurationsSettingsLoad).toHaveBeenCalledWith({
+ enabled: true,
+ mode: 'automatic',
+ });
+ });
+
+ it('presents any API errors to the user', async () => {
+ http.get.mockReturnValueOnce(Promise.reject('error'));
+ mount();
+
+ CurationsSettingsLogic.actions.loadCurationsSettings();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+
+ describe('toggleCurationsEnabled', () => {
+ it('enables curations when they are currently disabled', () => {
+ mount({
+ curationsSettings: {
+ ...DEFAULT_VALUES.curationsSettings,
+ enabled: false,
+ },
+ });
+ jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting');
+
+ CurationsSettingsLogic.actions.toggleCurationsEnabled();
+
+ expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({
+ enabled: true,
+ });
+ });
+
+ it('disables curations when they are currently enabled', () => {
+ mount({
+ curationsSettings: {
+ ...DEFAULT_VALUES.curationsSettings,
+ enabled: true,
+ },
+ });
+ jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting');
+
+ CurationsSettingsLogic.actions.toggleCurationsEnabled();
+
+ expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({
+ enabled: false,
+ mode: 'manual',
+ });
+ });
+ });
+
+ describe('toggleCurationsMode', () => {
+ it('sets to manual mode when it is currently automatic', () => {
+ mount({
+ curationsSettings: {
+ ...DEFAULT_VALUES.curationsSettings,
+ mode: 'automatic',
+ },
+ });
+ jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting');
+
+ CurationsSettingsLogic.actions.toggleCurationsMode();
+
+ expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({
+ mode: 'manual',
+ });
+ });
+
+ it('sets to automatic mode when it is currently manual', () => {
+ mount({
+ curationsSettings: {
+ ...DEFAULT_VALUES.curationsSettings,
+ mode: 'manual',
+ },
+ });
+ jest.spyOn(CurationsSettingsLogic.actions, 'updateCurationsSetting');
+
+ CurationsSettingsLogic.actions.toggleCurationsMode();
+
+ expect(CurationsSettingsLogic.actions.updateCurationsSetting).toHaveBeenCalledWith({
+ mode: 'automatic',
+ });
+ });
+ });
+
+ describe('updateCurationsSetting', () => {
+ it('calls the curations settings API and saves the returned settings', async () => {
+ http.put.mockReturnValueOnce(
+ Promise.resolve({
+ curation: {
+ enabled: true,
+ mode: 'automatic',
+ },
+ })
+ );
+ mount();
+ jest.spyOn(CurationsSettingsLogic.actions, 'onCurationsSettingsLoad');
+
+ CurationsSettingsLogic.actions.updateCurationsSetting({
+ enabled: true,
+ });
+ await nextTick();
+
+ expect(http.put).toHaveBeenCalledWith(
+ '/internal/app_search/engines/some-engine/search_relevance_suggestions/settings',
+ {
+ body: JSON.stringify({
+ curation: {
+ enabled: true,
+ },
+ }),
+ }
+ );
+ expect(CurationsSettingsLogic.actions.onCurationsSettingsLoad).toHaveBeenCalledWith({
+ enabled: true,
+ mode: 'automatic',
+ });
+ });
+
+ it('presents any API errors to the user', async () => {
+ http.put.mockReturnValueOnce(Promise.reject('error'));
+ mount();
+
+ CurationsSettingsLogic.actions.updateCurationsSetting({});
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts
new file mode 100644
index 0000000000000..d79ad64a69788
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+
+import { flashAPIErrors } from '../../../../../shared/flash_messages';
+import { HttpLogic } from '../../../../../shared/http';
+import { EngineLogic } from '../../../engine';
+
+export interface CurationsSettings {
+ enabled: boolean;
+ mode: 'automatic' | 'manual';
+}
+
+interface CurationsSettingsValues {
+ dataLoading: boolean;
+ curationsSettings: CurationsSettings;
+}
+
+interface CurationsSettingsActions {
+ loadCurationsSettings(): void;
+ onCurationsSettingsLoad(curationsSettings: CurationsSettings): {
+ curationsSettings: CurationsSettings;
+ };
+ onSkipLoadingCurationsSettings(): void;
+ toggleCurationsEnabled(): void;
+ toggleCurationsMode(): void;
+ updateCurationsSetting(currationsSetting: Partial): {
+ currationsSetting: Partial;
+ };
+}
+
+export const CurationsSettingsLogic = kea<
+ MakeLogicType
+>({
+ path: ['enterprise_search', 'app_search', 'curations', 'curations_settings_logic'],
+ actions: () => ({
+ loadCurationsSettings: true,
+ onCurationsSettingsLoad: (curationsSettings) => ({ curationsSettings }),
+ onSkipLoadingCurationsSettings: true,
+ toggleCurationsEnabled: true,
+ toggleCurationsMode: true,
+ updateCurationsSetting: (currationsSetting) => ({ currationsSetting }),
+ }),
+ reducers: () => ({
+ dataLoading: [
+ true,
+ {
+ onCurationsSettingsLoad: () => false,
+ onSkipLoadingCurationsSettings: () => false,
+ },
+ ],
+ curationsSettings: [
+ {
+ enabled: false,
+ mode: 'manual',
+ },
+ {
+ onCurationsSettingsLoad: (_, { curationsSettings }) => curationsSettings,
+ },
+ ],
+ }),
+ listeners: ({ actions, values }) => ({
+ loadCurationsSettings: async () => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ const response = await http.get(
+ `/internal/app_search/engines/${engineName}/search_relevance_suggestions/settings`
+ );
+ actions.onCurationsSettingsLoad(response.curation);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ toggleCurationsEnabled: async () => {
+ if (values.curationsSettings.enabled) {
+ actions.updateCurationsSetting({ enabled: false, mode: 'manual' });
+ } else {
+ actions.updateCurationsSetting({ enabled: true });
+ }
+ },
+ toggleCurationsMode: async () => {
+ actions.updateCurationsSetting({
+ mode: values.curationsSettings.mode === 'automatic' ? 'manual' : 'automatic',
+ });
+ },
+ updateCurationsSetting: async ({ currationsSetting }) => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+ try {
+ const response = await http.put(
+ `/internal/app_search/engines/${engineName}/search_relevance_suggestions/settings`,
+ {
+ body: JSON.stringify({ curation: currationsSetting }),
+ }
+ );
+ actions.onCurationsSettingsLoad(response.curation);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/index.ts
new file mode 100644
index 0000000000000..fd7d3156cc5ab
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { CurationsSettings } from './curations_settings';
+export { CurationsSettingsLogic } from './curations_settings_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts
index ca6924879324a..7268c0fdbc4dc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts
@@ -7,3 +7,4 @@
export { Curations } from './curations';
export { CurationCreation } from './curation_creation';
+export { CurationSuggestion } from './curation_suggestion';
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
index 555a66cedc85e..d6f741526b29b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
@@ -37,4 +37,53 @@ describe('search relevance insights routes', () => {
});
});
});
+
+ describe('GET /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => {
+ const mockRouter = new MockRouter({
+ method: 'get',
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings',
+ });
+
+ beforeEach(() => {
+ registerSearchRelevanceSuggestionsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings',
+ });
+ });
+ });
+
+ describe('PUT /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => {
+ const mockRouter = new MockRouter({
+ method: 'put',
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings',
+ });
+
+ beforeEach(() => {
+ registerSearchRelevanceSuggestionsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ body: { curation: { enabled: true } },
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings',
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
index 147f68f0476ee..861d8c52b537f 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
@@ -7,6 +7,8 @@
import { schema } from '@kbn/config-schema';
+import { skipBodyValidation } from '../../lib/route_config_helpers';
+
import { RouteDependencies } from '../../plugin';
export function registerSearchRelevanceSuggestionsRoutes({
@@ -36,4 +38,32 @@ export function registerSearchRelevanceSuggestionsRoutes({
path: '/api/as/v0/engines/:engineName/search_relevance_suggestions',
})
);
+
+ router.get(
+ {
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings',
+ })
+ );
+
+ router.put(
+ skipBodyValidation({
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ },
+ }),
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions/settings',
+ })
+ );
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
index 1d43f90b80def..a8cab77af447c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
@@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { DownloadStep } from '../../../../components';
import {
useStartServices,
- useGetOutputs,
+ useDefaultOutput,
sendGenerateServiceToken,
usePlatform,
PLATFORM_OPTIONS,
@@ -242,7 +242,7 @@ export const FleetServerCommandStep = ({
};
export const useFleetServerInstructions = (policyId?: string) => {
- const outputsRequest = useGetOutputs();
+ const { output, refresh: refreshOutputs } = useDefaultOutput();
const { notifications } = useStartServices();
const [serviceToken, setServiceToken] = useState();
const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false);
@@ -250,9 +250,7 @@ export const useFleetServerInstructions = (policyId?: string) => {
const [deploymentMode, setDeploymentMode] = useState('production');
const { data: settings, resendRequest: refreshSettings } = useGetSettings();
const fleetServerHost = settings?.item.fleet_server_hosts?.[0];
- const output = outputsRequest.data?.items?.[0];
const esHost = output?.hosts?.[0];
- const refreshOutputs = outputsRequest.resendRequest;
const installCommand = useMemo((): string => {
if (!serviceToken || !esHost) {
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx
index daa0520ebc041..d60ccd93b8db1 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx
@@ -11,22 +11,24 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { Loading } from '../../../../components';
-import type { CategoryCount } from '../../../../../../../../../../src/plugins/custom_integrations/common';
-import { CATEGORY_DISPLAY } from '../../../../../../../../../../src/plugins/custom_integrations/common';
+import type { IntegrationCategoryCount } from '../../../../../../../../../../src/plugins/custom_integrations/common';
+import { INTEGRATION_CATEGORY_DISPLAY } from '../../../../../../../../../../src/plugins/custom_integrations/common';
interface ALL_CATEGORY {
id: '';
count: number;
}
-export type CategoryFacet = CategoryCount | ALL_CATEGORY;
+export type CategoryFacet = IntegrationCategoryCount | ALL_CATEGORY;
export function CategoryFacets({
+ showCounts,
isLoading,
categories,
selectedCategory,
onCategoryChange,
}: {
+ showCounts: boolean;
isLoading?: boolean;
categories: CategoryFacet[];
selectedCategory: string;
@@ -49,14 +51,14 @@ export function CategoryFacets({
defaultMessage: 'All',
});
} else {
- title = CATEGORY_DISPLAY[category.id];
+ title = INTEGRATION_CATEGORY_DISPLAY[category.id];
}
return (
onCategoryChange(category)}
>
{title}
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx
index 48a9dc0f6b63c..27e2d11769ef8 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx
@@ -21,7 +21,8 @@ import {
useGetCategories,
useGetPackages,
useBreadcrumbs,
- useGetAddableCustomIntegrations,
+ useGetAppendCustomIntegrations,
+ useGetReplacementCustomIntegrations,
useLink,
} from '../../../../hooks';
import { doesPackageHaveIntegrations } from '../../../../services';
@@ -35,7 +36,9 @@ import type { PackageListItem } from '../../../../types';
import type { IntegrationCardItem } from '../../../../../../../common/types/models';
-import type { Category } from '../../../../../../../../../../src/plugins/custom_integrations/common';
+import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common';
+
+import { useMergeEprPackagesWithReplacements } from '../../../../../../hooks/use_merge_epr_with_replacements';
import { mergeAndReplaceCategoryCounts } from './util';
import { CategoryFacets } from './category_facets';
@@ -209,6 +212,7 @@ const InstalledPackages: React.FC = memo(() => {
const controls = (
setSelectedCategory(id)}
@@ -266,6 +270,7 @@ const AvailablePackages: React.FC = memo(() => {
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({
include_policy_templates: true,
});
+
const eprPackages = useMemo(
() => packageListToIntegrationsList(categoryPackagesRes?.response || []),
[categoryPackagesRes]
@@ -276,14 +281,23 @@ const AvailablePackages: React.FC = memo(() => {
[allCategoryPackagesRes]
);
- const { loading: isLoadingAddableCustomIntegrations, value: addableCustomIntegrations } =
- useGetAddableCustomIntegrations();
- const filteredAddableIntegrations = addableCustomIntegrations
- ? addableCustomIntegrations.filter((integration: CustomIntegration) => {
+ const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations();
+
+ const mergedEprPackages: Array =
+ useMergeEprPackagesWithReplacements(
+ eprPackages || [],
+ replacementCustomIntegrations || [],
+ selectedCategory as IntegrationCategory
+ );
+
+ const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } =
+ useGetAppendCustomIntegrations();
+ const filteredAddableIntegrations = appendCustomIntegrations
+ ? appendCustomIntegrations.filter((integration: CustomIntegration) => {
if (!selectedCategory) {
return true;
}
- return integration.categories.indexOf(selectedCategory as Category) >= 0;
+ return integration.categories.indexOf(selectedCategory as IntegrationCategory) >= 0;
})
: [];
@@ -296,7 +310,7 @@ const AvailablePackages: React.FC = memo(() => {
);
const eprAndCustomPackages: Array = [
- ...eprPackages,
+ ...mergedEprPackages,
...filteredAddableIntegrations,
];
eprAndCustomPackages.sort((a, b) => {
@@ -306,26 +320,26 @@ const AvailablePackages: React.FC = memo(() => {
const categories = useMemo(() => {
const eprAndCustomCategories: CategoryFacet[] =
isLoadingCategories ||
- isLoadingAddableCustomIntegrations ||
- !addableCustomIntegrations ||
+ isLoadingAppendCustomIntegrations ||
+ !appendCustomIntegrations ||
!categoriesRes
? []
: mergeAndReplaceCategoryCounts(
categoriesRes.response as CategoryFacet[],
- addableCustomIntegrations
+ appendCustomIntegrations
);
return [
{
id: '',
- count: (allEprPackages?.length || 0) + (addableCustomIntegrations?.length || 0),
+ count: (allEprPackages?.length || 0) + (appendCustomIntegrations?.length || 0),
},
...(eprAndCustomCategories ? eprAndCustomCategories : []),
] as CategoryFacet[];
}, [
allEprPackages?.length,
- addableCustomIntegrations,
+ appendCustomIntegrations,
categoriesRes,
- isLoadingAddableCustomIntegrations,
+ isLoadingAppendCustomIntegrations,
isLoadingCategories,
]);
@@ -336,7 +350,8 @@ const AvailablePackages: React.FC = memo(() => {
const controls = categories ? (
{
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts
index 2f0e4f71ea59d..53a62555650ab 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts
@@ -7,7 +7,7 @@
import type {
CustomIntegration,
- Category,
+ IntegrationCategory,
} from '../../../../../../../../../../src/plugins/custom_integrations/common';
import type { CategoryFacet } from './category_facets';
@@ -27,7 +27,7 @@ export function mergeAndReplaceCategoryCounts(
match.count += count;
} else {
merged.push({
- id: category as Category,
+ id: category as IntegrationCategory,
count,
});
}
diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
index e42733bbd2da0..9bedfca0d3bca 100644
--- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
@@ -36,7 +36,7 @@ import {
useGetSettings,
useInput,
sendPutSettings,
- useGetOutputs,
+ useDefaultOutput,
sendPutOutput,
} from '../../hooks';
import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common';
@@ -258,8 +258,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
- const outputsRequest = useGetOutputs();
- const output = outputsRequest.data?.items?.[0];
+ const { output } = useDefaultOutput();
const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose);
const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false);
diff --git a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts
new file mode 100644
index 0000000000000..687fb01b04546
--- /dev/null
+++ b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts
@@ -0,0 +1,220 @@
+/*
+ * 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 type { PackageListItem } from '../../common/types/models';
+import type { CustomIntegration } from '../../../../../src/plugins/custom_integrations/common';
+
+import type { IntegrationCategory } from '../../../../../src/plugins/custom_integrations/common';
+
+import { useMergeEprPackagesWithReplacements } from './use_merge_epr_with_replacements';
+
+function mockEprPackages(
+ items: Array<{ name: string; release: 'ga' | 'beta' | 'experimental'; integration?: string }>
+): PackageListItem[] {
+ return items as unknown as PackageListItem[];
+}
+
+function mockIntegrations(
+ items: Array<{ eprOverlap?: string; id: string; categories: IntegrationCategory[] }>
+): CustomIntegration[] {
+ return items as unknown as CustomIntegration[];
+}
+
+describe('useMergeEprWithReplacements', () => {
+ test('should not replace ga packages', () => {
+ const eprPackages: PackageListItem[] = mockEprPackages([
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 'cloudwatch',
+ },
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 's3',
+ },
+ ]);
+ const replacements: CustomIntegration[] = mockIntegrations([
+ {
+ eprOverlap: 'aws',
+ id: 'awsLogs',
+ categories: ['cloud', 'datastore'],
+ },
+ ]);
+
+ expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 'cloudwatch',
+ },
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 's3',
+ },
+ ]);
+ });
+
+ test('should replace non-ga packages', () => {
+ const eprPackages: PackageListItem[] = mockEprPackages([
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ ]);
+ const replacements: CustomIntegration[] = mockIntegrations([
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-logs',
+ categories: ['web'],
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-metrics',
+ categories: ['web'],
+ },
+ ]);
+
+ expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-logs',
+ categories: ['web'],
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-metrics',
+ categories: ['web'],
+ },
+ ]);
+ });
+
+ test('should merge if no equivalent package', () => {
+ const eprPackages: PackageListItem[] = mockEprPackages([
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ ]);
+ const replacements: CustomIntegration[] = mockIntegrations([
+ {
+ id: 'prometheus',
+ categories: ['monitoring', 'datastore'],
+ },
+ ]);
+
+ expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ {
+ id: 'prometheus',
+ categories: ['monitoring', 'datastore'],
+ },
+ ]);
+ });
+
+ test('should respect category assignment', () => {
+ const eprPackages: PackageListItem[] = mockEprPackages([
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ ]);
+ const replacements: CustomIntegration[] = mockIntegrations([
+ {
+ id: 'prometheus',
+ categories: ['monitoring', 'datastore'],
+ },
+ {
+ id: 'oracle',
+ categories: ['datastore'],
+ },
+ ]);
+
+ expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, 'web')).toEqual([
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ ]);
+ });
+
+ test('should consists of all 3 types (ga eprs, replacements for non-ga eprs, replacements without epr equivalent', () => {
+ const eprPackages: PackageListItem[] = mockEprPackages([
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 'cloudwatch',
+ },
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 's3',
+ },
+ {
+ name: 'activemq',
+ release: 'beta',
+ },
+ ]);
+ const replacements: CustomIntegration[] = mockIntegrations([
+ {
+ id: 'prometheus',
+ categories: ['monitoring', 'datastore'],
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-logs',
+ categories: ['web'],
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-metrics',
+ categories: ['web'],
+ },
+ {
+ eprOverlap: 'aws',
+ id: 'awsLogs',
+ categories: ['cloud', 'datastore'],
+ },
+ {
+ eprOverlap: 'aws',
+ id: 'awsMetrics',
+ categories: ['cloud', 'datastore'],
+ },
+ ]);
+
+ expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 'cloudwatch',
+ },
+ {
+ name: 'aws',
+ release: 'ga',
+ integration: 's3',
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-logs',
+ categories: ['web'],
+ },
+ {
+ eprOverlap: 'activemq',
+ id: 'activemq-metrics',
+ categories: ['web'],
+ },
+ {
+ id: 'prometheus',
+ categories: ['monitoring', 'datastore'],
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts
new file mode 100644
index 0000000000000..a3c1fea5e744f
--- /dev/null
+++ b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 type { PackageListItem } from '../../common/types/models';
+import type {
+ CustomIntegration,
+ IntegrationCategory,
+} from '../../../../../src/plugins/custom_integrations/common';
+
+// Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package)
+function findReplacementsForEprPackage(
+ replacements: CustomIntegration[],
+ packageName: string,
+ release: 'beta' | 'experimental' | 'ga'
+): CustomIntegration[] {
+ if (release === 'ga') {
+ return [];
+ }
+ return replacements.filter((customIntegration: CustomIntegration) => {
+ return customIntegration.eprOverlap === packageName;
+ });
+}
+
+export function useMergeEprPackagesWithReplacements(
+ eprPackages: PackageListItem[],
+ replacements: CustomIntegration[],
+ category: IntegrationCategory | ''
+): Array {
+ const merged: Array = [];
+
+ const filteredReplacements = replacements.filter((customIntegration) => {
+ return !category || customIntegration.categories.includes(category);
+ });
+
+ // Either select replacement or select beat
+ eprPackages.forEach((eprPackage) => {
+ const hits = findReplacementsForEprPackage(
+ filteredReplacements,
+ eprPackage.name,
+ eprPackage.release
+ );
+ if (hits.length) {
+ hits.forEach((hit) => {
+ const match = merged.find(({ id }) => {
+ return id === hit.id;
+ });
+ if (!match) {
+ merged.push(hit);
+ }
+ });
+ } else {
+ merged.push(eprPackage);
+ }
+ });
+
+ // Add unused replacements
+ // This is an edge-case. E.g. the Oracle-beat did not have an Epr-equivalent at the time of writing
+ const unusedReplacements = filteredReplacements.filter((integration) => {
+ return !eprPackages.find((eprPackage) => {
+ return eprPackage.name === integration.eprOverlap;
+ });
+ });
+
+ merged.push(...unusedReplacements);
+
+ return merged;
+}
diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
index 650667000409a..d6764aac7de00 100644
--- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
@@ -24,11 +24,16 @@ import { getCustomIntegrations } from '../../services/custom_integrations';
import { useRequest, sendRequest } from './use_request';
-export function useGetAddableCustomIntegrations() {
+export function useGetAppendCustomIntegrations() {
const customIntegrations = getCustomIntegrations();
return useAsync(customIntegrations.getAppendCustomIntegrations, []);
}
+export function useGetReplacementCustomIntegrations() {
+ const customIntegrations = getCustomIntegrations();
+ return useAsync(customIntegrations.getReplacementCustomIntegrations, []);
+}
+
export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
return useRequest({
path: epmRouteService.getCategoriesPath(),
diff --git a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
index 0fcaa262cf321..2d623da505c65 100644
--- a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { useMemo, useCallback } from 'react';
+
import { outputRoutesService } from '../../services';
import type { PutOutputRequest, GetOutputsResponse } from '../../types';
@@ -17,6 +19,21 @@ export function useGetOutputs() {
});
}
+export function useDefaultOutput() {
+ const outputsRequest = useGetOutputs();
+ const output = useMemo(() => {
+ return outputsRequest.data?.items.find((o) => o.is_default);
+ }, [outputsRequest.data]);
+
+ const refresh = useCallback(() => {
+ return outputsRequest.resendRequest();
+ }, [outputsRequest]);
+
+ return useMemo(() => {
+ return { output, refresh };
+ }, [output, refresh]);
+}
+
export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) {
return sendRequest({
method: 'put',
diff --git a/x-pack/plugins/fleet/server/integration_tests/router.test.ts b/x-pack/plugins/fleet/server/integration_tests/router.test.ts
index d5f40e4db2cfe..55518923e65f2 100644
--- a/x-pack/plugins/fleet/server/integration_tests/router.test.ts
+++ b/x-pack/plugins/fleet/server/integration_tests/router.test.ts
@@ -19,7 +19,6 @@ import * as kbnTestServer from '../../../../../src/test_utils/kbn_server';
function createXPackRoot(config: {} = {}) {
return kbnTestServer.createRoot({
plugins: {
- scanDirs: [],
paths: [
resolve(__dirname, '../../../../../x-pack/plugins/encrypted_saved_objects'),
resolve(__dirname, '../../../../../x-pack/plugins/fleet'),
diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
index 7fa63404a4abd..2fe79a22fc764 100644
--- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
+++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
@@ -50,6 +50,7 @@ function ListItem({
children: ReactNode;
}) {
return (
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
= (props) => {
[alertParams.groupBy]
);
+ const groupByFilterTestPatterns = useMemo(() => {
+ if (!alertParams.groupBy) return null;
+ const groups = !Array.isArray(alertParams.groupBy)
+ ? [alertParams.groupBy]
+ : alertParams.groupBy;
+ return groups.map((group: string) => ({
+ groupName: group,
+ pattern: new RegExp(`{"match(_phrase)?":{"${group}":"(.*?)"}}`),
+ }));
+ }, [alertParams.groupBy]);
+
+ const redundantFilterGroupBy = useMemo(() => {
+ if (!alertParams.filterQuery || !groupByFilterTestPatterns) return [];
+ return groupByFilterTestPatterns
+ .map(({ groupName, pattern }) => {
+ if (pattern.test(alertParams.filterQuery!)) {
+ return groupName;
+ }
+ })
+ .filter((g) => typeof g === 'string') as string[];
+ }, [alertParams.filterQuery, groupByFilterTestPatterns]);
+
return (
<>
@@ -425,8 +447,24 @@ export const Expressions: React.FC = (props) => {
...options,
groupBy: alertParams.groupBy || undefined,
}}
+ errorOptions={redundantFilterGroupBy}
/>
+ {redundantFilterGroupBy.length > 0 && (
+ <>
+
+
+ {redundantFilterGroupBy.join(', ')},
+ groupCount: redundantFilterGroupBy.length,
+ }}
+ />
+
+ >
+ )}
void;
fields: IFieldType[];
+ errorOptions?: string[];
}
-export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => {
+export const MetricsExplorerGroupBy = ({ options, onChange, fields, errorOptions }: Props) => {
const handleChange = useCallback(
(selectedOptions: Array<{ label: string }>) => {
const groupBy = selectedOptions.map((option) => option.label);
@@ -28,9 +29,17 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) =>
);
const selectedOptions = Array.isArray(options.groupBy)
- ? options.groupBy.map((field) => ({ label: field }))
+ ? options.groupBy.map((field) => ({
+ label: field,
+ color: errorOptions?.includes(field) ? 'danger' : undefined,
+ }))
: options.groupBy
- ? [{ label: options.groupBy }]
+ ? [
+ {
+ label: options.groupBy,
+ color: errorOptions?.includes(options.groupBy) ? 'danger' : undefined,
+ },
+ ]
: [];
return (
diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
index 29b0fb1352e5b..47bb1f91b4ab2 100644
--- a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
+++ b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
@@ -30,6 +30,7 @@ interface AxisConfig {
export type YAxisMode = 'auto' | 'left' | 'right' | 'bottom';
export type LineStyle = 'solid' | 'dashed' | 'dotted';
export type FillStyle = 'none' | 'above' | 'below';
+export type IconPosition = 'auto' | 'left' | 'right' | 'above' | 'below';
export interface YConfig {
forAccessor: string;
@@ -39,6 +40,7 @@ export interface YConfig {
lineWidth?: number;
lineStyle?: LineStyle;
fill?: FillStyle;
+ iconPosition?: IconPosition;
}
export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & {
@@ -180,6 +182,11 @@ export const yAxisConfig: ExpressionFunctionDefinition<
types: ['string'],
help: 'An optional icon used for threshold lines',
},
+ iconPosition: {
+ types: ['string'],
+ options: ['auto', 'above', 'below', 'left', 'right'],
+ help: 'The placement of the icon for the threshold line',
+ },
fill: {
types: ['string'],
options: ['none', 'above', 'below'],
diff --git a/x-pack/plugins/lens/public/_mixins.scss b/x-pack/plugins/lens/public/_mixins.scss
index 5a798bcc6c23b..7282de214636c 100644
--- a/x-pack/plugins/lens/public/_mixins.scss
+++ b/x-pack/plugins/lens/public/_mixins.scss
@@ -15,7 +15,7 @@
// Static styles for a draggable item
@mixin lnsDraggable {
@include euiSlightShadow;
- background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade);
+ background: $euiColorEmptyShade;
border: $euiBorderWidthThin dashed transparent;
cursor: grab;
}
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index d10fe42feb322..a2c7c67e1fc77 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -13,7 +13,13 @@ import { App } from './app';
import { LensAppProps, LensAppServices } from './types';
import { EditorFrameInstance, EditorFrameProps } from '../types';
import { Document } from '../persistence';
-import { visualizationMap, datasourceMap, makeDefaultServices, mountWithProvider } from '../mocks';
+import {
+ visualizationMap,
+ datasourceMap,
+ makeDefaultServices,
+ mountWithProvider,
+ mockStoreDeps,
+} from '../mocks';
import { I18nProvider } from '@kbn/i18n/react';
import {
SavedObjectSaveModal,
@@ -92,9 +98,11 @@ describe('Lens App', () => {
};
}
+ const makeDefaultServicesForApp = () => makeDefaultServices(sessionIdSubject, 'sessionId-1');
+
async function mountWith({
props = makeDefaultProps(),
- services = makeDefaultServices(sessionIdSubject),
+ services = makeDefaultServicesForApp(),
preloadedState,
}: {
props?: jest.Mocked;
@@ -110,11 +118,11 @@ describe('Lens App', () => {
);
};
-
+ const storeDeps = mockStoreDeps({ lensServices: services });
const { instance, lensStore } = await mountWithProvider(
,
{
- data: services.data,
+ storeDeps,
preloadedState,
},
{ wrappingComponent }
@@ -144,7 +152,7 @@ describe('Lens App', () => {
});
it('updates global filters with store state', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
const indexPattern = { id: 'index1' } as unknown as IndexPattern;
const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec;
const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern);
@@ -216,7 +224,7 @@ describe('Lens App', () => {
it('sets originatingApp breadcrumb when the document title changes', async () => {
const props = makeDefaultProps();
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
props.incomingState = { originatingApp: 'coolContainer' };
services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made');
@@ -262,7 +270,7 @@ describe('Lens App', () => {
describe('TopNavMenu#showDatePicker', () => {
it('shows date picker if any used index pattern isTimeBased', async () => {
- const customServices = makeDefaultServices(sessionIdSubject);
+ const customServices = makeDefaultServicesForApp();
customServices.data.indexPatterns.get = jest
.fn()
.mockImplementation((id) =>
@@ -275,7 +283,7 @@ describe('Lens App', () => {
);
});
it('shows date picker if active datasource isTimeBased', async () => {
- const customServices = makeDefaultServices(sessionIdSubject);
+ const customServices = makeDefaultServicesForApp();
customServices.data.indexPatterns.get = jest
.fn()
.mockImplementation((id) =>
@@ -290,7 +298,7 @@ describe('Lens App', () => {
);
});
it('does not show date picker if index pattern nor active datasource is not time based', async () => {
- const customServices = makeDefaultServices(sessionIdSubject);
+ const customServices = makeDefaultServicesForApp();
customServices.data.indexPatterns.get = jest
.fn()
.mockImplementation((id) =>
@@ -337,7 +345,7 @@ describe('Lens App', () => {
);
});
it('handles rejected index pattern', async () => {
- const customServices = makeDefaultServices(sessionIdSubject);
+ const customServices = makeDefaultServicesForApp();
customServices.data.indexPatterns.get = jest
.fn()
.mockImplementation((id) => Promise.reject({ reason: 'Could not locate that data view' }));
@@ -385,7 +393,7 @@ describe('Lens App', () => {
: undefined,
};
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.attributeService.wrapAttributes = jest
.fn()
.mockImplementation(async ({ savedObjectId }) => ({
@@ -419,7 +427,7 @@ describe('Lens App', () => {
}
it('shows a disabled save button when the user does not have permissions', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.application = {
...services.application,
capabilities: {
@@ -469,7 +477,7 @@ describe('Lens App', () => {
it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => {
const props = makeDefaultProps();
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.dashboardFeatureFlag = { allowByValueEmbeddables: true };
props.incomingState = {
originatingApp: 'ultraDashboard',
@@ -618,7 +626,7 @@ describe('Lens App', () => {
const mockedConsoleDir = jest.spyOn(console, 'dir'); // mocked console.dir to avoid messages in the console when running tests
mockedConsoleDir.mockImplementation(() => {});
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.attributeService.wrapAttributes = jest
.fn()
.mockRejectedValue({ message: 'failed' });
@@ -692,7 +700,7 @@ describe('Lens App', () => {
});
it('checks for duplicate title before saving', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.attributeService.wrapAttributes = jest
.fn()
.mockReturnValue(Promise.resolve({ savedObjectId: '123' }));
@@ -759,7 +767,7 @@ describe('Lens App', () => {
});
it('should still be enabled even if the user is missing save permissions', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.application = {
...services.application,
capabilities: {
@@ -799,7 +807,7 @@ describe('Lens App', () => {
});
it('should open inspect panel', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
const { instance } = await mountWith({ services, preloadedState: { isSaveable: true } });
await runInspect(instance);
@@ -943,7 +951,7 @@ describe('Lens App', () => {
describe('saved query handling', () => {
it('does not allow saving when the user is missing the saveQuery permission', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.application = {
...services.application,
capabilities: {
@@ -1136,7 +1144,7 @@ describe('Lens App', () => {
it('updates the state if session id changes from the outside', async () => {
const sessionIdS = new Subject();
- const services = makeDefaultServices(sessionIdS);
+ const services = makeDefaultServices(sessionIdS, 'sessionId-1');
const { lensStore } = await mountWith({ props: undefined, services });
act(() => {
@@ -1180,7 +1188,7 @@ describe('Lens App', () => {
});
it('does not confirm if the user is missing save permissions', async () => {
- const services = makeDefaultServices(sessionIdSubject);
+ const services = makeDefaultServicesForApp();
services.application = {
...services.application,
capabilities: {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx
index 69e4aa629cec6..b0c10abb75810 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx
@@ -57,14 +57,13 @@ export function AddLayerButton({
})}
content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', {
defaultMessage:
- 'Use multiple layers to combine visualization types or visualize different index patterns.',
+ 'Use multiple layers to combine visualization types or visualize different data views.',
})}
position="bottom"
>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss
deleted file mode 100644
index 0d51108fb2dcb..0000000000000
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.lnsConfigPanel__addLayerBtn {
- @include kbnThemeStyle('v7') {
- // sass-lint:disable-block no-important
- background-color: transparent !important;
- color: transparentize($euiColorMediumShade, .3) !important;
- border-color: $euiColorLightShade !important;
- box-shadow: none !important;
- }
-
-}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
index 2668a31d70754..61d37d4cc9fed 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
@@ -7,12 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import {
- createMockVisualization,
- createMockFramePublicAPI,
- createMockDatasource,
- DatasourceMock,
-} from '../../../mocks';
+import { createMockFramePublicAPI, visualizationMap, datasourceMap } from '../../../mocks';
import { Visualization } from '../../../types';
import { LayerPanels } from './config_panel';
import { LayerPanel } from './layer_panel';
@@ -43,32 +38,23 @@ afterEach(() => {
});
describe('ConfigPanel', () => {
- let mockVisualization: jest.Mocked;
- let mockVisualization2: jest.Mocked;
- let mockDatasource: DatasourceMock;
const frame = createMockFramePublicAPI();
function getDefaultProps() {
frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
+ first: datasourceMap.testDatasource.publicAPIMock,
};
return {
- activeVisualizationId: 'vis1',
- visualizationMap: {
- vis1: mockVisualization,
- vis2: mockVisualization2,
- },
- activeDatasourceId: 'mockindexpattern',
- datasourceMap: {
- mockindexpattern: mockDatasource,
- },
+ activeVisualizationId: 'testVis',
+ visualizationMap,
+ activeDatasourceId: 'testDatasource',
+ datasourceMap,
activeVisualization: {
- ...mockVisualization,
+ ...visualizationMap.testVis,
getLayerIds: () => Object.keys(frame.datasourceLayers),
- appendLayer: jest.fn(),
} as unknown as Visualization,
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
@@ -85,38 +71,6 @@ describe('ConfigPanel', () => {
};
}
- beforeEach(() => {
- mockVisualization = {
- ...createMockVisualization(),
- id: 'testVis',
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'testVis',
- label: 'TEST1',
- groupLabel: 'testVisGroup',
- },
- ],
- };
-
- mockVisualization2 = {
- ...createMockVisualization(),
-
- id: 'testVis2',
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'testVis2',
- label: 'TEST2',
- groupLabel: 'testVis2Group',
- },
- ],
- };
-
- mockVisualization.getLayerIds.mockReturnValue(Object.keys(frame.datasourceLayers));
- mockDatasource = createMockDatasource('mockindexpattern');
- });
-
// in what case is this test needed?
it('should fail to render layerPanels if the public API is out of date', async () => {
const props = getDefaultProps();
@@ -130,7 +84,7 @@ describe('ConfigPanel', () => {
const { instance, lensStore } = await mountWithProvider( , {
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
@@ -140,22 +94,22 @@ describe('ConfigPanel', () => {
const { updateDatasource, updateAll } = instance.find(LayerPanel).props();
const updater = () => 'updated';
- updateDatasource('mockindexpattern', updater);
+ updateDatasource('testDatasource', updater);
await waitMs(0);
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
expect(
(lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
- props.datasourceStates.mockindexpattern.state
+ props.datasourceStates.testDatasource.state
)
).toEqual('updated');
- updateAll('mockindexpattern', updater, props.visualizationState);
+ updateAll('testDatasource', updater, props.visualizationState);
// wait for one tick so async updater has a chance to trigger
await waitMs(0);
expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
expect(
(lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
- props.datasourceStates.mockindexpattern.state
+ props.datasourceStates.testDatasource.state
)
).toEqual('updated');
});
@@ -167,7 +121,7 @@ describe('ConfigPanel', () => {
{
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
@@ -195,15 +149,15 @@ describe('ConfigPanel', () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- second: mockDatasource.publicAPIMock,
+ first: datasourceMap.testDatasource.publicAPIMock,
+ second: datasourceMap.testDatasource.publicAPIMock,
};
const { instance } = await mountWithProvider(
,
{
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
@@ -232,15 +186,15 @@ describe('ConfigPanel', () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- second: mockDatasource.publicAPIMock,
+ first: datasourceMap.testDatasource.publicAPIMock,
+ second: datasourceMap.testDatasource.publicAPIMock,
};
const { instance } = await mountWithProvider(
,
{
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
@@ -273,16 +227,16 @@ describe('ConfigPanel', () => {
{
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
},
- activeDatasourceId: 'mockindexpattern',
+ activeDatasourceId: 'testDatasource',
},
dispatch: jest.fn((x) => {
if (x.payload.subType === 'ADD_LAYER') {
- frame.datasourceLayers.second = mockDatasource.publicAPIMock;
+ frame.datasourceLayers.second = datasourceMap.testDatasource.publicAPIMock;
}
}),
},
@@ -303,16 +257,15 @@ describe('ConfigPanel', () => {
(generateId as jest.Mock).mockReturnValue(`newId`);
return mountWithProvider(
,
-
{
preloadedState: {
datasourceStates: {
- mockindexpattern: {
+ testDatasource: {
isLoading: false,
state: 'state',
},
},
- activeDatasourceId: 'mockindexpattern',
+ activeDatasourceId: 'testDatasource',
},
},
{
@@ -352,13 +305,13 @@ describe('ConfigPanel', () => {
label: 'Threshold layer',
},
]);
- mockDatasource.initializeDimension = jest.fn();
+ datasourceMap.testDatasource.initializeDimension = jest.fn();
const { instance, lensStore } = await prepareAndMountComponent(props);
await clickToAddLayer(instance);
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
- expect(mockDatasource.initializeDimension).not.toHaveBeenCalled();
+ expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled();
});
it('should not add an initial dimension when initialDimensions are not available for the given layer type', async () => {
@@ -382,13 +335,13 @@ describe('ConfigPanel', () => {
label: 'Threshold layer',
},
]);
- mockDatasource.initializeDimension = jest.fn();
+ datasourceMap.testDatasource.initializeDimension = jest.fn();
const { instance, lensStore } = await prepareAndMountComponent(props);
await clickToAddLayer(instance);
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
- expect(mockDatasource.initializeDimension).not.toHaveBeenCalled();
+ expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled();
});
it('should use group initial dimension value when adding a new layer if available', async () => {
@@ -409,13 +362,13 @@ describe('ConfigPanel', () => {
],
},
]);
- mockDatasource.initializeDimension = jest.fn();
+ datasourceMap.testDatasource.initializeDimension = jest.fn();
const { instance, lensStore } = await prepareAndMountComponent(props);
await clickToAddLayer(instance);
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
- expect(mockDatasource.initializeDimension).toHaveBeenCalledWith(undefined, 'newId', {
+ expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith({}, 'newId', {
columnId: 'myColumn',
dataType: 'number',
groupId: 'testGroup',
@@ -441,20 +394,24 @@ describe('ConfigPanel', () => {
],
},
]);
- mockDatasource.initializeDimension = jest.fn();
+ datasourceMap.testDatasource.initializeDimension = jest.fn();
const { instance, lensStore } = await prepareAndMountComponent(props);
await clickToAddDimension(instance);
expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
- expect(mockDatasource.initializeDimension).toHaveBeenCalledWith('state', 'first', {
- groupId: 'a',
- columnId: 'newId',
- dataType: 'number',
- label: 'Initial value',
- staticValue: 100,
- });
+ expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith(
+ 'state',
+ 'first',
+ {
+ groupId: 'a',
+ columnId: 'newId',
+ dataType: 'number',
+ label: 'Initial value',
+ staticValue: 100,
+ }
+ );
});
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
index 57e4cf5b8dffd..0b6223ac87ce2 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-import './config_panel.scss';
-
import React, { useMemo, memo } from 'react';
import { EuiForm } from '@elastic/eui';
import { mapValues } from 'lodash';
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss
index 135286fc2172b..692db8171d124 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss
@@ -24,48 +24,24 @@
}
}
-.lnsDimensionContainer__footer {
- padding: $euiSizeS;
-
- .lnsFrameLayout__sidebar-isFullscreen & {
- display: none;
- }
-}
-
.lnsDimensionContainer__header {
- padding: $euiSizeS $euiSizeXS;
+ padding: $euiSize;
.lnsFrameLayout__sidebar-isFullscreen & {
display: none;
}
}
-.lnsDimensionContainer__headerTitle {
- padding: $euiSizeS $euiSizeXS;
- cursor: pointer;
-
- &:hover {
- text-decoration: underline;
- }
+.lnsDimensionContainer__content {
+ @include euiYScroll;
+ flex: 1;
}
-.lnsDimensionContainer__headerLink {
- &:focus-within {
- background-color: transparentize($euiColorVis1, .9);
-
- .lnsDimensionContainer__headerTitle {
- text-decoration: underline;
- }
- }
-}
-
-.lnsDimensionContainer__backIcon {
- &:hover {
- transform: none !important; // sass-lint:disable-line no-important
- }
+.lnsDimensionContainer__footer {
+ padding: $euiSize;
- &:focus {
- background-color: transparent;
+ .lnsFrameLayout__sidebar-isFullscreen & {
+ display: none;
}
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
index c62b10093e6e5..f7402e78ebd96 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
@@ -98,13 +98,7 @@ export function DimensionContainer({
}}
>
-
+
+
-
- {panel}
-
+
+ {panel}
+
{i18n.translate('xpack.lens.dimensionContainer.close', {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
index 967e6e47c55f0..44cefb0bf8ec4 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
@@ -4,9 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { layerTypes } from '../../../../common';
-import { initialState } from '../../../state_management/lens_slice';
+import { LensAppState } from '../../../state_management/types';
import { removeLayer, appendLayer } from './layer_actions';
function createTestArgs(initialLayerIds: string[]) {
@@ -44,15 +43,14 @@ function createTestArgs(initialLayerIds: string[]) {
return {
state: {
- ...initialState,
activeDatasourceId: 'ds1',
datasourceStates,
title: 'foo',
visualization: {
- activeId: 'vis1',
+ activeId: 'testVis',
state: initialLayerIds,
},
- },
+ } as unknown as LensAppState,
activeVisualization,
datasourceMap: {
ds1: testDatasource('ds1'),
@@ -61,7 +59,7 @@ function createTestArgs(initialLayerIds: string[]) {
trackUiEvent,
stagedPreview: {
visualization: {
- activeId: 'vis1',
+ activeId: 'testVis',
state: initialLayerIds,
},
datasourceStates,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
index 7a1cbb8237f50..781a08d0f60bb 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
@@ -36,32 +36,32 @@
background: $euiColorLightestShade;
padding: $euiSize;
+ &:last-child {
+ border-radius: 0 0 $euiBorderRadius $euiBorderRadius;
+ }
+
// Add border to the top of the next same panel
& + & {
border-top: $euiBorderThin;
margin-top: 0;
}
- &:last-child {
- border-bottom-right-radius: $euiBorderRadius;
- border-bottom-left-radius: $euiBorderRadius;
+
+ & > * {
+ margin-bottom: 0;
}
-}
-.lnsLayerPanel__row--notSupportsMoreColumns {
- padding-bottom: 0;
+ // Targeting EUI class as we are unable to apply a class to this element in component
+ &,
+ .euiFormRow__fieldWrapper {
+ & > * + * {
+ margin-top: $euiSize;
+ }
+ }
}
.lnsLayerPanel__group {
- padding: $euiSizeS 0;
- margin-bottom: $euiSizeS;
-}
-
-.lnsLayerPanel__group:empty {
- padding: 0;
-}
-
-.lnsLayerPanel__error {
- padding: 0 $euiSizeS;
+ margin: (-$euiSizeS) (-$euiSize);
+ padding: $euiSizeS $euiSize;
}
.lnsLayerPanel__dimension {
@@ -87,11 +87,10 @@
}
.lnsLayerPanel__dimensionContainer {
- margin: 0 0 $euiSizeS;
position: relative;
- &:last-child {
- margin-bottom: 0;
+ & + & {
+ margin-top: $euiSizeS;
}
}
@@ -105,6 +104,10 @@
min-height: $euiSizeXXL - 2;
word-break: break-word;
font-weight: $euiFontWeightRegular;
+
+ @include kbnThemeStyle('v7') {
+ font-size: $euiFontSizeS;
+ }
}
.lnsLayerPanel__triggerTextLabel {
@@ -119,7 +122,8 @@
}
.lnsLayerPanel__styleEditor {
- padding: 0 $euiSizeS $euiSizeS;
+ margin-top: -$euiSizeS;
+ padding: 0 $euiSize $euiSize;
}
.lnsLayerPanel__colorIndicator {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
index f777fd0976dfd..5b432f85efde2 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
@@ -222,7 +222,7 @@ describe('LayerPanel', () => {
const group = instance
.find(EuiFormRow)
- .findWhere((e) => e.prop('error')?.props?.children === 'Required dimension');
+ .findWhere((e) => e.prop('error') === 'Required dimension');
expect(group).toHaveLength(1);
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 6e2e77af4f3b0..8d19620cebbdc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -326,12 +326,7 @@ export function LayerPanel(
className="lnsLayerPanel"
style={{ visibility: isDimensionPanelOpen ? 'hidden' : 'visible' }}
>
-
+
@@ -394,16 +389,13 @@ export function LayerPanel(
{groups.map((group, groupIndex) => {
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
+
return (
+ <>
{group.groupLabel}
{group.groupTooltip && (
<>
@@ -420,92 +412,91 @@ export function LayerPanel(
/>
>
)}
-
+ >
}
labelType="legend"
key={group.groupId}
isInvalid={isMissing}
error={
- isMissing ? (
-
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
+ isMissing
+ ? i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
defaultMessage: 'Required dimension',
- })}
-
- ) : (
- []
- )
+ })
+ : []
}
>
<>
-
- {group.accessors.map((accessorConfig, accessorIndex) => {
- const { columnId } = accessorConfig;
-
- return (
-
-
-
{
- setActiveDimension({
- isNew: false,
- activeGroup: group,
- activeId: id,
- });
- }}
- onRemoveClick={(id: string) => {
- trackUiEvent('indexpattern_dimension_removed');
- props.updateAll(
- datasourceId,
- layerDatasource.removeColumn({
- layerId,
- columnId: id,
- prevState: layerDatasourceState,
- }),
- activeVisualization.removeDimension({
- layerId,
- columnId: id,
- prevState: props.visualizationState,
- frame: framePublicAPI,
- })
- );
- removeButtonRef(id);
- }}
- >
-
+ {group.accessors.map((accessorConfig, accessorIndex) => {
+ const { columnId } = accessorConfig;
+
+ return (
+
+
+ {
+ setActiveDimension({
+ isNew: false,
+ activeGroup: group,
+ activeId: id,
+ });
}}
- />
-
-
-
- );
- })}
-
+ onRemoveClick={(id: string) => {
+ trackUiEvent('indexpattern_dimension_removed');
+ props.updateAll(
+ datasourceId,
+ layerDatasource.removeColumn({
+ layerId,
+ columnId: id,
+ prevState: layerDatasourceState,
+ }),
+ activeVisualization.removeDimension({
+ layerId,
+ columnId: id,
+ prevState: props.visualizationState,
+ frame: framePublicAPI,
+ })
+ );
+ removeButtonRef(id);
+ }}
+ >
+
+
+
+
+ );
+ })}
+
+ ) : null}
+
{group.supportsMoreColumns ? (
{
};
const lensStore = (
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
activeDatasourceId: 'testDatasource',
datasourceStates: {
@@ -196,7 +195,6 @@ describe('editor_frame', () => {
};
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: initialState },
},
@@ -228,7 +226,6 @@ describe('editor_frame', () => {
};
instance = (
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: {} },
datasourceStates: {
@@ -283,7 +280,6 @@ describe('editor_frame', () => {
instance = (
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: {} },
datasourceStates: {
@@ -395,7 +391,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
activeDatasourceId: 'testDatasource',
visualization: { activeId: mockVisualization.id, state: {} },
@@ -437,7 +432,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider( , { data: props.plugins.data });
+ await mountWithProvider( );
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
@@ -474,7 +469,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: { visualization: { activeId: mockVisualization.id, state: {} } },
});
@@ -523,7 +517,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -587,8 +580,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider( , { data: props.plugins.data }))
- .instance;
+ instance = (await mountWithProvider( )).instance;
// necessary to flush elements to dom synchronously
instance.update();
@@ -692,7 +684,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider( , { data: props.plugins.data });
+ await mountWithProvider( );
expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
@@ -725,7 +717,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider( , { data: props.plugins.data });
+ await mountWithProvider( );
expect(mockVisualization.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
@@ -793,8 +785,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider( , { data: props.plugins.data }))
- .instance;
+ instance = (await mountWithProvider( )).instance;
expect(
instance
@@ -840,8 +831,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider( , { data: props.plugins.data }))
- .instance;
+ instance = (await mountWithProvider( )).instance;
act(() => {
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
@@ -898,8 +888,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider( , { data: props.plugins.data }))
- .instance;
+ instance = (await mountWithProvider( )).instance;
act(() => {
instance.find('[data-test-subj="lnsWorkspace"]').last().simulate('drop');
@@ -968,7 +957,6 @@ describe('editor_frame', () => {
} as EditorFrameProps;
instance = (
await mountWithProvider( , {
- data: props.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -1080,11 +1068,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
} as EditorFrameProps;
- instance = (
- await mountWithProvider( , {
- data: props.plugins.data,
- })
- ).instance;
+ instance = (await mountWithProvider( )).instance;
act(() => {
instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index 3b55c4923f967..c68c04b4b3e21 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -76,7 +76,7 @@ export function EditorFrame(props: EditorFrameProps) {
const suggestion = getSuggestionForField.current!(field);
if (suggestion) {
trackUiEvent('drop_onto_workspace');
- switchToSuggestion(dispatchLens, suggestion, 'SWITCH_VISUALIZATION');
+ switchToSuggestion(dispatchLens, suggestion, true);
}
},
[getSuggestionForField, dispatchLens]
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
index 91793d1f6cb71..c8c0a6e2ebbd2 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
@@ -106,11 +106,8 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */
}
.lnsConfigPanel {
- @include euiScrollBar;
- padding: $euiSize $euiSizeXS $euiSizeXL $euiSize;
- overflow-x: hidden;
- overflow-y: scroll;
- padding-left: $euiFormMaxWidth + $euiSize;
+ @include euiYScroll;
+ padding: $euiSize $euiSize $euiSizeXL ($euiFormMaxWidth + $euiSize);
margin-left: -$euiFormMaxWidth;
@include euiBreakpoint('xs', 's', 'm') {
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 5e059a1e2e8b1..a09cf269f753c 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
@@ -57,7 +57,7 @@ export async function initializeDatasources(
return states;
}
-export const createDatasourceLayers = memoizeOne(function createDatasourceLayers(
+export const getDatasourceLayers = memoizeOne(function getDatasourceLayers(
datasourceStates: DatasourceStates,
datasourceMap: DatasourceMap
) {
@@ -111,7 +111,7 @@ export async function persistedStateToExpression(
{ isFullEditor: false }
);
- const datasourceLayers = createDatasourceLayers(datasourceStates, datasourceMap);
+ const datasourceLayers = getDatasourceLayers(datasourceStates, datasourceMap);
const datasourceId = getActiveDatasourceIdFromDoc(doc);
if (datasourceId == null) {
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 90fa2ab080dd2..0d68e2d72e73b 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
@@ -46,7 +46,7 @@ describe('suggestion helpers', () => {
]);
const suggestedState = {};
const visualizationMap = {
- vis1: {
+ testVis: {
...mockVisualization,
getSuggestions: () => [
{
@@ -60,7 +60,7 @@ describe('suggestion helpers', () => {
};
const suggestions = getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -76,7 +76,7 @@ describe('suggestion helpers', () => {
generateSuggestion(),
]);
const visualizationMap = {
- vis1: {
+ testVis: {
...mockVisualization1,
getSuggestions: () => [
{
@@ -107,7 +107,7 @@ describe('suggestion helpers', () => {
};
const suggestions = getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -119,11 +119,11 @@ describe('suggestion helpers', () => {
datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([generateSuggestion()]);
const droppedField = {};
const visualizationMap = {
- vis1: createMockVisualization(),
+ testVis: createMockVisualization(),
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -153,12 +153,12 @@ describe('suggestion helpers', () => {
mock3: createMockDatasource('a'),
};
const visualizationMap = {
- vis1: createMockVisualization(),
+ testVis: createMockVisualization(),
};
const droppedField = {};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap: multiDatasourceMap,
datasourceStates: multiDatasourceStates,
@@ -183,12 +183,12 @@ describe('suggestion helpers', () => {
]);
const visualizationMap = {
- vis1: createMockVisualization(),
+ testVis: createMockVisualization(),
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -226,11 +226,11 @@ describe('suggestion helpers', () => {
};
const visualizationMap = {
- vis1: createMockVisualization(),
+ testVis: createMockVisualization(),
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap: multiDatasourceMap,
datasourceStates: multiDatasourceStates,
@@ -258,7 +258,7 @@ describe('suggestion helpers', () => {
generateSuggestion(),
]);
const visualizationMap = {
- vis1: {
+ testVis: {
...mockVisualization1,
getSuggestions: () => [
{
@@ -289,7 +289,7 @@ describe('suggestion helpers', () => {
};
const suggestions = getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -319,12 +319,12 @@ describe('suggestion helpers', () => {
{ state: {}, table: table2, keptLayerIds: ['first'] },
]);
const visualizationMap = {
- vis1: mockVisualization1,
+ testVis: mockVisualization1,
vis2: mockVisualization2,
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -372,7 +372,7 @@ describe('suggestion helpers', () => {
},
]);
const visualizationMap = {
- vis1: {
+ testVis: {
...mockVisualization1,
getSuggestions: vis1Suggestions,
},
@@ -384,7 +384,7 @@ describe('suggestion helpers', () => {
const suggestions = getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -407,13 +407,13 @@ describe('suggestion helpers', () => {
]);
const visualizationMap = {
- vis1: mockVisualization1,
+ testVis: mockVisualization1,
vis2: mockVisualization2,
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -439,12 +439,12 @@ describe('suggestion helpers', () => {
generateSuggestion(1),
]);
const visualizationMap = {
- vis1: mockVisualization1,
+ testVis: mockVisualization1,
vis2: mockVisualization2,
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -472,13 +472,13 @@ describe('suggestion helpers', () => {
generateSuggestion(1),
]);
const visualizationMap = {
- vis1: mockVisualization1,
+ testVis: mockVisualization1,
vis2: mockVisualization2,
};
getSuggestions({
visualizationMap,
- activeVisualization: visualizationMap.vis1,
+ activeVisualization: visualizationMap.testVis,
visualizationState: {},
datasourceMap,
datasourceStates,
@@ -542,9 +542,9 @@ describe('suggestion helpers', () => {
getOperationForColumnId: jest.fn(),
},
},
- { activeId: 'vis1', state: {} },
- { mockindexpattern: { state: mockDatasourceState, isLoading: false } },
- { vis1: mockVisualization1 },
+ { activeId: 'testVis', state: {} },
+ { testDatasource: { state: mockDatasourceState, isLoading: false } },
+ { testVis: mockVisualization1 },
datasourceMap.mock,
{ id: 'myfield', humanData: { label: 'myfieldLabel' } },
];
@@ -574,7 +574,7 @@ describe('suggestion helpers', () => {
it('should return nothing if datasource does not produce suggestions', () => {
datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]);
defaultParams[3] = {
- vis1: { ...mockVisualization1, getSuggestions: () => [] },
+ testVis: { ...mockVisualization1, getSuggestions: () => [] },
vis2: mockVisualization2,
};
const result = getTopSuggestionForField(...defaultParams);
@@ -583,7 +583,7 @@ describe('suggestion helpers', () => {
it('should not consider suggestion from other visualization if there is data', () => {
defaultParams[3] = {
- vis1: { ...mockVisualization1, getSuggestions: () => [] },
+ testVis: { ...mockVisualization1, getSuggestions: () => [] },
vis2: mockVisualization2,
};
const result = getTopSuggestionForField(...defaultParams);
@@ -609,7 +609,7 @@ describe('suggestion helpers', () => {
},
]);
defaultParams[3] = {
- vis1: mockVisualization1,
+ testVis: mockVisualization1,
vis2: mockVisualization2,
vis3: mockVisualization3,
};
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 a5c7871f33dfc..7f1e4aa58dba3 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
@@ -25,7 +25,6 @@ import { LayerType, layerTypes } from '../../../common';
import { getLayerType } from './config_panel/add_layer';
import {
LensDispatch,
- selectSuggestion,
switchVisualization,
DatasourceStates,
VisualizationState,
@@ -164,24 +163,21 @@ export function getVisualizeFieldSuggestions({
datasourceMap,
datasourceStates,
visualizationMap,
- activeVisualization,
- visualizationState,
visualizeTriggerFieldContext,
}: {
datasourceMap: DatasourceMap;
datasourceStates: DatasourceStates;
visualizationMap: VisualizationMap;
- activeVisualization: Visualization;
subVisualizationId?: string;
- visualizationState: unknown;
visualizeTriggerFieldContext?: VisualizeFieldContext;
}): Suggestion | undefined {
+ const activeVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null;
const suggestions = getSuggestions({
datasourceMap,
datasourceStates,
visualizationMap,
activeVisualization,
- visualizationState,
+ visualizationState: undefined,
visualizeTriggerFieldContext,
});
if (suggestions.length) {
@@ -230,19 +226,18 @@ export function switchToSuggestion(
Suggestion,
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId'
>,
- type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION'
+ clearStagedPreview?: boolean
) {
- const pickedSuggestion = {
- newVisualizationId: suggestion.visualizationId,
- initialState: suggestion.visualizationState,
- datasourceState: suggestion.datasourceState,
- datasourceId: suggestion.datasourceId!,
- };
-
dispatchLens(
- type === 'SELECT_SUGGESTION'
- ? selectSuggestion(pickedSuggestion)
- : switchVisualization(pickedSuggestion)
+ switchVisualization({
+ suggestion: {
+ newVisualizationId: suggestion.visualizationId,
+ visualizationState: suggestion.visualizationState,
+ datasourceState: suggestion.datasourceState,
+ datasourceId: suggestion.datasourceId!,
+ },
+ clearStagedPreview,
+ })
);
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
index b63d2956cfe6b..26e0be3555714 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
@@ -214,16 +214,17 @@ describe('suggestion_panel', () => {
act(() => {
instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
});
- // instance.update();
expect(lensStore.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
- type: 'lens/selectSuggestion',
+ type: 'lens/switchVisualization',
payload: {
- datasourceId: undefined,
- datasourceState: {},
- initialState: { suggestion1: true },
- newVisualizationId: 'testVis',
+ suggestion: {
+ datasourceId: undefined,
+ datasourceState: {},
+ visualizationState: { suggestion1: true },
+ newVisualizationId: 'testVis',
+ },
},
})
);
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 e7abf291b6eba..7cb97882a5e03 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
@@ -200,10 +200,13 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- initialState: 'suggestion visB',
- newVisualizationId: 'visB',
- datasourceId: 'testDatasource',
- datasourceState: {},
+ suggestion: {
+ visualizationState: 'suggestion visB',
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: {},
+ },
+ clearStagedPreview: true,
},
});
});
@@ -238,8 +241,11 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- initialState: 'visB initial state',
- newVisualizationId: 'visB',
+ suggestion: {
+ visualizationState: 'visB initial state',
+ newVisualizationId: 'visB',
+ },
+ clearStagedPreview: true,
},
});
expect(lensStore.dispatch).toHaveBeenCalledWith({
@@ -522,10 +528,13 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- datasourceId: undefined,
- datasourceState: undefined,
- initialState: 'visB initial state',
- newVisualizationId: 'visB',
+ suggestion: {
+ datasourceId: undefined,
+ datasourceState: undefined,
+ visualizationState: 'visB initial state',
+ newVisualizationId: 'visB',
+ },
+ clearStagedPreview: true,
},
});
});
@@ -598,10 +607,13 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- datasourceId: 'testDatasource',
- datasourceState: {},
- initialState: 'switched',
- newVisualizationId: 'visC',
+ suggestion: {
+ datasourceId: 'testDatasource',
+ datasourceState: {},
+ visualizationState: 'switched',
+ newVisualizationId: 'visC',
+ },
+ clearStagedPreview: true,
},
});
expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled();
@@ -694,10 +706,13 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- newVisualizationId: 'visB',
- datasourceId: 'testDatasource',
- datasourceState: 'testDatasource suggestion',
- initialState: 'suggestion visB',
+ suggestion: {
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: 'testDatasource suggestion',
+ visualizationState: 'suggestion visB',
+ },
+ clearStagedPreview: true,
},
});
});
@@ -731,10 +746,13 @@ describe('chart_switch', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- initialState: 'suggestion visB visB',
- newVisualizationId: 'visB',
- datasourceId: 'testDatasource',
- datasourceState: {},
+ suggestion: {
+ visualizationState: 'suggestion visB visB',
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: {},
+ },
+ clearStagedPreview: true,
},
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
index 51d4f2955a52b..a5ba12941cf7f 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
@@ -166,7 +166,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
...selection,
visualizationState: selection.getVisualizationState(),
},
- 'SWITCH_VISUALIZATION'
+ true
);
if (
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
index 4df3632c7f7da..2ed65d3b0f146 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
@@ -103,7 +103,6 @@ describe('workspace_panel', () => {
/>,
{
- data: defaultProps.plugins.data,
preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} },
}
);
@@ -121,7 +120,7 @@ describe('workspace_panel', () => {
}}
/>,
- { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } }
+ { preloadedState: { datasourceStates: {} } }
);
instance = mounted.instance;
@@ -138,7 +137,7 @@ describe('workspace_panel', () => {
}}
/>,
- { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } }
+ { preloadedState: { datasourceStates: {} } }
);
instance = mounted.instance;
@@ -165,8 +164,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -199,9 +197,7 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
plugins={{ ...props.plugins, uiActions: uiActionsMock }}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -233,9 +229,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -279,7 +273,6 @@ describe('workspace_panel', () => {
/>,
{
- data: defaultProps.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -360,9 +353,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
});
@@ -408,9 +399,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
});
@@ -456,7 +445,6 @@ describe('workspace_panel', () => {
/>,
{
- data: defaultProps.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -499,7 +487,6 @@ describe('workspace_panel', () => {
/>,
{
- data: defaultProps.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -543,7 +530,6 @@ describe('workspace_panel', () => {
/>,
{
- data: defaultProps.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@@ -582,9 +568,7 @@ describe('workspace_panel', () => {
visualizationMap={{
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -614,9 +598,7 @@ describe('workspace_panel', () => {
visualizationMap={{
testVis: mockVisualization,
}}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -648,9 +630,7 @@ describe('workspace_panel', () => {
visualizationMap={{
testVis: mockVisualization,
}}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -679,9 +659,7 @@ describe('workspace_panel', () => {
visualizationMap={{
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
@@ -709,9 +687,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
});
@@ -745,9 +721,7 @@ describe('workspace_panel', () => {
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
ExpressionRenderer={expressionRendererMock}
- />,
-
- { data: defaultProps.plugins.data }
+ />
);
instance = mounted.instance;
lensStore = mounted.lensStore;
@@ -832,10 +806,13 @@ describe('workspace_panel', () => {
expect(lensStore.dispatch).toHaveBeenCalledWith({
type: 'lens/switchVisualization',
payload: {
- newVisualizationId: 'testVis',
- initialState: {},
- datasourceState: {},
- datasourceId: 'testDatasource',
+ suggestion: {
+ newVisualizationId: 'testVis',
+ visualizationState: {},
+ datasourceState: {},
+ datasourceId: 'testDatasource',
+ },
+ clearStagedPreview: true,
},
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index c34e3c4137368..f1161b83c228e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -151,9 +151,9 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
? [
{
shortMessage: '',
- longMessage: i18n.translate('xpack.lens.indexPattern.missingIndexPattern', {
+ longMessage: i18n.translate('xpack.lens.indexPattern.missingDataView', {
defaultMessage:
- 'The {count, plural, one {index pattern} other {index patterns}} ({count, plural, one {id} other {ids}}: {indexpatterns}) cannot be found',
+ 'The {count, plural, one {data view} other {data views}} ({count, plural, one {id} other {ids}}: {indexpatterns}) cannot be found',
values: {
count: missingIndexPatterns.length,
indexpatterns: missingIndexPatterns.join(', '),
@@ -275,7 +275,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
if (suggestionForDraggedField) {
trackUiEvent('drop_onto_workspace');
trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty');
- switchToSuggestion(dispatchLens, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
+ switchToSuggestion(dispatchLens, suggestionForDraggedField, true);
}
}, [suggestionForDraggedField, expressionExists, dispatchLens]);
@@ -569,8 +569,8 @@ export const VisualizationWrapper = ({
})}
data-test-subj="configuration-failure-reconfigure-indexpatterns"
>
- {i18n.translate('xpack.lens.editorFrame.indexPatternReconfigure', {
- defaultMessage: `Recreate it in the index pattern management page`,
+ {i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
+ defaultMessage: `Recreate it in the data view management page`,
})}
@@ -580,8 +580,8 @@ export const VisualizationWrapper = ({
<>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
index 21e3f9aa36674..e57455e5bd5ab 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
@@ -1,20 +1,13 @@
@import '../../../mixins';
.lnsWorkspacePanelWrapper {
- @include euiScrollBar;
- // Override panel size padding
- padding: 0 !important; // sass-lint:disable-line no-important
margin-bottom: $euiSize;
display: flex;
flex-direction: column;
position: relative; // For positioning the dnd overlay
min-height: $euiSizeXXL * 10;
overflow: visible;
- border: none;
height: 100%;
- @include kbnThemeStyle('v7') {
- border: none !important; // sass-lint:disable-line no-important
- }
.lnsWorkspacePanelWrapper__pageContentBody {
@include euiScrollBar;
@@ -22,6 +15,17 @@
display: flex;
align-items: stretch;
justify-content: stretch;
+ border-radius: $euiBorderRadius;
+ background: $euiColorEmptyShade;
+ height: 100%;
+
+ @include kbnThemeStyle('v7') {
+ @include euiBottomShadowSmall;
+ }
+
+ @include kbnThemeStyle('v8') {
+ @include euiBottomShadowMedium;
+ }
> * {
flex: 1 1 100%;
@@ -38,10 +42,13 @@
}
.lnsWorkspacePanel__dragDrop {
- width: 100%;
- height: 100%;
- border: $euiBorderThin;
- border-radius: $euiBorderRadiusSmall;
+ @include kbnThemeStyle('v7') {
+ border: $euiBorderThin;
+ }
+
+ @include kbnThemeStyle('v8') {
+ border: $euiBorderWidthThin solid transparent;
+ }
&.lnsDragDrop-isDropTarget {
@include lnsDroppable;
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
index e1cb1aeb9f825..d8959e714d16e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
@@ -125,10 +125,15 @@ export function WorkspacePanelWrapper({
+
{children}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
index b19a295b68407..9df48d99ce762 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
@@ -160,9 +160,8 @@ export function getMissingCurrentDatasource() {
}
export function getMissingIndexPatterns(indexPatternIds: string[]) {
- return i18n.translate('xpack.lens.editorFrame.expressionMissingIndexPattern', {
- defaultMessage:
- 'Could not find the {count, plural, one {index pattern} other {index pattern}}: {ids}',
+ return i18n.translate('xpack.lens.editorFrame.expressionMissingDataView', {
+ defaultMessage: 'Could not find the {count, plural, one {data view} other {data views}}: {ids}',
values: { count: indexPatternIds.length, ids: indexPatternIds.join(', ') },
});
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx
index 64d7f5efc9c4d..ca44e833981ab 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx
@@ -69,8 +69,8 @@ export function ChangeIndexPattern({
>
- {i18n.translate('xpack.lens.indexPattern.changeIndexPatternTitle', {
- defaultMessage: 'Index pattern',
+ {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', {
+ defaultMessage: 'Data view',
})}
@@ -642,7 +642,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
iconType="boxesHorizontal"
data-test-subj="lnsIndexPatternActions"
aria-label={i18n.translate('xpack.lens.indexPatterns.actionsPopoverLabel', {
- defaultMessage: 'Index pattern settings',
+ defaultMessage: 'Data view settings',
})}
onClick={() => {
setPopoverOpen(!popoverOpen);
@@ -663,7 +663,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
}}
>
{i18n.translate('xpack.lens.indexPatterns.addFieldButton', {
- defaultMessage: 'Add field to index pattern',
+ defaultMessage: 'Add field to data view',
})}
,
{i18n.translate('xpack.lens.indexPatterns.manageFieldButton', {
- defaultMessage: 'Manage index pattern fields',
+ defaultMessage: 'Manage data view fields',
})}
,
]}
@@ -709,7 +709,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
data-test-subj="lnsIndexPatternFieldSearch"
placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
- description: 'Search the list of fields in the index pattern for the provided text',
+ description: 'Search the list of fields in the data view for the provided text',
})}
value={localState.nameFilter}
onChange={(e) => {
@@ -717,7 +717,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
}}
aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
- description: 'Search the list of fields in the index pattern for the provided text',
+ description: 'Search the list of fields in the data view for the provided text',
})}
aria-describedby={fieldSearchDescriptionId}
/>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss
index 874291ae25e34..30e2e00c7c85d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss
@@ -6,8 +6,8 @@
position: sticky;
top: 0;
background: $euiColorEmptyShade;
- // Raise it above the elements that are after it in DOM order
- z-index: $euiZLevel1;
+ z-index: $euiZLevel1; // Raise it above the elements that are after it in DOM order
+ padding: 0 $euiSize;
}
.lnsIndexPatternDimensionEditor-isFullscreen {
@@ -23,21 +23,14 @@
}
.lnsIndexPatternDimensionEditor__section--padded {
- padding: $euiSizeS;
+ padding: $euiSize;
}
.lnsIndexPatternDimensionEditor__section--shaded {
background-color: $euiColorLightestShade;
-}
-
-.lnsIndexPatternDimensionEditor__section--top {
border-bottom: $euiBorderThin;
}
-.lnsIndexPatternDimensionEditor__section--bottom {
- border-top: $euiBorderThin;
-}
-
.lnsIndexPatternDimensionEditor__columns {
column-count: 2;
column-gap: $euiSizeXL;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index 333e76f5a4f57..29bbe6a96b9e1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -422,7 +422,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
maxWidth={false}
/>
-
+
{!incompleteInfo &&
selectedColumn &&
@@ -636,8 +636,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
/>
)}
-
-
>
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx
index a39f3705fd230..dc6dc6dc31c86 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx
@@ -217,7 +217,7 @@ export function getErrorMessage(
}
if (fieldInvalid) {
return i18n.translate('xpack.lens.indexPattern.invalidFieldLabel', {
- defaultMessage: 'Invalid field. Check your index pattern or pick another field.',
+ defaultMessage: 'Invalid field. Check your data view or pick another field.',
});
}
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 9c22ec9d4bb05..ee6065aabf9d1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -348,7 +348,7 @@ function FieldPanelHeader({
@@ -366,7 +366,7 @@ function FieldPanelHeader({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index 2138b06a4c344..c408d0130825b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -98,8 +98,8 @@ export function getIndexPatternDatasource({
const uiSettings = core.uiSettings;
const onIndexPatternLoadError = (err: Error) =>
core.notifications.toasts.addError(err, {
- title: i18n.translate('xpack.lens.indexPattern.indexPatternLoadError', {
- defaultMessage: 'Error loading index pattern',
+ title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', {
+ defaultMessage: 'Error loading data view',
}),
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx
index 12536e556f306..28f2921ccc771 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx
@@ -23,8 +23,8 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter
const indexPattern = state.indexPatterns[layer.indexPatternId];
- const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingIndexPattern', {
- defaultMessage: 'Index pattern not found',
+ const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', {
+ defaultMessage: 'Data view not found',
});
return (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
index 69dc150922b4a..635c06691a733 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
@@ -16,7 +16,7 @@ describe('NoFieldCallout', () => {
`);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx
index 6b434e8cd41a6..073b21c700ccc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx
@@ -32,7 +32,7 @@ export const NoFieldsCallout = ({
size="s"
color="warning"
title={i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
- defaultMessage: 'No fields exist in this index pattern.',
+ defaultMessage: 'No fields exist in this data view.',
})}
/>
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
index d66e19bec8a1c..92a778ebfb803 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss
@@ -17,8 +17,6 @@
}
.lnsFormula__editor {
- border-bottom: $euiBorderThin;
-
.lnsIndexPatternDimensionEditor-isFullscreen & {
border-bottom: none;
display: flex;
@@ -32,7 +30,7 @@
.lnsFormula__editorHeader,
.lnsFormula__editorFooter {
- padding: $euiSizeS;
+ padding: $euiSizeS $euiSize;
}
.lnsFormula__editorFooter {
@@ -130,7 +128,7 @@
}
.lnsFormula__docsSearch {
- padding: $euiSizeS;
+ padding: $euiSize;
}
.lnsFormula__docsNav {
@@ -138,7 +136,7 @@
}
.lnsFormula__docsNavGroup {
- padding: $euiSizeS;
+ padding: $euiSize;
& + & {
border-top: $euiBorderThin;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
index 4f6f13ea843ef..7abe80003ea0e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -549,6 +549,8 @@ export function FormulaEditor({
dimension: { width: 320, height: 200 },
fixedOverflowWidgets: true,
matchBrackets: 'always',
+ // Undocumented Monaco option to force left margin width
+ lineDecorationsWidth: 16,
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
index 3c3068a595bc0..47dd8fbc9c569 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
@@ -18,6 +18,7 @@ import {
EuiTitle,
EuiFieldSearch,
EuiHighlight,
+ EuiSpacer,
} from '@elastic/eui';
import { Markdown } from '../../../../../../../../../src/plugins/kibana_react/public';
import { IndexPattern } from '../../../../types';
@@ -298,7 +299,7 @@ sum(products.base_price) / overall_sum(sum(products.base_price))
return (
<>
-
+
{i18n.translate('xpack.lens.formulaDocumentation.header', {
defaultMessage: 'Formula reference',
})}
@@ -347,22 +348,28 @@ sum(products.base_price) / overall_sum(sum(products.base_price))
-
- {helpGroup.items.map((helpItem) => {
- return (
- {helpItem.label}
- }
- size="s"
- onClick={() => {
- setSelectedFunction(helpItem.label);
- }}
- />
- );
- })}
-
+ {helpGroup.items.length ? (
+ <>
+
+
+
+ {helpGroup.items.map((helpItem) => {
+ return (
+ {helpItem.label}
+ }
+ size="s"
+ onClick={() => {
+ setSelectedFunction(helpItem.label);
+ }}
+ />
+ );
+ })}
+
+ >
+ ) : null}
);
})}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
index 77af42ab41888..d0dd8a438ed1c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
@@ -343,7 +343,7 @@ describe('last_value', () => {
'data'
);
expect(disabledStatus).toEqual(
- 'This function requires the presence of a date field in your index'
+ 'This function requires the presence of a date field in your data view'
);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
index 88c9d82092e21..9a3ba9a044148 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
@@ -134,7 +134,7 @@ export const lastValueOperation: OperationDefinition
@@ -284,7 +284,7 @@ export const lastValueOperation: OperationDefinition {
+export function createMockVisualization(id = 'testVis'): jest.Mocked {
return {
id,
clearLayer: jest.fn((state, _layerId) => state),
@@ -75,11 +82,12 @@ export function createMockVisualization(id = 'vis1'): jest.Mocked
groupLabel: `${id}Group`,
},
],
+ appendLayer: jest.fn(),
getVisualizationTypeId: jest.fn((_state) => 'empty'),
getDescription: jest.fn((_state) => ({ label: '' })),
switchVisualizationType: jest.fn((_, x) => x),
getSuggestions: jest.fn((_options) => []),
- initialize: jest.fn((_frame, _state?) => ({})),
+ initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })),
getConfiguration: jest.fn((props) => ({
groups: [
{
@@ -120,7 +128,7 @@ export function createMockDatasource(id: string): DatasourceMock {
};
return {
- id: 'mockindexpattern',
+ id: 'testDatasource',
clearLayer: jest.fn((state, _layerId) => state),
getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []),
getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []),
@@ -134,7 +142,7 @@ export function createMockDatasource(id: string): DatasourceMock {
renderDataPanel: jest.fn(),
renderLayerPanel: jest.fn(),
toExpression: jest.fn((_frame, _state) => null),
- insertLayer: jest.fn((_state, _newLayerId) => {}),
+ insertLayer: jest.fn((_state, _newLayerId) => ({})),
removeLayer: jest.fn((_state, _layerId) => {}),
removeColumn: jest.fn((props) => {}),
getLayers: jest.fn((_state) => []),
@@ -153,8 +161,9 @@ export function createMockDatasource(id: string): DatasourceMock {
};
}
-const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
-const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
+export const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
+export const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
+
export const datasourceMap = {
testDatasource2: mockDatasource2,
testDatasource: mockDatasource,
@@ -251,14 +260,41 @@ export function createMockTimefilter() {
};
}
-export function mockDataPlugin(sessionIdSubject = new Subject()) {
+export const exactMatchDoc = {
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'exactMatch',
+ },
+};
+
+export const mockStoreDeps = (deps?: {
+ lensServices?: LensAppServices;
+ datasourceMap?: DatasourceMap;
+ visualizationMap?: VisualizationMap;
+}) => {
+ return {
+ datasourceMap: deps?.datasourceMap || datasourceMap,
+ visualizationMap: deps?.visualizationMap || visualizationMap,
+ lensServices: deps?.lensServices || makeDefaultServices(),
+ };
+};
+
+export function mockDataPlugin(
+ sessionIdSubject = new Subject(),
+ initialSessionId?: string
+) {
function createMockSearchService() {
- let sessionIdCounter = 1;
+ let sessionIdCounter = initialSessionId ? 1 : 0;
+ let currentSessionId: string | undefined = initialSessionId;
+ const start = () => {
+ currentSessionId = `sessionId-${++sessionIdCounter}`;
+ return currentSessionId;
+ };
return {
session: {
- start: jest.fn(() => `sessionId-${sessionIdCounter++}`),
+ start: jest.fn(start),
clear: jest.fn(),
- getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`),
+ getSessionId: jest.fn(() => currentSessionId),
getSession$: jest.fn(() => sessionIdSubject.asObservable()),
},
};
@@ -296,7 +332,6 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) {
},
};
}
-
function createMockQueryString() {
return {
getQuery: jest.fn(() => ({ query: '', language: 'lucene' })),
@@ -328,6 +363,7 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) {
export function makeDefaultServices(
sessionIdSubject = new Subject(),
+ sessionId: string | undefined = undefined,
doc = defaultDoc
): jest.Mocked {
const core = coreMock.createStart({ basePath: '/testbasepath' });
@@ -365,13 +401,7 @@ export function makeDefaultServices(
},
core
);
-
- attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue({
- ...doc,
- sharingSavedObjectProps: {
- outcome: 'exactMatch',
- },
- });
+ attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc);
attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({
savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId,
});
@@ -402,7 +432,7 @@ export function makeDefaultServices(
},
getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`),
},
- data: mockDataPlugin(sessionIdSubject),
+ data: mockDataPlugin(sessionIdSubject, sessionId),
fieldFormats: fieldFormatsServiceMock.createStartContract(),
storage: {
get: jest.fn(),
@@ -432,44 +462,34 @@ export const defaultState = {
};
export function makeLensStore({
- data,
preloadedState,
dispatch,
+ storeDeps = mockStoreDeps(),
}: {
- data?: DataPublicPluginStart;
+ storeDeps?: LensStoreDeps;
preloadedState?: Partial;
dispatch?: jest.Mock;
}) {
- if (!data) {
- data = mockDataPlugin();
- }
- const lensStore = makeConfigureStore(
- {
- lensServices: { ...makeDefaultServices(), data },
- datasourceMap,
- visualizationMap,
+ const data = storeDeps.lensServices.data;
+ const store = makeConfigureStore(storeDeps, {
+ lens: {
+ ...defaultState,
+ query: data.query.queryString.getQuery(),
+ filters: data.query.filterManager.getGlobalFilters(),
+ resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
+ ...preloadedState,
},
- {
- lens: {
- ...defaultState,
- searchSessionId: data.search.session.start(),
- query: data.query.queryString.getQuery(),
- filters: data.query.filterManager.getGlobalFilters(),
- resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
- ...preloadedState,
- },
- } as DeepPartial
- );
+ } as DeepPartial);
- const origDispatch = lensStore.dispatch;
- lensStore.dispatch = jest.fn(dispatch || origDispatch);
- return lensStore;
+ const origDispatch = store.dispatch;
+ store.dispatch = jest.fn(dispatch || origDispatch);
+ return { store, deps: storeDeps };
}
export const mountWithProvider = async (
component: React.ReactElement,
store?: {
- data?: DataPublicPluginStart;
+ storeDeps?: LensStoreDeps;
preloadedState?: Partial;
dispatch?: jest.Mock;
},
@@ -480,7 +500,7 @@ export const mountWithProvider = async (
attachTo?: HTMLElement;
}
) => {
- const lensStore = makeLensStore(store || {});
+ const { store: lensStore, deps } = makeLensStore(store || {});
let wrappingComponent: React.FC<{
children: React.ReactNode;
@@ -510,5 +530,5 @@ export const mountWithProvider = async (
...restOptions,
} as unknown as ReactWrapper);
});
- return { instance, lensStore };
+ return { instance, lensStore, deps };
};
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index e79066b0145d2..8babb11c856b1 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -215,11 +215,11 @@ export function PieComponent(
},
});
- const [state, setState] = useState({ isReady: false });
+ const [isReady, setIsReady] = useState(false);
// It takes a cycle for the chart to render. This prevents
// reporting from printing a blank chart placeholder.
useEffect(() => {
- setState({ isReady: true });
+ setIsReady(true);
}, []);
const hasNegative = firstTable.rows.some((row) => {
@@ -273,7 +273,7 @@ export function PieComponent(
reportTitle={props.args.title}
reportDescription={props.args.description}
className="lnsPieExpression__container"
- isReady={state.isReady}
+ isReady={isReady}
>
-
+
+
-
- {children}
-
+
+ {children}
+
{i18n.translate('xpack.lens.table.palettePanelContainer.back', {
diff --git a/x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx b/x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx
index 0b361c8fa7f1e..5ab7800e05349 100644
--- a/x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx
+++ b/x-pack/plugins/lens/public/shared_components/tooltip_wrapper.tsx
@@ -10,6 +10,7 @@ import { EuiToolTip, EuiToolTipProps } from '@elastic/eui';
export type TooltipWrapperProps = Partial> & {
tooltipContent: string;
+ /** When the condition is truthy, the tooltip will be shown */
condition: boolean;
};
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap
similarity index 94%
rename from x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap
rename to x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap
index 32d221e14730b..57da18d9dc92f 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap
+++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`init_middleware should initialize all datasources with state from doc 1`] = `
+exports[`Initializing the store should initialize all datasources with state from doc 1`] = `
Object {
"lens": Object {
"activeDatasourceId": "testDatasource",
@@ -82,7 +82,7 @@ Object {
"fromDate": "2021-01-10T04:00:00.000Z",
"toDate": "2021-01-10T08:00:00.000Z",
},
- "searchSessionId": "sessionId-2",
+ "searchSessionId": "sessionId-1",
"sharingSavedObjectProps": Object {
"aliasTargetId": undefined,
"outcome": undefined,
diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts
index 1d8f4fdffa730..cc83cc612f32d 100644
--- a/x-pack/plugins/lens/public/state_management/index.ts
+++ b/x-pack/plugins/lens/public/state_management/index.ts
@@ -8,7 +8,7 @@
import { configureStore, getDefaultMiddleware, DeepPartial } from '@reduxjs/toolkit';
import { createLogger } from 'redux-logger';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
-import { lensSlice } from './lens_slice';
+import { makeLensReducer, lensActions } from './lens_slice';
import { timeRangeMiddleware } from './time_range_middleware';
import { optimizingMiddleware } from './optimizing_middleware';
import { LensState, LensStoreDeps } from './types';
@@ -16,10 +16,6 @@ import { initMiddleware } from './init_middleware';
export * from './types';
export * from './selectors';
-export const reducer = {
- lens: lensSlice.reducer,
-};
-
export const {
loadInitial,
navigateAway,
@@ -31,12 +27,12 @@ export const {
updateVisualizationState,
updateLayer,
switchVisualization,
- selectSuggestion,
rollbackSuggestion,
submitSuggestion,
switchDatasource,
setToggleFullscreen,
-} = lensSlice.actions;
+ initEmpty,
+} = lensActions;
export const makeConfigureStore = (
storeDeps: LensStoreDeps,
@@ -60,7 +56,9 @@ export const makeConfigureStore = (
}
return configureStore({
- reducer,
+ reducer: {
+ lens: makeLensReducer(storeDeps),
+ },
middleware,
preloadedState,
});
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
index 256684c5dbc25..a21388a67ce96 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
+++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts
@@ -7,7 +7,7 @@
import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import { LensStoreDeps } from '..';
-import { lensSlice } from '../lens_slice';
+import { loadInitial as loadInitialAction, navigateAway } from '..';
import { loadInitial } from './load_initial';
import { subscribeToExternalContext } from './subscribe_to_external_context';
@@ -18,9 +18,9 @@ export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAP
store.dispatch
);
return (next: Dispatch) => (action: PayloadAction) => {
- if (lensSlice.actions.loadInitial.match(action)) {
+ if (loadInitialAction.match(action)) {
return loadInitial(store, storeDeps, action.payload);
- } else if (lensSlice.actions.navigateAway.match(action)) {
+ } else if (navigateAway.match(action)) {
return unsubscribeFromExternalContext();
}
next(action);
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
deleted file mode 100644
index 342490e5360a5..0000000000000
--- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
+++ /dev/null
@@ -1,410 +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 {
- makeDefaultServices,
- makeLensStore,
- defaultDoc,
- createMockVisualization,
- createMockDatasource,
-} from '../../mocks';
-import { Location, History } from 'history';
-import { act } from 'react-dom/test-utils';
-import { loadInitial } from './load_initial';
-import { LensEmbeddableInput } from '../../embeddable';
-import { getPreloadedState } from '../lens_slice';
-import { LensAppState } from '..';
-import { LensAppServices } from '../../app_plugin/types';
-import { DatasourceMap, VisualizationMap } from '../../types';
-
-const defaultSavedObjectId = '1234';
-const preloadedState = {
- isLoading: true,
- visualization: {
- state: null,
- activeId: 'testVis',
- },
-};
-
-const exactMatchDoc = {
- ...defaultDoc,
- sharingSavedObjectProps: {
- outcome: 'exactMatch',
- },
-};
-
-const getDefaultLensServices = () => {
- const lensServices = makeDefaultServices();
- lensServices.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc);
- return lensServices;
-};
-
-const getStoreDeps = (deps?: {
- lensServices?: LensAppServices;
- datasourceMap?: DatasourceMap;
- visualizationMap?: VisualizationMap;
-}) => {
- const lensServices = deps?.lensServices || getDefaultLensServices();
- const datasourceMap = deps?.datasourceMap || {
- testDatasource2: createMockDatasource('testDatasource2'),
- testDatasource: createMockDatasource('testDatasource'),
- };
- const visualizationMap = deps?.visualizationMap || {
- testVis: {
- ...createMockVisualization(),
- id: 'testVis',
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'testVis',
- label: 'TEST1',
- groupLabel: 'testVisGroup',
- },
- ],
- },
- testVis2: {
- ...createMockVisualization(),
- id: 'testVis2',
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'testVis2',
- label: 'TEST2',
- groupLabel: 'testVis2Group',
- },
- ],
- },
- };
- return {
- datasourceMap,
- visualizationMap,
- lensServices,
- };
-};
-
-describe('init_middleware', () => {
- it('should initialize initial datasource', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices, datasourceMap } = storeDeps;
-
- const lensStore = await makeLensStore({
- data: lensServices.data,
- preloadedState,
- });
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput,
- });
- });
- expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled();
- });
-
- it('should have initialized the initial datasource and visualization', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices, datasourceMap, visualizationMap } = storeDeps;
-
- const lensStore = await makeLensStore({ data: lensServices.data, preloadedState });
- await act(async () => {
- await loadInitial(lensStore, storeDeps, { redirectCallback: jest.fn() });
- });
- expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled();
- expect(datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled();
- expect(visualizationMap.testVis.initialize).toHaveBeenCalled();
- expect(visualizationMap.testVis2.initialize).not.toHaveBeenCalled();
- });
-
- it('should initialize all datasources with state from doc', async () => {
- const datasource1State = { datasource1: '' };
- const datasource2State = { datasource2: '' };
- const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
- exactMatchDoc,
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource: datasource1State,
- testDatasource2: datasource2State,
- },
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- references: [],
- });
-
- const storeDeps = getStoreDeps({
- lensServices: services,
- visualizationMap: {
- testVis: {
- ...createMockVisualization(),
- id: 'testVis',
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'testVis',
- label: 'TEST1',
- groupLabel: 'testVisGroup',
- },
- ],
- },
- },
- datasourceMap: {
- testDatasource: createMockDatasource('testDatasource'),
- testDatasource2: createMockDatasource('testDatasource2'),
- testDatasource3: createMockDatasource('testDatasource3'),
- },
- });
- const { datasourceMap } = storeDeps;
-
- const lensStore = await makeLensStore({
- data: services.data,
- preloadedState,
- });
-
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput,
- });
- });
- expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled();
-
- expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith(
- datasource1State,
- [],
- undefined,
- {
- isFullEditor: true,
- }
- );
- expect(datasourceMap.testDatasource2.initialize).toHaveBeenCalledWith(
- datasource2State,
- [],
- undefined,
- {
- isFullEditor: true,
- }
- );
- expect(datasourceMap.testDatasource3.initialize).not.toHaveBeenCalled();
- expect(lensStore.getState()).toMatchSnapshot();
- });
-
- describe('loadInitial', () => {
- it('does not load a document if there is no initial input', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices } = storeDeps;
-
- const lensStore = makeLensStore({ data: lensServices.data, preloadedState });
- await loadInitial(lensStore, storeDeps, { redirectCallback: jest.fn() });
- expect(lensServices.attributeService.unwrapAttributes).not.toHaveBeenCalled();
- });
-
- it('cleans datasource and visualization state properly when reloading', async () => {
- const storeDeps = getStoreDeps();
- const lensStore = await makeLensStore({
- data: storeDeps.lensServices.data,
- preloadedState: {
- ...preloadedState,
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- datasourceStates: { testDatasource: { isLoading: false, state: {} } },
- },
- });
-
- expect(lensStore.getState()).toEqual({
- lens: expect.objectContaining({
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- activeDatasourceId: 'testDatasource',
- datasourceStates: {
- testDatasource: { isLoading: false, state: {} },
- },
- }),
- });
-
- const emptyState = getPreloadedState(storeDeps) as LensAppState;
- storeDeps.lensServices.attributeService.unwrapAttributes = jest.fn();
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: undefined,
- emptyState,
- });
- });
-
- expect(lensStore.getState()).toEqual({
- lens: expect.objectContaining({
- visualization: {
- activeId: 'testVis',
- state: null, // resets to null
- },
- activeDatasourceId: 'testDatasource2', // resets to first on the list
- datasourceStates: {
- testDatasource: { isLoading: false, state: undefined }, // state resets to undefined
- },
- }),
- });
- });
-
- it('loads a document and uses query and filters if initial input is provided', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices } = storeDeps;
- const emptyState = getPreloadedState(storeDeps) as LensAppState;
-
- const lensStore = await makeLensStore({ data: lensServices.data, preloadedState });
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- emptyState,
- });
- });
-
- expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
- savedObjectId: defaultSavedObjectId,
- });
-
- expect(lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([
- { query: { match_phrase: { src: 'test' } } },
- ]);
-
- expect(lensStore.getState()).toEqual({
- lens: expect.objectContaining({
- persistedDoc: { ...defaultDoc, type: 'lens' },
- query: 'kuery',
- isLoading: false,
- activeDatasourceId: 'testDatasource',
- }),
- });
- });
-
- it('does not load documents on sequential renders unless the id changes', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices } = storeDeps;
-
- const lensStore = makeLensStore({ data: lensServices.data, preloadedState });
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- });
- });
-
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- });
- });
-
- expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
-
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: { savedObjectId: '5678' } as unknown as LensEmbeddableInput,
- });
- });
-
- expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2);
- });
-
- it('handles document load errors', async () => {
- const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load');
-
- const storeDeps = getStoreDeps({ lensServices: services });
- const { lensServices } = storeDeps;
-
- const redirectCallback = jest.fn();
-
- const lensStore = makeLensStore({ data: lensServices.data, preloadedState });
-
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback,
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- });
- });
- expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
- savedObjectId: defaultSavedObjectId,
- });
- expect(lensServices.notifications.toasts.addDanger).toHaveBeenCalled();
- expect(redirectCallback).toHaveBeenCalled();
- });
-
- it('redirects if saved object is an aliasMatch', async () => {
- const services = makeDefaultServices();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
- ...defaultDoc,
- sharingSavedObjectProps: {
- outcome: 'aliasMatch',
- aliasTargetId: 'id2',
- },
- });
-
- const storeDeps = getStoreDeps({ lensServices: services });
- const lensStore = makeLensStore({ data: storeDeps.lensServices.data, preloadedState });
-
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- history: {
- location: {
- search: '?search',
- } as Location,
- } as History,
- });
- });
- expect(storeDeps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
- savedObjectId: defaultSavedObjectId,
- });
-
- expect(storeDeps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith(
- '#/edit/id2?search',
- 'Lens visualization'
- );
- });
-
- it('adds to the recently accessed list on load', async () => {
- const storeDeps = getStoreDeps();
- const { lensServices } = storeDeps;
-
- const lensStore = makeLensStore({ data: lensServices.data, preloadedState });
- await act(async () => {
- await loadInitial(lensStore, storeDeps, {
- redirectCallback: jest.fn(),
- initialInput: {
- savedObjectId: defaultSavedObjectId,
- } as unknown as LensEmbeddableInput,
- });
- });
-
- expect(lensServices.chrome.recentlyAccessed.add).toHaveBeenCalledWith(
- '/app/lens#/edit/1234',
- 'An extremely cool default document!',
- '1234'
- );
- });
- });
-});
diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
index 7db03a17a3a8f..314434a16af8c 100644
--- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
+++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts
@@ -9,17 +9,11 @@ import { MiddlewareAPI } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
-import { LensAppState, setState } from '..';
-import { updateLayer, updateVisualizationState, LensStoreDeps } from '..';
+import { LensAppState, setState, initEmpty, LensStoreDeps } from '..';
import { SharingSavedObjectProps } from '../../types';
import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable';
import { getInitialDatasourceId } from '../../utils';
import { initializeDatasources } from '../../editor_frame_service/editor_frame';
-import { generateId } from '../../id_generator';
-import {
- getVisualizeFieldSuggestions,
- switchToSuggestion,
-} from '../../editor_frame_service/editor_frame/suggestion_helpers';
import { LensAppServices } from '../../app_plugin/types';
import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants';
import { Document, injectFilterReferences } from '../../persistence';
@@ -89,13 +83,7 @@ export const getPersisted = async ({
export function loadInitial(
store: MiddlewareAPI,
- {
- lensServices,
- datasourceMap,
- visualizationMap,
- embeddableEditorIncomingState,
- initialContext,
- }: LensStoreDeps,
+ { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext }: LensStoreDeps,
{
redirectCallback,
initialInput,
@@ -108,78 +96,39 @@ export function loadInitial(
history?: History;
}
) {
- const { getState, dispatch } = store;
const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices;
- const { persistedDoc } = getState().lens;
+ const currentSessionId = data.search.session.getSessionId();
+ const { lens } = store.getState();
if (
!initialInput ||
(attributeService.inputIsRefType(initialInput) &&
- initialInput.savedObjectId === persistedDoc?.savedObjectId)
+ initialInput.savedObjectId === lens.persistedDoc?.savedObjectId)
) {
- return initializeDatasources(
- datasourceMap,
- getState().lens.datasourceStates,
- undefined,
- initialContext,
- {
- isFullEditor: true,
- }
- )
+ return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, {
+ isFullEditor: true,
+ })
.then((result) => {
- const datasourceStates = Object.entries(result).reduce(
- (state, [datasourceId, datasourceState]) => ({
- ...state,
- [datasourceId]: {
- ...datasourceState,
+ store.dispatch(
+ initEmpty({
+ newState: {
+ ...emptyState,
+ searchSessionId: currentSessionId || data.search.session.start(),
+ datasourceStates: Object.entries(result).reduce(
+ (state, [datasourceId, datasourceState]) => ({
+ ...state,
+ [datasourceId]: {
+ ...datasourceState,
+ isLoading: false,
+ },
+ }),
+ {}
+ ),
isLoading: false,
},
- }),
- {}
- );
- dispatch(
- setState({
- ...emptyState,
- datasourceStates,
- isLoading: false,
+ initialContext,
})
);
- if (initialContext) {
- const selectedSuggestion = getVisualizeFieldSuggestions({
- datasourceMap,
- datasourceStates,
- visualizationMap,
- activeVisualization: visualizationMap?.[Object.keys(visualizationMap)[0]] || null,
- visualizationState: null,
- visualizeTriggerFieldContext: initialContext,
- });
- if (selectedSuggestion) {
- switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
- }
- }
- const activeDatasourceId = getInitialDatasourceId(datasourceMap);
- const visualization = getState().lens.visualization;
- const activeVisualization =
- visualization.activeId && visualizationMap[visualization.activeId];
-
- if (visualization.state === null && activeVisualization) {
- const newLayerId = generateId();
-
- const initialVisualizationState = activeVisualization.initialize(() => newLayerId);
- dispatch(
- updateLayer({
- datasourceId: activeDatasourceId!,
- layerId: newLayerId,
- updater: datasourceMap[activeDatasourceId!].insertLayer,
- })
- );
- dispatch(
- updateVisualizationState({
- visualizationId: activeVisualization.id,
- updater: initialVisualizationState,
- })
- );
- }
})
.catch((e: { message: string }) => {
notifications.toasts.addDanger({
@@ -188,6 +137,7 @@ export function loadInitial(
redirectCallback();
});
}
+
getPersisted({ initialInput, lensServices, history })
.then(
(persisted) => {
@@ -226,11 +176,7 @@ export function loadInitial(
}
)
.then((result) => {
- const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc);
-
- const currentSessionId = data.search.session.getSessionId();
-
- dispatch(
+ store.dispatch(
setState({
sharingSavedObjectProps,
query: doc.state.query,
@@ -241,8 +187,8 @@ export function loadInitial(
currentSessionId
? currentSessionId
: data.search.session.start(),
- ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
- activeDatasourceId,
+ ...(!isEqual(lens.persistedDoc, doc) ? { persistedDoc: doc } : null),
+ activeDatasourceId: getInitialDatasourceId(datasourceMap, doc),
visualization: {
activeId: doc.visualizationType,
state: doc.state.visualization,
@@ -271,7 +217,7 @@ export function loadInitial(
}
},
() => {
- dispatch(
+ store.dispatch(
setState({
isLoading: false,
})
@@ -279,9 +225,10 @@ export function loadInitial(
redirectCallback();
}
)
- .catch((e: { message: string }) =>
+ .catch((e: { message: string }) => {
notifications.toasts.addDanger({
title: e.message,
- })
- );
+ });
+ redirectCallback();
+ });
}
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts
index cce0376707143..7d88e6ceb616c 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts
@@ -17,13 +17,9 @@ import {
import { makeLensStore, defaultState } from '../mocks';
describe('lensSlice', () => {
- const store = makeLensStore({});
+ const { store } = makeLensStore({});
const customQuery = { query: 'custom' } as Query;
- // TODO: need to move some initialization logic from mounter
- // describe('initialization', () => {
- // })
-
describe('state update', () => {
it('setState: updates state ', () => {
const lensState = store.getState().lens;
@@ -79,8 +75,11 @@ describe('lensSlice', () => {
const newVisState = {};
store.dispatch(
switchVisualization({
- newVisualizationId: 'testVis2',
- initialState: newVisState,
+ suggestion: {
+ newVisualizationId: 'testVis2',
+ visualizationState: newVisState,
+ },
+ clearStagedPreview: true,
})
);
@@ -93,10 +92,13 @@ describe('lensSlice', () => {
store.dispatch(
switchVisualization({
- newVisualizationId: 'testVis2',
- initialState: newVisState,
- datasourceState: newDatasourceState,
- datasourceId: 'testDatasource',
+ suggestion: {
+ newVisualizationId: 'testVis2',
+ visualizationState: newVisState,
+ datasourceState: newDatasourceState,
+ datasourceId: 'testDatasource',
+ },
+ clearStagedPreview: true,
})
);
@@ -117,7 +119,7 @@ describe('lensSlice', () => {
it('not initialize already initialized datasource on switch', () => {
const datasource2State = {};
- const customStore = makeLensStore({
+ const { store: customStore } = makeLensStore({
preloadedState: {
datasourceStates: {
testDatasource: {
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
index 6cf0529b34575..0461070020055 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -5,12 +5,18 @@
* 2.0.
*/
-import { createSlice, current, PayloadAction } from '@reduxjs/toolkit';
+import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
+import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
import { History } from 'history';
import { LensEmbeddableInput } from '..';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { getInitialDatasourceId, getResolvedDateRange } from '../utils';
import { LensAppState, LensStoreDeps } from './types';
+import { generateId } from '../id_generator';
+import {
+ getVisualizeFieldSuggestions,
+ Suggestion,
+} from '../editor_frame_service/editor_frame/suggestion_helpers';
export const initialState: LensAppState = {
persistedDoc: undefined,
@@ -68,29 +74,105 @@ export const getPreloadedState = ({
return state;
};
-export const lensSlice = createSlice({
- name: 'lens',
- initialState,
- reducers: {
- setState: (state, { payload }: PayloadAction>) => {
+export const setState = createAction>('lens/setState');
+export const onActiveDataChange = createAction('lens/onActiveDataChange');
+export const setSaveable = createAction('lens/setSaveable');
+export const updateState = createAction<{
+ subType: string;
+ updater: (prevState: LensAppState) => LensAppState;
+}>('lens/updateState');
+export const updateDatasourceState = createAction<{
+ updater: unknown | ((prevState: unknown) => unknown);
+ datasourceId: string;
+ clearStagedPreview?: boolean;
+}>('lens/updateDatasourceState');
+export const updateVisualizationState = createAction<{
+ visualizationId: string;
+ updater: unknown;
+ clearStagedPreview?: boolean;
+}>('lens/updateVisualizationState');
+
+export const updateLayer = createAction<{
+ layerId: string;
+ datasourceId: string;
+ updater: (state: unknown, layerId: string) => unknown;
+}>('lens/updateLayer');
+
+export const switchVisualization = createAction<{
+ suggestion: {
+ newVisualizationId: string;
+ visualizationState: unknown;
+ datasourceState?: unknown;
+ datasourceId?: string;
+ };
+ clearStagedPreview?: boolean;
+}>('lens/switchVisualization');
+export const rollbackSuggestion = createAction('lens/rollbackSuggestion');
+export const setToggleFullscreen = createAction('lens/setToggleFullscreen');
+export const submitSuggestion = createAction('lens/submitSuggestion');
+export const switchDatasource = createAction<{
+ newDatasourceId: string;
+}>('lens/switchDatasource');
+export const navigateAway = createAction('lens/navigateAway');
+export const loadInitial = createAction<{
+ initialInput?: LensEmbeddableInput;
+ redirectCallback: (savedObjectId?: string) => void;
+ emptyState: LensAppState;
+ history: History;
+}>('lens/loadInitial');
+export const initEmpty = createAction(
+ 'initEmpty',
+ function prepare({
+ newState,
+ initialContext,
+ }: {
+ newState: Partial;
+ initialContext?: VisualizeFieldContext;
+ }) {
+ return { payload: { layerId: generateId(), newState, initialContext } };
+ }
+);
+
+export const lensActions = {
+ setState,
+ onActiveDataChange,
+ setSaveable,
+ updateState,
+ updateDatasourceState,
+ updateVisualizationState,
+ updateLayer,
+ switchVisualization,
+ rollbackSuggestion,
+ setToggleFullscreen,
+ submitSuggestion,
+ switchDatasource,
+ navigateAway,
+ loadInitial,
+ initEmpty,
+};
+
+export const makeLensReducer = (storeDeps: LensStoreDeps) => {
+ const { datasourceMap, visualizationMap } = storeDeps;
+ return createReducer(initialState, {
+ [setState.type]: (state, { payload }: PayloadAction>) => {
return {
...state,
...payload,
};
},
- onActiveDataChange: (state, { payload }: PayloadAction) => {
+ [onActiveDataChange.type]: (state, { payload }: PayloadAction) => {
return {
...state,
activeData: payload,
};
},
- setSaveable: (state, { payload }: PayloadAction) => {
+ [setSaveable.type]: (state, { payload }: PayloadAction) => {
return {
...state,
isSaveable: payload,
};
},
- updateState: (
+ [updateState.type]: (
state,
action: {
payload: {
@@ -101,7 +183,7 @@ export const lensSlice = createSlice({
) => {
return action.payload.updater(current(state) as LensAppState);
},
- updateDatasourceState: (
+ [updateDatasourceState.type]: (
state,
{
payload,
@@ -128,7 +210,7 @@ export const lensSlice = createSlice({
stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
};
},
- updateVisualizationState: (
+ [updateVisualizationState.type]: (
state,
{
payload,
@@ -161,7 +243,7 @@ export const lensSlice = createSlice({
stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
};
},
- updateLayer: (
+ [updateLayer.type]: (
state,
{
payload,
@@ -188,92 +270,65 @@ export const lensSlice = createSlice({
};
},
- switchVisualization: (
- state,
- {
- payload,
- }: {
- payload: {
- newVisualizationId: string;
- initialState: unknown;
- datasourceState?: unknown;
- datasourceId?: string;
- };
- }
- ) => {
- return {
- ...state,
- datasourceStates:
- 'datasourceId' in payload && payload.datasourceId
- ? {
- ...state.datasourceStates,
- [payload.datasourceId]: {
- ...state.datasourceStates[payload.datasourceId],
- state: payload.datasourceState,
- },
- }
- : state.datasourceStates,
- visualization: {
- ...state.visualization,
- activeId: payload.newVisualizationId,
- state: payload.initialState,
- },
- stagedPreview: undefined,
- };
- },
- selectSuggestion: (
+ [switchVisualization.type]: (
state,
{
payload,
}: {
payload: {
- newVisualizationId: string;
- initialState: unknown;
- datasourceState: unknown;
- datasourceId: string;
+ suggestion: {
+ newVisualizationId: string;
+ visualizationState: unknown;
+ datasourceState?: unknown;
+ datasourceId?: string;
+ };
+ clearStagedPreview?: boolean;
};
}
) => {
+ const { newVisualizationId, visualizationState, datasourceState, datasourceId } =
+ payload.suggestion;
return {
...state,
- datasourceStates:
- 'datasourceId' in payload && payload.datasourceId
- ? {
- ...state.datasourceStates,
- [payload.datasourceId]: {
- ...state.datasourceStates[payload.datasourceId],
- state: payload.datasourceState,
- },
- }
- : state.datasourceStates,
+ datasourceStates: datasourceId
+ ? {
+ ...state.datasourceStates,
+ [datasourceId]: {
+ ...state.datasourceStates[datasourceId],
+ state: datasourceState,
+ },
+ }
+ : state.datasourceStates,
visualization: {
...state.visualization,
- activeId: payload.newVisualizationId,
- state: payload.initialState,
- },
- stagedPreview: state.stagedPreview || {
- datasourceStates: state.datasourceStates,
- visualization: state.visualization,
+ activeId: newVisualizationId,
+ state: visualizationState,
},
+ stagedPreview: payload.clearStagedPreview
+ ? undefined
+ : state.stagedPreview || {
+ datasourceStates: state.datasourceStates,
+ visualization: state.visualization,
+ },
};
},
- rollbackSuggestion: (state) => {
+ [rollbackSuggestion.type]: (state) => {
return {
...state,
...(state.stagedPreview || {}),
stagedPreview: undefined,
};
},
- setToggleFullscreen: (state) => {
+ [setToggleFullscreen.type]: (state) => {
return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
},
- submitSuggestion: (state) => {
+ [submitSuggestion.type]: (state) => {
return {
...state,
stagedPreview: undefined,
};
},
- switchDatasource: (
+ [switchDatasource.type]: (
state,
{
payload,
@@ -295,8 +350,8 @@ export const lensSlice = createSlice({
activeDatasourceId: payload.newDatasourceId,
};
},
- navigateAway: (state) => state,
- loadInitial: (
+ [navigateAway.type]: (state) => state,
+ [loadInitial.type]: (
state,
payload: PayloadAction<{
initialInput?: LensEmbeddableInput;
@@ -305,9 +360,78 @@ export const lensSlice = createSlice({
history: History;
}>
) => state,
- },
-});
+ [initEmpty.type]: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ newState: Partial;
+ initialContext: VisualizeFieldContext | undefined;
+ layerId: string;
+ };
+ }
+ ) => {
+ const newState = {
+ ...state,
+ ...payload.newState,
+ };
+ const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({
+ datasourceMap,
+ datasourceStates: newState.datasourceStates,
+ visualizationMap,
+ visualizeTriggerFieldContext: payload.initialContext,
+ });
+ if (suggestion) {
+ return {
+ ...newState,
+ datasourceStates: {
+ ...newState.datasourceStates,
+ [suggestion.datasourceId!]: {
+ ...newState.datasourceStates[suggestion.datasourceId!],
+ state: suggestion.datasourceState,
+ },
+ },
+ visualization: {
+ ...newState.visualization,
+ activeId: suggestion.visualizationId,
+ state: suggestion.visualizationState,
+ },
+ stagedPreview: undefined,
+ };
+ }
+
+ const visualization = newState.visualization;
+
+ if (!visualization.activeId) {
+ throw new Error('Invariant: visualization state got updated without active visualization');
+ }
-export const reducer = {
- lens: lensSlice.reducer,
+ const activeVisualization = visualizationMap[visualization.activeId];
+ if (visualization.state === null && activeVisualization) {
+ const activeDatasourceId = getInitialDatasourceId(datasourceMap)!;
+ const newVisState = activeVisualization.initialize(() => payload.layerId);
+ const activeDatasource = datasourceMap[activeDatasourceId];
+ return {
+ ...newState,
+ activeDatasourceId,
+ datasourceStates: {
+ ...newState.datasourceStates,
+ [activeDatasourceId]: {
+ ...newState.datasourceStates[activeDatasourceId],
+ state: activeDatasource.insertLayer(
+ newState.datasourceStates[activeDatasourceId]?.state,
+ payload.layerId
+ ),
+ },
+ },
+ visualization: {
+ ...visualization,
+ state: newVisState,
+ },
+ };
+ }
+ return newState;
+ },
+ });
};
diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx
new file mode 100644
index 0000000000000..fe4c553ce4bd7
--- /dev/null
+++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx
@@ -0,0 +1,323 @@
+/*
+ * 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 {
+ makeDefaultServices,
+ makeLensStore,
+ defaultDoc,
+ createMockVisualization,
+ createMockDatasource,
+ mockStoreDeps,
+ exactMatchDoc,
+} from '../mocks';
+import { Location, History } from 'history';
+import { act } from 'react-dom/test-utils';
+import { LensEmbeddableInput } from '../embeddable';
+import { getPreloadedState, initialState, loadInitial } from './lens_slice';
+import { LensAppState } from '.';
+
+const history = {
+ location: {
+ search: '?search',
+ } as Location,
+} as History;
+
+const defaultSavedObjectId = '1234';
+const preloadedState = {
+ isLoading: true,
+ visualization: {
+ state: null,
+ activeId: 'testVis',
+ },
+};
+
+const defaultProps = {
+ redirectCallback: jest.fn(),
+ initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput,
+ history,
+ emptyState: initialState,
+};
+
+describe('Initializing the store', () => {
+ it('should initialize initial datasource', async () => {
+ const { store, deps } = await makeLensStore({ preloadedState });
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+ expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled();
+ });
+
+ it('should have initialized the initial datasource and visualization', async () => {
+ const { store, deps } = await makeLensStore({ preloadedState });
+ const emptyState = getPreloadedState(deps) as LensAppState;
+ await act(async () => {
+ await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined, emptyState }));
+ });
+ expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled();
+ expect(deps.datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled();
+ expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled();
+ expect(deps.visualizationMap.testVis2.initialize).not.toHaveBeenCalled();
+ });
+
+ it('should initialize all datasources with state from doc', async () => {
+ const datasource1State = { datasource1: '' };
+ const datasource2State = { datasource2: '' };
+ const services = makeDefaultServices();
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ exactMatchDoc,
+ visualizationType: 'testVis',
+ title: '',
+ state: {
+ datasourceStates: {
+ testDatasource: datasource1State,
+ testDatasource2: datasource2State,
+ },
+ visualization: {},
+ query: { query: '', language: 'lucene' },
+ filters: [],
+ },
+ references: [],
+ });
+
+ const storeDeps = mockStoreDeps({
+ lensServices: services,
+ visualizationMap: {
+ testVis: {
+ ...createMockVisualization(),
+ id: 'testVis',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis',
+ label: 'TEST1',
+ groupLabel: 'testVisGroup',
+ },
+ ],
+ },
+ },
+ datasourceMap: {
+ testDatasource: createMockDatasource('testDatasource'),
+ testDatasource2: createMockDatasource('testDatasource2'),
+ testDatasource3: createMockDatasource('testDatasource3'),
+ },
+ });
+
+ const { store, deps } = await makeLensStore({
+ storeDeps,
+ preloadedState,
+ });
+
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+ const { datasourceMap } = deps;
+ expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled();
+
+ expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith(
+ datasource1State,
+ [],
+ undefined,
+ {
+ isFullEditor: true,
+ }
+ );
+ expect(datasourceMap.testDatasource2.initialize).toHaveBeenCalledWith(
+ datasource2State,
+ [],
+ undefined,
+ {
+ isFullEditor: true,
+ }
+ );
+ expect(datasourceMap.testDatasource3.initialize).not.toHaveBeenCalled();
+ expect(store.getState()).toMatchSnapshot();
+ });
+
+ describe('loadInitial', () => {
+ it('does not load a document if there is no initial input', async () => {
+ const { deps, store } = makeLensStore({ preloadedState });
+ await act(async () => {
+ await store.dispatch(
+ loadInitial({
+ ...defaultProps,
+ initialInput: undefined,
+ })
+ );
+ });
+ expect(deps.lensServices.attributeService.unwrapAttributes).not.toHaveBeenCalled();
+ });
+
+ it('starts new searchSessionId', async () => {
+ const { store } = await makeLensStore({ preloadedState });
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+ expect(store.getState()).toEqual({
+ lens: expect.objectContaining({
+ searchSessionId: 'sessionId-1',
+ }),
+ });
+ });
+
+ it('cleans datasource and visualization state properly when reloading', async () => {
+ const { store, deps } = await makeLensStore({
+ preloadedState: {
+ ...preloadedState,
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ datasourceStates: { testDatasource: { isLoading: false, state: {} } },
+ },
+ });
+
+ expect(store.getState()).toEqual({
+ lens: expect.objectContaining({
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ activeDatasourceId: 'testDatasource',
+ datasourceStates: {
+ testDatasource: { isLoading: false, state: {} },
+ },
+ }),
+ });
+
+ const emptyState = getPreloadedState(deps) as LensAppState;
+
+ await act(async () => {
+ await store.dispatch(
+ loadInitial({
+ ...defaultProps,
+ emptyState,
+ initialInput: undefined,
+ })
+ );
+ });
+
+ expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled();
+ expect(store.getState()).toEqual({
+ lens: expect.objectContaining({
+ visualization: {
+ state: { newState: 'newState' }, // new vis gets initialized
+ activeId: 'testVis',
+ },
+ activeDatasourceId: 'testDatasource2', // resets to first on the list
+ datasourceStates: {
+ testDatasource: { isLoading: false, state: undefined }, // state resets to undefined
+ testDatasource2: {
+ state: {}, // initializes first in the map
+ },
+ },
+ }),
+ });
+ });
+
+ it('loads a document and uses query and filters if initial input is provided', async () => {
+ const { store, deps } = await makeLensStore({ preloadedState });
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+
+ expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
+ savedObjectId: defaultSavedObjectId,
+ });
+
+ expect(deps.lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([
+ { query: { match_phrase: { src: 'test' } } },
+ ]);
+
+ expect(store.getState()).toEqual({
+ lens: expect.objectContaining({
+ persistedDoc: { ...defaultDoc, type: 'lens' },
+ query: 'kuery',
+ isLoading: false,
+ activeDatasourceId: 'testDatasource',
+ }),
+ });
+ });
+
+ it('does not load documents on sequential renders unless the id changes', async () => {
+ const { store, deps } = makeLensStore({ preloadedState });
+
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+
+ expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ await store.dispatch(
+ loadInitial({
+ ...defaultProps,
+ initialInput: { savedObjectId: '5678' } as unknown as LensEmbeddableInput,
+ })
+ );
+ });
+
+ expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2);
+ });
+
+ it('handles document load errors', async () => {
+ const { store, deps } = makeLensStore({ preloadedState });
+
+ deps.lensServices.attributeService.unwrapAttributes = jest
+ .fn()
+ .mockRejectedValue('failed to load');
+ const redirectCallback = jest.fn();
+ await act(async () => {
+ await store.dispatch(loadInitial({ ...defaultProps, redirectCallback }));
+ });
+
+ expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
+ savedObjectId: defaultSavedObjectId,
+ });
+ expect(deps.lensServices.notifications.toasts.addDanger).toHaveBeenCalled();
+ expect(redirectCallback).toHaveBeenCalled();
+ });
+
+ it('redirects if saved object is an aliasMatch', async () => {
+ const { store, deps } = makeLensStore({ preloadedState });
+ deps.lensServices.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
+ ...defaultDoc,
+ sharingSavedObjectProps: {
+ outcome: 'aliasMatch',
+ aliasTargetId: 'id2',
+ },
+ });
+
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+
+ expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({
+ savedObjectId: defaultSavedObjectId,
+ });
+ expect(deps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith(
+ '#/edit/id2?search',
+ 'Lens visualization'
+ );
+ });
+
+ it('adds to the recently accessed list on load', async () => {
+ const { store, deps } = makeLensStore({ preloadedState });
+ await act(async () => {
+ await store.dispatch(loadInitial(defaultProps));
+ });
+
+ expect(deps.lensServices.chrome.recentlyAccessed.add).toHaveBeenCalledWith(
+ '/app/lens#/edit/1234',
+ 'An extremely cool default document!',
+ '1234'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts b/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
index f1293a84255b9..610be69166ba7 100644
--- a/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
+++ b/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
@@ -7,12 +7,12 @@
import { Dispatch, MiddlewareAPI, Action } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
-import { lensSlice } from './lens_slice';
+import { onActiveDataChange } from '.';
/** cancels updates to the store that don't change the state */
export const optimizingMiddleware = () => (store: MiddlewareAPI) => {
return (next: Dispatch) => (action: Action) => {
- if (lensSlice.actions.onActiveDataChange.match(action)) {
+ if (onActiveDataChange.match(action)) {
if (isEqual(store.getState().lens.activeData, action.payload)) {
return;
}
diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts
index 360ca48c2d279..c1d1700d8b3b5 100644
--- a/x-pack/plugins/lens/public/state_management/selectors.ts
+++ b/x-pack/plugins/lens/public/state_management/selectors.ts
@@ -10,7 +10,7 @@ import { SavedObjectReference } from 'kibana/server';
import { LensState } from './types';
import { extractFilterReferences } from '../persistence';
import { Datasource, DatasourceMap, VisualizationMap } from '../types';
-import { createDatasourceLayers } from '../editor_frame_service/editor_frame';
+import { getDatasourceLayers } from '../editor_frame_service/editor_frame';
export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc;
export const selectQuery = (state: LensState) => state.lens.query;
@@ -141,13 +141,13 @@ export const selectAreDatasourcesLoaded = createSelector(
export const selectDatasourceLayers = createSelector(
[selectDatasourceStates, selectDatasourceMap],
- (datasourceStates, datasourceMap) => createDatasourceLayers(datasourceStates, datasourceMap)
+ (datasourceStates, datasourceMap) => getDatasourceLayers(datasourceStates, datasourceMap)
);
export const selectFramePublicAPI = createSelector(
[selectDatasourceStates, selectActiveData, selectDatasourceMap],
(datasourceStates, activeData, datasourceMap) => ({
- datasourceLayers: createDatasourceLayers(datasourceStates, datasourceMap),
+ datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap),
activeData,
})
);
diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
index ddf50f6fd0d82..8ad6a300beaa4 100644
--- a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
+++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
@@ -14,117 +14,12 @@
import { timeRangeMiddleware } from './time_range_middleware';
-import { Observable, Subject } from 'rxjs';
-import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import moment from 'moment';
import { initialState } from './lens_slice';
import { LensAppState } from './types';
import { PayloadAction } from '@reduxjs/toolkit';
-
-const sessionIdSubject = new Subject();
-
-function createMockSearchService() {
- let sessionIdCounter = 1;
- return {
- session: {
- start: jest.fn(() => `sessionId-${sessionIdCounter++}`),
- clear: jest.fn(),
- getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`),
- getSession$: jest.fn(() => sessionIdSubject.asObservable()),
- },
- };
-}
-
-function createMockFilterManager() {
- const unsubscribe = jest.fn();
-
- let subscriber: () => void;
- let filters: unknown = [];
-
- return {
- getUpdates$: () => ({
- subscribe: ({ next }: { next: () => void }) => {
- subscriber = next;
- return unsubscribe;
- },
- }),
- setFilters: jest.fn((newFilters: unknown[]) => {
- filters = newFilters;
- if (subscriber) subscriber();
- }),
- setAppFilters: jest.fn((newFilters: unknown[]) => {
- filters = newFilters;
- if (subscriber) subscriber();
- }),
- getFilters: () => filters,
- getGlobalFilters: () => {
- // @ts-ignore
- return filters.filter(esFilters.isFilterPinned);
- },
- removeAll: () => {
- filters = [];
- subscriber();
- },
- };
-}
-
-function createMockQueryString() {
- return {
- getQuery: jest.fn(() => ({ query: '', language: 'kuery' })),
- setQuery: jest.fn(),
- getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })),
- };
-}
-
-function createMockTimefilter() {
- const unsubscribe = jest.fn();
-
- let timeFilter = { from: 'now-7d', to: 'now' };
- let subscriber: () => void;
- return {
- getTime: jest.fn(() => timeFilter),
- setTime: jest.fn((newTimeFilter) => {
- timeFilter = newTimeFilter;
- if (subscriber) {
- subscriber();
- }
- }),
- getTimeUpdate$: () => ({
- subscribe: ({ next }: { next: () => void }) => {
- subscriber = next;
- return unsubscribe;
- },
- }),
- calculateBounds: jest.fn(() => ({
- min: moment('2021-01-10T04:00:00.000Z'),
- max: moment('2021-01-10T08:00:00.000Z'),
- })),
- getBounds: jest.fn(() => timeFilter),
- getRefreshInterval: () => {},
- getRefreshIntervalDefaults: () => {},
- getAutoRefreshFetch$: () => new Observable(),
- };
-}
-
-function makeDefaultData(): jest.Mocked {
- return {
- query: {
- filterManager: createMockFilterManager(),
- timefilter: {
- timefilter: createMockTimefilter(),
- },
- queryString: createMockQueryString(),
- state$: new Observable(),
- },
- indexPatterns: {
- get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })),
- },
- search: createMockSearchService(),
- nowProvider: {
- get: jest.fn(),
- },
- } as unknown as DataPublicPluginStart;
-}
+import { mockDataPlugin } from '../mocks';
const createMiddleware = (data: DataPublicPluginStart) => {
const middleware = timeRangeMiddleware(data);
@@ -142,7 +37,7 @@ const createMiddleware = (data: DataPublicPluginStart) => {
describe('timeRangeMiddleware', () => {
describe('time update', () => {
it('does update the searchSessionId when the state changes and too much time passed', () => {
- const data = makeDefaultData();
+ const data = mockDataPlugin();
(data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
(data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
from: 'now-2m',
@@ -176,7 +71,7 @@ describe('timeRangeMiddleware', () => {
expect(next).toHaveBeenCalledWith(action);
});
it('does not update the searchSessionId when the state changes and too little time has passed', () => {
- const data = makeDefaultData();
+ const data = mockDataPlugin();
// time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update)
(data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300));
(data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
@@ -202,7 +97,7 @@ describe('timeRangeMiddleware', () => {
expect(next).toHaveBeenCalledWith(action);
});
it('does not trigger another update when the update already contains searchSessionId', () => {
- const data = makeDefaultData();
+ const data = mockDataPlugin();
(data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
(data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
from: 'now-2m',
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
index 6326d8680757e..fe3137c905ffb 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
@@ -28,6 +28,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -55,9 +56,11 @@ exports[`xy_expression XYChart component it renders area 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -86,9 +89,11 @@ exports[`xy_expression XYChart component it renders area 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -252,6 +257,7 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -279,9 +285,11 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -310,9 +318,11 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -490,6 +500,7 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -517,9 +528,11 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -548,9 +561,11 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -728,6 +743,7 @@ exports[`xy_expression XYChart component it renders line 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -755,9 +771,11 @@ exports[`xy_expression XYChart component it renders line 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -786,9 +804,11 @@ exports[`xy_expression XYChart component it renders line 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -952,6 +972,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -979,9 +1000,11 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -1010,9 +1033,11 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -1184,6 +1209,7 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -1211,9 +1237,11 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -1242,9 +1270,11 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
@@ -1430,6 +1460,7 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
"color": undefined,
},
"barSeriesStyle": Object {},
+ "chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
@@ -1457,9 +1488,11 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": 0,
"visible": true,
},
@@ -1488,9 +1521,11 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
style={
Object {
"axisTitle": Object {
+ "padding": undefined,
"visible": true,
},
"tickLabel": Object {
+ "padding": undefined,
"rotation": -90,
"visible": false,
},
diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
index 1e00d821d9b30..30238507c3566 100644
--- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
@@ -8,9 +8,10 @@
import { uniq, mapValues } from 'lodash';
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import type { Datatable } from 'src/plugins/expressions';
+import { euiLightVars } from '@kbn/ui-shared-deps-src/theme';
import type { AccessorConfig, FramePublicAPI } from '../types';
import { getColumnToLabelMap } from './state_helpers';
-import type { FormatFactory } from '../../common';
+import { FormatFactory, LayerType, layerTypes } from '../../common';
import type { XYLayerConfig } from '../../common/expressions';
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
@@ -20,8 +21,11 @@ interface LayerColorConfig {
splitAccessor?: string;
accessors: string[];
layerId: string;
+ layerType: LayerType;
}
+export const defaultThresholdColor = euiLightVars.euiColorDarkShade;
+
export type ColorAssignments = Record<
string,
{
@@ -37,13 +41,15 @@ export function getColorAssignments(
): ColorAssignments {
const layersPerPalette: Record = {};
- layers.forEach((layer) => {
- const palette = layer.palette?.name || 'default';
- if (!layersPerPalette[palette]) {
- layersPerPalette[palette] = [];
- }
- layersPerPalette[palette].push(layer);
- });
+ layers
+ .filter(({ layerType }) => layerType === layerTypes.DATA)
+ .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) => {
@@ -111,6 +117,13 @@ export function getAccessorColorConfig(
triggerIcon: 'disabled',
};
}
+ if (layer.layerType === layerTypes.THRESHOLD) {
+ return {
+ columnId: accessor as string,
+ triggerIcon: 'color',
+ color: currentYConfig?.color || defaultThresholdColor,
+ };
+ }
const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]);
const rank = colorAssignments[currentPalette.name].getRank(
layer,
@@ -133,7 +146,7 @@ export function getAccessorColorConfig(
return {
columnId: accessor as string,
triggerIcon: customColor ? 'color' : 'disabled',
- color: customColor ? customColor : undefined,
+ color: customColor ?? undefined,
};
});
}
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index 863289c31bba4..0cea52b5d3c9e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -59,7 +59,11 @@ import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axe
import { getColorAssignments } from './color_assignment';
import { getXDomain, XyEndzones } from './x_domain';
import { getLegendAction } from './get_legend_action';
-import { ThresholdAnnotations } from './expression_thresholds';
+import {
+ computeChartMargins,
+ getThresholdRequiredPaddings,
+ ThresholdAnnotations,
+} from './expression_thresholds';
declare global {
interface Window {
@@ -191,20 +195,18 @@ function getIconForSeriesType(seriesType: SeriesType): IconType {
const MemoizedChart = React.memo(XYChart);
export function XYChartReportable(props: XYChartRenderProps) {
- const [state, setState] = useState({
- isReady: false,
- });
+ const [isReady, setIsReady] = useState(false);
// It takes a cycle for the XY chart to render. This prevents
// reporting from printing a blank chart placeholder.
useEffect(() => {
- setState({ isReady: true });
- }, [setState]);
+ setIsReady(true);
+ }, [setIsReady]);
return (
@@ -316,6 +318,12 @@ export function XYChart({
Boolean(isHistogramViz)
);
+ const yAxesMap = {
+ left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'),
+ right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
+ };
+ const thresholdPaddings = getThresholdRequiredPaddings(thresholdLayers, yAxesMap);
+
const getYAxesTitles = (
axisSeries: Array<{ layer: string; accessor: string }>,
groupId: string
@@ -332,23 +340,38 @@ export function XYChart({
);
};
- const getYAxesStyle = (groupId: string) => {
+ const getYAxesStyle = (groupId: 'left' | 'right') => {
+ const tickVisible =
+ groupId === 'right'
+ ? tickLabelsVisibilitySettings?.yRight
+ : tickLabelsVisibilitySettings?.yLeft;
+
const style = {
tickLabel: {
- visible:
- groupId === 'right'
- ? tickLabelsVisibilitySettings?.yRight
- : tickLabelsVisibilitySettings?.yLeft,
+ visible: tickVisible,
rotation:
groupId === 'right'
? args.labelsOrientation?.yRight || 0
: args.labelsOrientation?.yLeft || 0,
+ padding:
+ thresholdPaddings[groupId] != null
+ ? {
+ inner: thresholdPaddings[groupId],
+ }
+ : undefined,
},
axisTitle: {
visible:
groupId === 'right'
? axisTitlesVisibilitySettings?.yRight
: axisTitlesVisibilitySettings?.yLeft,
+ // if labels are not visible add the padding to the title
+ padding:
+ !tickVisible && thresholdPaddings[groupId] != null
+ ? {
+ inner: thresholdPaddings[groupId],
+ }
+ : undefined,
},
};
return style;
@@ -512,6 +535,17 @@ export function XYChart({
legend: {
labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
},
+ // if not title or labels are shown for axes, add some padding if required by threshold markers
+ chartMargins: {
+ ...chartTheme.chartPaddings,
+ ...computeChartMargins(
+ thresholdPaddings,
+ tickLabelsVisibilitySettings,
+ axisTitlesVisibilitySettings,
+ yAxesMap,
+ shouldRotate
+ ),
+ },
}}
baseTheme={chartBaseTheme}
tooltip={{
@@ -547,9 +581,15 @@ export function XYChart({
tickLabel: {
visible: tickLabelsVisibilitySettings?.x,
rotation: labelsOrientation?.x,
+ padding:
+ thresholdPaddings.bottom != null ? { inner: thresholdPaddings.bottom } : undefined,
},
axisTitle: {
visible: axisTitlesVisibilitySettings.x,
+ padding:
+ !tickLabelsVisibilitySettings?.x && thresholdPaddings.bottom != null
+ ? { inner: thresholdPaddings.bottom }
+ : undefined,
},
}}
/>
@@ -570,7 +610,7 @@ export function XYChart({
}}
hide={filteredLayers[0].hide}
tickFormat={(d) => axis.formatter?.convert(d) || ''}
- style={getYAxesStyle(axis.groupId)}
+ style={getYAxesStyle(axis.groupId as 'left' | 'right')}
domain={getYAxisDomain(axis)}
/>
);
@@ -838,14 +878,18 @@ export function XYChart({
groupId === 'left')?.formatter,
- right: yAxesConfiguration.find(({ groupId }) => groupId === 'right')?.formatter,
+ left: yAxesMap.left?.formatter,
+ right: yAxesMap.right?.formatter,
bottom: xAxisFormatter,
}}
+ axesMap={{
+ left: Boolean(yAxesMap.left),
+ right: Boolean(yAxesMap.right),
+ }}
+ isHorizontal={shouldRotate}
/>
) : null}
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx
index 171e2f1cfba9e..7532d41f091d1 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx
@@ -8,27 +8,144 @@
import React from 'react';
import { groupBy } from 'lodash';
import { EuiIcon } from '@elastic/eui';
-import { RectAnnotation, AnnotationDomainType, LineAnnotation } from '@elastic/charts';
-import type { PaletteRegistry, SeriesLayer } from 'src/plugins/charts/public';
+import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts';
+import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { FieldFormat } from 'src/plugins/field_formats/common';
-import type { LayerArgs } from '../../common/expressions';
+import { euiLightVars } from '@kbn/ui-shared-deps-src/theme';
+import type { LayerArgs, YConfig } from '../../common/expressions';
import type { LensMultiTable } from '../../common/types';
-import type { ColorAssignments } from './color_assignment';
+
+const THRESHOLD_ICON_SIZE = 20;
+
+export const computeChartMargins = (
+ thresholdPaddings: Partial>,
+ labelVisibility: Partial>,
+ titleVisibility: Partial>,
+ axesMap: Record<'left' | 'right', unknown>,
+ isHorizontal: boolean
+) => {
+ const result: Partial> = {};
+ if (!labelVisibility?.x && !titleVisibility?.x && thresholdPaddings.bottom) {
+ const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom';
+ result[placement] = thresholdPaddings.bottom;
+ }
+ if (
+ thresholdPaddings.left &&
+ (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft))
+ ) {
+ const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left';
+ result[placement] = thresholdPaddings.left;
+ }
+ if (
+ thresholdPaddings.right &&
+ (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight))
+ ) {
+ const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right';
+ result[placement] = thresholdPaddings.right;
+ }
+ // there's no top axis, so just check if a margin has been computed
+ if (thresholdPaddings.top) {
+ const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top';
+ result[placement] = thresholdPaddings.top;
+ }
+ return result;
+};
+
+function hasIcon(icon: string | undefined): icon is string {
+ return icon != null && icon !== 'none';
+}
+
+// Note: it does not take into consideration whether the threshold is in view or not
+export const getThresholdRequiredPaddings = (
+ thresholdLayers: LayerArgs[],
+ axesMap: Record<'left' | 'right', unknown>
+) => {
+ const positions = Object.keys(Position);
+ return thresholdLayers.reduce((memo, layer) => {
+ if (positions.some((pos) => !(pos in memo))) {
+ layer.yConfig?.forEach(({ axisMode, icon, iconPosition }) => {
+ if (axisMode && hasIcon(icon)) {
+ const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap);
+ memo[placement] = THRESHOLD_ICON_SIZE;
+ }
+ });
+ }
+ return memo;
+ }, {} as Partial>);
+};
+
+function mapVerticalToHorizontalPlacement(placement: Position) {
+ switch (placement) {
+ case Position.Top:
+ return Position.Right;
+ case Position.Bottom:
+ return Position.Left;
+ case Position.Left:
+ return Position.Bottom;
+ case Position.Right:
+ return Position.Top;
+ }
+}
+
+// if there's just one axis, put it on the other one
+// otherwise use the same axis
+// this function assume the chart is vertical
+function getBaseIconPlacement(
+ iconPosition: YConfig['iconPosition'],
+ axisMode: YConfig['axisMode'],
+ axesMap: Record
+) {
+ if (iconPosition === 'auto') {
+ if (axisMode === 'bottom') {
+ return Position.Top;
+ }
+ if (axisMode === 'left') {
+ return axesMap.right ? Position.Left : Position.Right;
+ }
+ return axesMap.left ? Position.Right : Position.Left;
+ }
+
+ if (iconPosition === 'left') {
+ return Position.Left;
+ }
+ if (iconPosition === 'right') {
+ return Position.Right;
+ }
+ if (iconPosition === 'below') {
+ return Position.Bottom;
+ }
+ return Position.Top;
+}
+
+function getIconPlacement(
+ iconPosition: YConfig['iconPosition'],
+ axisMode: YConfig['axisMode'],
+ axesMap: Record,
+ isHorizontal: boolean
+) {
+ const vPosition = getBaseIconPlacement(iconPosition, axisMode, axesMap);
+ if (isHorizontal) {
+ return mapVerticalToHorizontalPlacement(vPosition);
+ }
+ return vPosition;
+}
export const ThresholdAnnotations = ({
thresholdLayers,
data,
- colorAssignments,
formatters,
paletteService,
syncColors,
+ axesMap,
+ isHorizontal,
}: {
thresholdLayers: LayerArgs[];
data: LensMultiTable;
- colorAssignments: ColorAssignments;
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
paletteService: PaletteRegistry;
syncColors: boolean;
+ axesMap: Record<'left' | 'right', boolean>;
+ isHorizontal: boolean;
}) => {
return (
<>
@@ -36,12 +153,11 @@ export const ThresholdAnnotations = ({
if (!thresholdLayer.yConfig) {
return [];
}
- const { columnToLabel, palette, yConfig: yConfigs, layerId } = thresholdLayer;
+ const { columnToLabel, yConfig: yConfigs, layerId } = thresholdLayer;
const columnToLabelMap: Record = columnToLabel
? JSON.parse(columnToLabel)
: {};
const table = data.tables[layerId];
- const colorAssignment = colorAssignments[palette.name];
const row = table.rows[0];
@@ -62,31 +178,17 @@ export const ThresholdAnnotations = ({
const formatter = formatters[groupId || 'bottom'];
- const seriesLayers: SeriesLayer[] = [
- {
- name: columnToLabelMap[yConfig.forAccessor],
- totalSeriesAtDepth: colorAssignment.totalSeriesCount,
- rankAtDepth: colorAssignment.getRank(
- thresholdLayer,
- String(yConfig.forAccessor),
- String(yConfig.forAccessor)
- ),
- },
- ];
- const defaultColor = paletteService.get(palette.name).getCategoricalColor(
- seriesLayers,
- {
- maxDepth: 1,
- behindText: false,
- totalSeries: colorAssignment.totalSeriesCount,
- syncColors,
- },
- palette.params
- );
+ const defaultColor = euiLightVars.euiColorDarkShade;
const props = {
groupId,
- marker: yConfig.icon ? : undefined,
+ marker: hasIcon(yConfig.icon) ? : undefined,
+ markerPosition: getIconPlacement(
+ yConfig.iconPosition,
+ yConfig.axisMode,
+ axesMap,
+ isHorizontal
+ ),
};
const annotations = [];
@@ -99,7 +201,7 @@ export const ThresholdAnnotations = ({
const sharedStyle = {
strokeWidth: yConfig.lineWidth || 1,
- stroke: (yConfig.color || defaultColor) ?? '#f00',
+ stroke: yConfig.color || defaultColor,
dash: dashStyle,
};
@@ -179,7 +281,7 @@ export const ThresholdAnnotations = ({
})}
style={{
...sharedStyle,
- fill: (yConfig.color || defaultColor) ?? '#f00',
+ fill: yConfig.color || defaultColor,
opacity: 0.1,
}}
/>
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index 1996f918b675e..2fce7c6a612ae 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -13,6 +13,7 @@ import { OperationMetadata, DatasourcePublicAPI } from '../types';
import { getColumnToLabelMap } from './state_helpers';
import type { ValidLayer, XYLayerConfig } from '../../common/expressions';
import { layerTypes } from '../../common';
+import { defaultThresholdColor } from './color_assignment';
export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: XYLayerConfig) => {
const originalOrder = datasource
@@ -334,11 +335,12 @@ export const buildExpression = (
arguments: {
forAccessor: [yConfig.forAccessor],
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
- color: yConfig.color ? [yConfig.color] : [],
- lineStyle: yConfig.lineStyle ? [yConfig.lineStyle] : [],
- lineWidth: yConfig.lineWidth ? [yConfig.lineWidth] : [],
+ color: [yConfig.color || defaultThresholdColor],
+ lineStyle: [yConfig.lineStyle || 'solid'],
+ lineWidth: [yConfig.lineWidth || 1],
fill: [yConfig.fill || 'none'],
icon: yConfig.icon ? [yConfig.icon] : [],
+ iconPosition: [yConfig.iconPosition || 'auto'],
},
},
],
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
index 8907db4954f99..8052b0d593215 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
@@ -347,9 +347,6 @@ describe('xy_visualization', () => {
{
axisMode: 'bottom',
forAccessor: 'newCol',
- icon: undefined,
- lineStyle: 'solid',
- lineWidth: 1,
},
],
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
index 33cd01c8fda7a..4e279d2e0026d 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
@@ -448,12 +448,7 @@ export const getXyVisualization = ({
if (!hasYConfig) {
newLayer.yConfig = [
...(newLayer.yConfig || []),
- // TODO: move this
- // add a default config if none is available
{
- icon: undefined,
- lineStyle: 'solid',
- lineWidth: 1,
// override with previous styling,
...previousYConfig,
// but keep the new group & id config
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
index 5a6458a4654d0..516adbf585b9f 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
@@ -13,9 +13,13 @@ import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon }
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { VisualizationDimensionEditorProps } from '../../types';
import { State } from '../types';
-import { FormatFactory } from '../../../common';
+import { FormatFactory, layerTypes } from '../../../common';
import { getSeriesColor } from '../state_helpers';
-import { getAccessorColorConfig, getColorAssignments } from '../color_assignment';
+import {
+ defaultThresholdColor,
+ getAccessorColorConfig,
+ getColorAssignments,
+} from '../color_assignment';
import { getSortedAccessors } from '../to_expression';
import { updateLayer } from '.';
import { TooltipWrapper } from '../../shared_components';
@@ -56,6 +60,9 @@ export const ColorPicker = ({
const overwriteColor = getSeriesColor(layer, accessor);
const currentColor = useMemo(() => {
if (overwriteColor || !frame.activeData) return overwriteColor;
+ if (layer.layerType === layerTypes.THRESHOLD) {
+ return defaultThresholdColor;
+ }
const datasource = frame.datasourceLayers[layer.layerId];
const sortedAccessors: string[] = getSortedAccessors(datasource, layer);
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
index 1427a3d28ea39..41d00e2eef32a 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
@@ -565,7 +565,7 @@ export function DimensionEditor(
}
if (layer.layerType === 'threshold') {
- return ;
+ return ;
}
return (
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
index 1e5b90e41b623..cdf5bb2cc2ef1 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
@@ -6,19 +6,19 @@
*/
import './xy_config_panel.scss';
-import React, { useCallback } from 'react';
+import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonGroup, EuiComboBox, EuiFormRow, EuiIcon, EuiRange } from '@elastic/eui';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import type { VisualizationDimensionEditorProps } from '../../types';
-import { State } from '../types';
+import { State, XYState } from '../types';
import { FormatFactory } from '../../../common';
import { YConfig } from '../../../common/expressions';
-import { LineStyle, FillStyle } from '../../../common/expressions/xy_chart';
+import { LineStyle, FillStyle, IconPosition } from '../../../common/expressions/xy_chart';
import { ColorPicker } from './color_picker';
import { updateLayer, idPrefix } from '.';
-import { useDebouncedValue } from '../../shared_components';
+import { TooltipWrapper, useDebouncedValue } from '../../shared_components';
const icons = [
{
@@ -109,36 +109,111 @@ const IconSelect = ({
);
};
+function getIconPositionOptions({
+ isHorizontal,
+ axisMode,
+}: {
+ isHorizontal: boolean;
+ axisMode: YConfig['axisMode'];
+}) {
+ const options = [
+ {
+ id: `${idPrefix}auto`,
+ label: i18n.translate('xpack.lens.xyChart.thresholdMarker.auto', {
+ defaultMessage: 'Auto',
+ }),
+ 'data-test-subj': 'lnsXY_markerPosition_auto',
+ },
+ ];
+ const topLabel = i18n.translate('xpack.lens.xyChart.markerPosition.above', {
+ defaultMessage: 'Top',
+ });
+ const bottomLabel = i18n.translate('xpack.lens.xyChart.markerPosition.below', {
+ defaultMessage: 'Bottom',
+ });
+ const leftLabel = i18n.translate('xpack.lens.xyChart.markerPosition.left', {
+ defaultMessage: 'Left',
+ });
+ const rightLabel = i18n.translate('xpack.lens.xyChart.markerPosition.right', {
+ defaultMessage: 'Right',
+ });
+ if (axisMode === 'bottom') {
+ const bottomOptions = [
+ {
+ id: `${idPrefix}above`,
+ label: isHorizontal ? rightLabel : topLabel,
+ 'data-test-subj': 'lnsXY_markerPosition_above',
+ },
+ {
+ id: `${idPrefix}below`,
+ label: isHorizontal ? leftLabel : bottomLabel,
+ 'data-test-subj': 'lnsXY_markerPosition_below',
+ },
+ ];
+ if (isHorizontal) {
+ // above -> below
+ // left -> right
+ bottomOptions.reverse();
+ }
+ return [...options, ...bottomOptions];
+ }
+ const yOptions = [
+ {
+ id: `${idPrefix}left`,
+ label: isHorizontal ? bottomLabel : leftLabel,
+ 'data-test-subj': 'lnsXY_markerPosition_left',
+ },
+ {
+ id: `${idPrefix}right`,
+ label: isHorizontal ? topLabel : rightLabel,
+ 'data-test-subj': 'lnsXY_markerPosition_right',
+ },
+ ];
+ if (isHorizontal) {
+ // left -> right
+ // above -> below
+ yOptions.reverse();
+ }
+ return [...options, ...yOptions];
+}
+
export const ThresholdPanel = (
props: VisualizationDimensionEditorProps & {
formatFactory: FormatFactory;
paletteService: PaletteRegistry;
+ isHorizontal: boolean;
}
) => {
- const { state, setState, layerId, accessor } = props;
- const index = state.layers.findIndex((l) => l.layerId === layerId);
- const layer = state.layers[index];
+ const { state, setState, layerId, accessor, isHorizontal } = props;
+
+ const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({
+ value: state,
+ onChange: setState,
+ });
+
+ const index = localState.layers.findIndex((l) => l.layerId === layerId);
+ const layer = localState.layers[index];
const setYConfig = useCallback(
(yConfig: Partial | undefined) => {
if (yConfig == null) {
return;
}
- setState((currState) => {
- const currLayer = currState.layers[index];
- const newYConfigs = [...(currLayer.yConfig || [])];
- const existingIndex = newYConfigs.findIndex(
- (yAxisConfig) => yAxisConfig.forAccessor === accessor
- );
- if (existingIndex !== -1) {
- newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], ...yConfig };
- } else {
- newYConfigs.push({ forAccessor: accessor, ...yConfig });
- }
- return updateLayer(currState, { ...currLayer, yConfig: newYConfigs }, index);
- });
+ const newYConfigs = [...(layer.yConfig || [])];
+ const existingIndex = newYConfigs.findIndex(
+ (yAxisConfig) => yAxisConfig.forAccessor === accessor
+ );
+ if (existingIndex !== -1) {
+ newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], ...yConfig };
+ } else {
+ newYConfigs.push({
+ forAccessor: accessor,
+ ...yConfig,
+ });
+ }
+ setLocalState(updateLayer(localState, { ...layer, yConfig: newYConfigs }, index));
},
- [accessor, index, setState]
+ [accessor, index, localState, layer, setLocalState]
);
const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor);
@@ -259,7 +334,7 @@ export const ThresholdPanel = (
@@ -270,6 +345,44 @@ export const ThresholdPanel = (
}}
/>
+
+
+ {
+ const newMode = id.replace(idPrefix, '') as IconPosition;
+ setYConfig({ forAccessor: accessor, iconPosition: newMode });
+ }}
+ />
+
+
>
);
};
@@ -291,35 +404,38 @@ const LineThicknessSlider = ({
value: number;
onChange: (value: number) => void;
}) => {
- const onChangeWrapped = useCallback(
- (newValue) => {
- if (Number.isInteger(newValue)) {
- onChange(getSafeValue(newValue, newValue, minRange, maxRange));
- }
- },
- [onChange]
- );
- const { inputValue, handleInputChange } = useDebouncedValue(
- { value, onChange: onChangeWrapped },
- { allowFalsyValue: true }
- );
+ const [unsafeValue, setUnsafeValue] = useState(String(value));
return (
{
- const newValue = e.currentTarget.value;
- handleInputChange(newValue === '' ? '' : Number(newValue));
+ onChange={({ currentTarget: { value: newValue } }) => {
+ setUnsafeValue(newValue);
+ const convertedValue = newValue === '' ? '' : Number(newValue);
+ const safeValue = getSafeValue(Number(newValue), Number(newValue), minRange, maxRange);
+ // only update onChange is the value is valid and in range
+ if (convertedValue === safeValue) {
+ onChange(safeValue);
+ }
}}
onBlur={() => {
- handleInputChange(getSafeValue(inputValue, value, minRange, maxRange));
+ if (unsafeValue !== String(value)) {
+ const safeValue = getSafeValue(
+ unsafeValue === '' ? unsafeValue : Number(unsafeValue),
+ value,
+ minRange,
+ maxRange
+ );
+ onChange(safeValue);
+ setUnsafeValue(String(safeValue));
+ }
}}
/>
);
diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts
index 7103e395eabdc..88e8e600aa906 100644
--- a/x-pack/plugins/lens/server/routes/field_stats.ts
+++ b/x-pack/plugins/lens/server/routes/field_stats.ts
@@ -56,7 +56,7 @@ export async function initFieldsRoute(setup: CoreSetup) {
const field = indexPattern.fields.find((f) => f.name === fieldName);
if (!field) {
- throw new Error(`Field {fieldName} not found in index pattern ${indexPattern.title}`);
+ throw new Error(`Field {fieldName} not found in data view ${indexPattern.title}`);
}
const filter = timeFieldName
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
index 5d903bd865911..8500d85d5580a 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
@@ -83,6 +83,7 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) =
application: { navigateToUrl },
},
} = useMlKibana();
+
const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
diff --git a/x-pack/plugins/ml/public/locator/ml_locator.test.ts b/x-pack/plugins/ml/public/locator/ml_locator.test.ts
index 3b736a9af4e3e..cac31abd9f62a 100644
--- a/x-pack/plugins/ml/public/locator/ml_locator.test.ts
+++ b/x-pack/plugins/ml/public/locator/ml_locator.test.ts
@@ -9,7 +9,7 @@ import { MlLocatorDefinition } from './ml_locator';
import { ML_PAGES } from '../../common/constants/locator';
import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
-describe('MlUrlGenerator', () => {
+describe('ML locator', () => {
const definition = new MlLocatorDefinition();
describe('AnomalyDetection', () => {
diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts
index 8c27a826564ee..2e6018ec89809 100644
--- a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts
+++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts
@@ -6,7 +6,6 @@
*/
import { useState, useCallback } from 'react';
-import { EUI_SORT_ASCENDING } from '../../../common/constants';
import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table';
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
@@ -83,22 +82,18 @@ export function useTable(storageKey: string) {
// get initial state from localStorage
const [sorting, setSorting] = useState(storageData.sort || { sort: {} });
- const cleanSortingData = (sortData: Sorting) => {
- const sort = sortData || { sort: {} };
-
- if (!sort.sort.field) {
- sort.sort.field = 'name';
- }
- if (!sort.sort.direction) {
- sort.sort.direction = EUI_SORT_ASCENDING;
- }
-
- return sort;
- };
const [query, setQuery] = useState('');
- const onTableChange = ({ page, sort }: { page: Page; sort: Sorting['sort'] }) => {
+ const onTableChange = ({
+ page,
+ sort,
+ queryText,
+ }: {
+ page: Page;
+ sort: Sorting['sort'];
+ queryText: string;
+ }) => {
setPagination({
...pagination,
...{
@@ -109,11 +104,14 @@ export function useTable(storageKey: string) {
pageSizeOptions: PAGE_SIZE_OPTIONS,
},
});
- setSorting(cleanSortingData({ sort }));
+ setSorting({ sort });
setLocalStorageData(storage, {
page,
- sort: { sort },
+ sort: {
+ sort,
+ },
});
+ setQuery(queryText);
};
const getPaginationRouteOptions = useCallback(() => {
@@ -136,33 +134,6 @@ export function useTable(storageKey: string) {
sorting,
pagination,
onTableChange,
- fetchMoreData: ({
- page,
- sort,
- queryText,
- }: {
- page: Page;
- sort: Sorting;
- queryText: string;
- }) => {
- setPagination({
- ...pagination,
- ...{
- initialPageSize: page.size,
- pageSize: page.size,
- initialPageIndex: page.index,
- pageIndex: page.index,
- pageSizeOptions: PAGE_SIZE_OPTIONS,
- },
- });
- setSorting(cleanSortingData(sort));
- setQuery(queryText);
-
- setLocalStorageData(storage, {
- page,
- sort,
- });
- },
};
};
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index 690ea26319bd3..a958e6061215d 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -22,9 +22,13 @@ import { NoDataPage } from './pages/no_data';
import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview';
import { BeatsOverviewPage } from './pages/beats/overview';
import { BeatsInstancesPage } from './pages/beats/instances';
-import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants';
+import { BeatsInstancePage } from './pages/beats/instance';
+import { KibanaOverviewPage } from './pages/kibana/overview';
+import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS, CODE_PATH_KIBANA } from '../../common/constants';
import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page';
import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page';
+import { ElasticsearchIndexPage } from './pages/elasticsearch/index_page';
+import { ElasticsearchIndexAdvancedPage } from './pages/elasticsearch/index_advanced_page';
import { ElasticsearchNodePage } from './pages/elasticsearch/node_page';
import { MonitoringTimeContainer } from './hooks/use_monitoring_time';
import { BreadcrumbContainer } from './hooks/use_breadcrumbs';
@@ -82,6 +86,21 @@ const MonitoringApp: React.FC<{
/>
{/* ElasticSearch Views */}
+
+
+
+
+
{/* Beats Views */}
+
+
+ {/* Kibana Views */}
+
+
= ({ cluster, ...props }) => {
- const tabs: TabMenuItem[] = [
- {
+export const BeatsTemplate: React.FC = ({ instance, ...props }) => {
+ const tabs: TabMenuItem[] = [];
+
+ if (!instance) {
+ tabs.push({
id: 'overview',
label: i18n.translate('xpack.monitoring.beatsNavigation.overviewLinkText', {
defaultMessage: 'Overview',
}),
route: '/beats',
- },
- {
+ });
+ tabs.push({
id: 'instances',
label: i18n.translate('xpack.monitoring.beatsNavigation.instancesLinkText', {
defaultMessage: 'Instances',
}),
route: '/beats/beats',
- },
- ];
+ });
+ } else {
+ tabs.push({
+ id: 'overview',
+ label: i18n.translate('xpack.monitoring.beatsNavigation.instance.overviewLinkText', {
+ defaultMessage: 'Overview',
+ }),
+ route: `/beats/beat/${instance}`,
+ });
+ }
return ;
};
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx
new file mode 100644
index 0000000000000..f7ff03898fda6
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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, { useContext, useState, useCallback, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { find } from 'lodash';
+import { ComponentProps } from '../../route_init';
+import { GlobalStateContext } from '../../global_state_context';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { useCharts } from '../../hooks/use_charts';
+// @ts-ignore
+import { Beat } from '../../../components/beats/beat';
+import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
+import { BeatsTemplate } from './beats_template';
+
+export const BeatsInstancePage: React.FC = ({ clusters }) => {
+ const { instance }: { instance: string } = useParams();
+
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context);
+ const { zoomInfo, onBrush } = useCharts();
+ const clusterUuid = globalState.cluster_uuid;
+ const ccs = globalState.ccs;
+ const cluster = find(clusters, {
+ cluster_uuid: clusterUuid,
+ }) as any;
+ const [data, setData] = useState({} as any);
+ const [beatName, setBeatName] = useState('');
+
+ const title = i18n.translate('xpack.monitoring.beats.instance.routeTitle', {
+ defaultMessage: 'Beats - {instanceName} - Overview',
+ values: {
+ instanceName: beatName,
+ },
+ });
+
+ const pageTitle = i18n.translate('xpack.monitoring.beats.instance.pageTitle', {
+ defaultMessage: 'Beat instance: {beatName}',
+ values: {
+ beatName,
+ },
+ });
+
+ useEffect(() => {
+ if (cluster) {
+ generateBreadcrumbs(cluster.cluster_name, {
+ inBeats: true,
+ instance: beatName,
+ });
+ }
+ }, [cluster, beatName, generateBreadcrumbs]);
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/beats/beat/${instance}`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ ccs,
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ }),
+ });
+
+ setData(response);
+ setBeatName(response.summary.name);
+ }, [ccs, clusterUuid, instance, services.data?.query.timefilter.timefilter, services.http]);
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
index 3f32e1abf9a88..7a65022d8ff53 100644
--- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
@@ -81,10 +81,10 @@ export const BeatsInstancesPage: React.FC = ({ clusters }) => {
pageTitle={pageTitle}
getPageData={getPageData}
data-test-subj="beatsListingPage"
- cluster={cluster}
>
(
{flyoutComponent}
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx
index 3efad7b82549c..8d28119c4ec1b 100644
--- a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx
@@ -77,7 +77,6 @@ export const BeatsOverviewPage: React.FC = ({ clusters }) => {
pageTitle={pageTitle}
getPageData={getPageData}
data-test-subj="beatsOverviewPage"
- cluster={cluster}
>
{renderOverview(data)}
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
new file mode 100644
index 0000000000000..ccaf23c7ade8e
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useContext, useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useParams } from 'react-router-dom';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { GlobalStateContext } from '../../global_state_context';
+import { ComponentProps } from '../../route_init';
+import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
+import { useCharts } from '../../hooks/use_charts';
+import { ItemTemplate } from './item_template';
+// @ts-ignore
+import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced';
+
+interface SetupModeProps {
+ setupMode: any;
+ flyoutComponent: any;
+ bottomBarComponent: any;
+}
+
+export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { index }: { index: string } = useParams();
+ const { zoomInfo, onBrush } = useCharts();
+ const clusterUuid = globalState.cluster_uuid;
+ const [data, setData] = useState({} as any);
+
+ const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', {
+ defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ is_advanced: true,
+ }),
+ });
+ setData(response);
+ }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]);
+
+ return (
+
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
new file mode 100644
index 0000000000000..b23f9c71a98bf
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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, { useContext, useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useParams } from 'react-router-dom';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { GlobalStateContext } from '../../global_state_context';
+// @ts-ignore
+import { IndexReact } from '../../../components/elasticsearch/index/index_react';
+import { ComponentProps } from '../../route_init';
+import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
+import { useCharts } from '../../hooks/use_charts';
+import { ItemTemplate } from './item_template';
+// @ts-ignore
+import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes';
+// @ts-ignore
+import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
+
+interface SetupModeProps {
+ setupMode: any;
+ flyoutComponent: any;
+ bottomBarComponent: any;
+}
+
+export const ElasticsearchIndexPage: React.FC = ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { index }: { index: string } = useParams();
+ const { zoomInfo, onBrush } = useCharts();
+ const clusterUuid = globalState.cluster_uuid;
+ const [data, setData] = useState({} as any);
+ const [indexLabel, setIndexLabel] = useState(labels.index as any);
+ const [nodesByIndicesData, setNodesByIndicesData] = useState([]);
+
+ const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', {
+ defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.index.overview.pageTitle', {
+ defaultMessage: 'Index: {indexName}',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ is_advanced: false,
+ }),
+ });
+ setData(response);
+ const transformer = indicesByNodes();
+ setNodesByIndicesData(transformer(response.shards, response.nodes));
+
+ const shards = response.shards;
+ if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) {
+ setIndexLabel(labels.indexWithUnassigned);
+ }
+ }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]);
+
+ return (
+
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
new file mode 100644
index 0000000000000..1f06ba18bf102
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { PageTemplate } from '../page_template';
+import { TabMenuItem, PageTemplateProps } from '../page_template';
+
+interface ItemTemplateProps extends PageTemplateProps {
+ id: string;
+ pageType: string;
+}
+export const ItemTemplate: React.FC = (props) => {
+ const { pageType, id, ...rest } = props;
+ const tabs: TabMenuItem[] = [
+ {
+ id: 'overview',
+ label: i18n.translate('xpack.monitoring.esItemNavigation.overviewLinkText', {
+ defaultMessage: 'Overview',
+ }),
+ route: `/elasticsearch/${pageType}/${id}`,
+ },
+ {
+ id: 'advanced',
+ label: i18n.translate('xpack.monitoring.esItemNavigation.advancedLinkText', {
+ defaultMessage: 'Advanced',
+ }),
+ route: `/elasticsearch/${pageType}/${id}/advanced`,
+ },
+ ];
+
+ return ;
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
index ffbde2efcac6b..9b3a67f612e5c 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
@@ -7,8 +7,7 @@
import React, { useContext, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
-import { find } from 'lodash';
-import { ElasticsearchTemplate } from './elasticsearch_template';
+import { ItemTemplate } from './item_template';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../global_state_context';
import { NodeReact } from '../../../components/elasticsearch';
@@ -18,6 +17,8 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont
import { useLocalStorage } from '../../hooks/use_local_storage';
import { useCharts } from '../../hooks/use_charts';
import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices';
+// @ts-ignore
+import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
interface SetupModeProps {
setupMode: any;
@@ -38,9 +39,6 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) =>
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
- const cluster = find(clusters, {
- cluster_uuid: clusterUuid,
- });
const [data, setData] = useState({} as any);
const [nodesByIndicesData, setNodesByIndicesData] = useState([]);
@@ -92,33 +90,33 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) =>
}, [showSystemIndices, setShowSystemIndices]);
return (
-
-
- (
-
- {flyoutComponent}
-
- {bottomBarComponent}
-
- )}
- />
-
-
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
);
};
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx
new file mode 100644
index 0000000000000..56c763b726d27
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { PageTemplate, TabMenuItem, PageTemplateProps } from '../page_template';
+
+export const KibanaTemplate: React.FC = ({ ...props }) => {
+ const tabs: TabMenuItem[] = [
+ {
+ id: 'overview',
+ label: i18n.translate('xpack.monitoring.kibanaNavigation.overviewLinkText', {
+ defaultMessage: 'Overview',
+ }),
+ route: '/kibana',
+ },
+ {
+ id: 'instances',
+ label: i18n.translate('xpack.monitoring.kibanaNavigation.instancesLinkText', {
+ defaultMessage: 'Instances',
+ }),
+ route: '/kibana/instances',
+ },
+ ];
+
+ return ;
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
new file mode 100644
index 0000000000000..2356011a3f77b
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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, { useCallback, useContext, useEffect, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { find } from 'lodash';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPanel,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { KibanaTemplate } from './kibana_template';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { GlobalStateContext } from '../../global_state_context';
+import { ComponentProps } from '../../route_init';
+// @ts-ignore
+import { MonitoringTimeseriesContainer } from '../../../components/chart';
+// @ts-ignore
+import { ClusterStatus } from '../../../components/kibana/cluster_status';
+import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
+import { useCharts } from '../../hooks/use_charts';
+
+const KibanaOverview = ({ data }: { data: any }) => {
+ const { zoomInfo, onBrush } = useCharts();
+
+ if (!data) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const KibanaOverviewPage: React.FC = ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context);
+ const [data, setData] = useState();
+ const clusterUuid = globalState.cluster_uuid;
+ const cluster = find(clusters, {
+ cluster_uuid: clusterUuid,
+ }) as any;
+ const ccs = globalState.ccs;
+ const title = i18n.translate('xpack.monitoring.kibana.overview.title', {
+ defaultMessage: 'Kibana',
+ });
+ const pageTitle = i18n.translate('xpack.monitoring.kibana.overview.pageTitle', {
+ defaultMessage: 'Kibana overview',
+ });
+
+ useEffect(() => {
+ if (cluster) {
+ generateBreadcrumbs(cluster.cluster_name, {
+ inKibana: true,
+ });
+ }
+ }, [cluster, generateBreadcrumbs]);
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana`;
+
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ ccs,
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ }),
+ });
+
+ setData(response);
+ }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]);
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
new file mode 100644
index 0000000000000..70bac52a0926c
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
@@ -0,0 +1,70 @@
+/*
+ * 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 {
+ EuiPage,
+ EuiPageContent,
+ EuiPageBody,
+ EuiPanel,
+ EuiSpacer,
+ EuiFlexGrid,
+ EuiFlexItem,
+} from '@elastic/eui';
+import { IndexDetailStatus } from '../index_detail_status';
+import { MonitoringTimeseriesContainer } from '../../chart';
+import { ShardAllocationReact } from '../shard_allocation/shard_allocation_react';
+import { Logs } from '../../logs';
+import { AlertsCallout } from '../../../alerts/callout';
+
+export const IndexReact = ({
+ indexSummary,
+ metrics,
+ clusterUuid,
+ indexUuid,
+ logs,
+ alerts,
+ ...props
+}) => {
+ const metricsToShow = [
+ metrics.index_mem,
+ metrics.index_size,
+ metrics.index_search_request_rate,
+ metrics.index_request_rate,
+ metrics.index_segment_count,
+ metrics.index_document_count,
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {metricsToShow.map((metric, index) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
index 8a4a8a3c63d9d..987422c2e8bb3 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
@@ -298,8 +298,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
};
export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsearch, ...props }) {
- const { sorting, pagination, onTableChange, clusterUuid, setupMode, fetchMoreData, alerts } =
- props;
+ const { sorting, pagination, onTableChange, clusterUuid, setupMode, alerts } = props;
const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts);
@@ -474,7 +473,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear
},
}}
onTableChange={onTableChange}
- fetchMoreData={fetchMoreData}
+ {...props}
/>
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
index 987ca467931f4..2d0c4b59df4b8 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
@@ -8,13 +8,12 @@
import React from 'react';
import { TableHeadReact } from './table_head_react';
import { TableBody } from './table_body';
-import { labels } from '../lib/labels';
export const ClusterViewReact = (props) => {
return (
@@ -22,7 +21,7 @@ export const ClusterViewReact = (props) => {
filter={props.filter}
totalCount={props.totalCount}
rows={props.nodesByIndices}
- cols={labels.node.length}
+ cols={props.labels.length}
shardStats={props.shardStats}
/>
diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
index 50a4d3f8cf32b..067f2b56937f8 100644
--- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
+++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
@@ -129,7 +129,7 @@ export class PipelineListing extends Component {
}
render() {
- const { data, sorting, pagination, onTableChange, fetchMoreData, upgradeMessage, className } =
+ const { data, sorting, pagination, onTableChange, upgradeMessage, className, ...props } =
this.props;
const sortingOptions = sorting || { field: 'id', direction: 'asc' };
@@ -159,7 +159,6 @@ export class PipelineListing extends Component {
sorting={sortingOptions}
message={upgradeMessage}
pagination={pagination}
- fetchMoreData={fetchMoreData}
search={{
box: {
placeholder: i18n.translate(
@@ -171,6 +170,7 @@ export class PipelineListing extends Component {
},
}}
onTableChange={onTableChange}
+ {...props}
/>
diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js
index 5042c3c938fb7..cbf589d5de2a0 100644
--- a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js
+++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js
@@ -20,10 +20,8 @@ export function EuiMonitoringSSPTable({
onTableChange,
setupMode,
productName,
- fetchMoreData,
...props
}) {
- const [isLoading, setIsLoading] = React.useState(false);
const [queryText, setQueryText] = React.useState('');
const [page, setPage] = React.useState({
index: pagination.pageIndex,
@@ -72,19 +70,28 @@ export function EuiMonitoringSSPTable({
const onChange = async ({ page, sort }) => {
setPage(page);
setSort({ sort });
- setIsLoading(true);
- await fetchMoreData({ page, sort: { sort }, queryText });
- setIsLoading(false);
- onTableChange({ page, sort });
+ // angular version
+ if (props.fetchMoreData) {
+ await props.fetchMoreData({ page, sort: { sort }, queryText });
+ onTableChange({ page, sort });
+ }
+ // react version
+ else {
+ onTableChange({ page, sort, queryText });
+ }
};
const onQueryChange = async ({ queryText }) => {
const newPage = { ...page, index: 0 };
setPage(newPage);
setQueryText(queryText);
- setIsLoading(true);
- await fetchMoreData({ page: newPage, sort, queryText });
- setIsLoading(false);
+ // angular version
+ if (props.fetchMoreData) {
+ await props.fetchMoreData({ page: newPage, sort, queryText });
+ } else {
+ // react version
+ onTableChange({ page, sort: sort.sort, queryText });
+ }
};
return (
@@ -97,7 +104,6 @@ export function EuiMonitoringSSPTable({
items={items}
pagination={pagination}
onChange={onChange}
- loading={isLoading}
columns={columns}
/>
{footerContent}
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
new file mode 100644
index 0000000000000..0e17c6277618b
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function MobileAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', {
+ defaultMessage: 'Add Mobile data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
new file mode 100644
index 0000000000000..af91624769e6b
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function SyntheticsAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', {
+ defaultMessage: 'Add synthetics data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
new file mode 100644
index 0000000000000..c6aa0742466f1
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function UXAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', {
+ defaultMessage: 'Add UX data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
new file mode 100644
index 0000000000000..2b59628c3e8d3
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { render } from '../../rtl_helpers';
+import { fireEvent, screen } from '@testing-library/dom';
+import React from 'react';
+import { sampleAttribute } from '../../configurations/test_data/sample_attribute';
+import * as pluginHook from '../../../../../hooks/use_plugin_context';
+import { TypedLensByValueInput } from '../../../../../../../lens/public';
+import { ExpViewActionMenuContent } from './action_menu';
+
+jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
+ appMountParameters: {
+ setHeaderActionMenu: jest.fn(),
+ },
+} as any);
+
+describe('Action Menu', function () {
+ it('should be able to click open in lens', async function () {
+ const { findByText, core } = render(
+
+ );
+
+ expect(await screen.findByText('Open in Lens')).toBeInTheDocument();
+
+ fireEvent.click(await findByText('Open in Lens'));
+
+ expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
+ expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
+ {
+ id: '',
+ attributes: sampleAttribute,
+ timeRange: { to: 'now', from: 'now-10m' },
+ },
+ {
+ openInNewTab: true,
+ }
+ );
+ });
+
+ it('should be able to click save', async function () {
+ const { findByText } = render(
+
+ );
+
+ expect(await screen.findByText('Save')).toBeInTheDocument();
+
+ fireEvent.click(await findByText('Save'));
+
+ expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
new file mode 100644
index 0000000000000..08b4a3b948c57
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public';
+import { ObservabilityAppServices } from '../../../../../application/types';
+import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { AddToCaseAction } from '../../header/add_to_case_action';
+
+export function ExpViewActionMenuContent({
+ timeRange,
+ lensAttributes,
+}: {
+ timeRange?: { from: string; to: string };
+ lensAttributes: TypedLensByValueInput['attributes'] | null;
+}) {
+ const kServices = useKibana().services;
+
+ const { lens } = kServices;
+
+ const [isSaveOpen, setIsSaveOpen] = useState(false);
+
+ const LensSaveModalComponent = lens.SaveModalComponent;
+
+ return (
+ <>
+
+
+
+
+
+ {
+ if (lensAttributes) {
+ lens.navigateToPrefilledEditor(
+ {
+ id: '',
+ timeRange,
+ attributes: lensAttributes,
+ },
+ {
+ openInNewTab: true,
+ }
+ );
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.openInLens', {
+ defaultMessage: 'Open in Lens',
+ })}
+
+
+
+ {
+ if (lensAttributes) {
+ setIsSaveOpen(true);
+ }
+ }}
+ size="s"
+ >
+ {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+ {isSaveOpen && lensAttributes && (
+ setIsSaveOpen(false)}
+ // if we want to do anything after the viz is saved
+ // right now there is no action, so an empty function
+ onSave={() => {}}
+ />
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
new file mode 100644
index 0000000000000..23500b63e900a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { ExpViewActionMenuContent } from './action_menu';
+import HeaderMenuPortal from '../../../header_menu_portal';
+import { usePluginContext } from '../../../../../hooks/use_plugin_context';
+import { TypedLensByValueInput } from '../../../../../../../lens/public';
+
+interface Props {
+ timeRange?: { from: string; to: string };
+ lensAttributes: TypedLensByValueInput['attributes'] | null;
+}
+export function ExpViewActionMenu(props: Props) {
+ const { appMountParameters } = usePluginContext();
+
+ return (
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
similarity index 58%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
index c30863585b3b0..aabde404aa7b4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
@@ -6,48 +6,48 @@
*/
import React from 'react';
-import { i18n } from '@kbn/i18n';
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
-import DateMath from '@elastic/datemath';
import { Moment } from 'moment';
+import DateMath from '@elastic/datemath';
+import { i18n } from '@kbn/i18n';
import { useSeriesStorage } from '../hooks/use_series_storage';
import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
+import { SeriesUrl } from '../types';
+import { ReportTypes } from '../configurations/constants';
export const parseAbsoluteDate = (date: string, options = {}) => {
return DateMath.parse(date, options)!;
};
-export function DateRangePicker({ seriesId }: { seriesId: string }) {
- const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
+export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
+ const { firstSeries, setSeries, reportType } = useSeriesStorage();
const dateFormat = useUiSetting('dateFormat');
- const {
- time: { from, to },
- reportType,
- } = getSeries(firstSeriesId);
+ const seriesFrom = series.time?.from;
+ const seriesTo = series.time?.to;
- const series = getSeries(seriesId);
+ const { from: mainFrom, to: mainTo } = firstSeries!.time;
- const {
- time: { from: seriesFrom, to: seriesTo },
- } = series;
+ const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!;
+ const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!;
- const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
- const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
+ const getTotalDuration = () => {
+ const mainStartDate = parseAbsoluteDate(mainFrom)!;
+ const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!;
+ return mainEndDate.diff(mainStartDate, 'millisecond');
+ };
- const onStartChange = (newDate: Moment) => {
- if (reportType === 'kpi-over-time') {
- const mainStartDate = parseAbsoluteDate(from)!;
- const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
- const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
- const newFrom = newDate.toISOString();
- const newTo = newDate.add(totalDuration, 'millisecond').toISOString();
+ const onStartChange = (newStartDate: Moment) => {
+ if (reportType === ReportTypes.KPI) {
+ const totalDuration = getTotalDuration();
+ const newFrom = newStartDate.toISOString();
+ const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
- const newFrom = newDate.toISOString();
+ const newFrom = newStartDate.toISOString();
setSeries(seriesId, {
...series,
@@ -55,20 +55,19 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
});
}
};
- const onEndChange = (newDate: Moment) => {
- if (reportType === 'kpi-over-time') {
- const mainStartDate = parseAbsoluteDate(from)!;
- const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
- const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
- const newTo = newDate.toISOString();
- const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString();
+
+ const onEndChange = (newEndDate: Moment) => {
+ if (reportType === ReportTypes.KPI) {
+ const totalDuration = getTotalDuration();
+ const newTo = newEndDate.toISOString();
+ const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
- const newTo = newDate.toISOString();
+ const newTo = newEndDate.toISOString();
setSeries(seriesId, {
...series,
@@ -90,7 +89,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
defaultMessage: 'Start date',
})}
- dateFormat={dateFormat}
+ dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
/>
}
@@ -104,7 +103,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
defaultMessage: 'End date',
})}
- dateFormat={dateFormat}
+ dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
/>
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
index 3566835b1701c..d17e451ef702c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
@@ -10,19 +10,19 @@ import { isEmpty } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
-import { LOADING_VIEW } from '../series_builder/series_builder';
-import { SeriesUrl } from '../types';
+import { LOADING_VIEW } from '../series_editor/series_editor';
+import { ReportViewType, SeriesUrl } from '../types';
export function EmptyView({
loading,
- height,
series,
+ reportType,
}: {
loading: boolean;
- height: string;
- series: SeriesUrl;
+ series?: SeriesUrl;
+ reportType: ReportViewType;
}) {
- const { dataType, reportType, reportDefinitions } = series ?? {};
+ const { dataType, reportDefinitions } = series ?? {};
let emptyMessage = EMPTY_LABEL;
@@ -45,7 +45,7 @@ export function EmptyView({
}
return (
-
+
{loading && (
`
+const Wrapper = styled.div`
text-align: center;
- height: ${(props) => props.height};
position: relative;
`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
index fe2953edd36d6..03fd23631f755 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
+import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers';
import { FilterLabel } from './filter_label';
import * as useSeriesHook from '../hooks/use_series_filters';
import { buildFilterLabel } from '../../filter_value_label/filter_value_label';
@@ -27,9 +27,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={jest.fn()}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -51,9 +52,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={removeFilter}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -74,9 +76,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={removeFilter}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -100,9 +103,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={true}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={jest.fn()}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
index a08e777c5ea71..c6254a85de9ac 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
@@ -9,21 +9,24 @@ import React from 'react';
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { useSeriesFilters } from '../hooks/use_series_filters';
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
+import { SeriesUrl } from '../types';
interface Props {
field: string;
label: string;
- value: string;
- seriesId: string;
+ value: string | string[];
+ seriesId: number;
+ series: SeriesUrl;
negate: boolean;
definitionFilter?: boolean;
indexPattern: IndexPattern;
- removeFilter: (field: string, value: string, notVal: boolean) => void;
+ removeFilter: (field: string, value: string | string[], notVal: boolean) => void;
}
export function FilterLabel({
label,
seriesId,
+ series,
field,
value,
negate,
@@ -31,7 +34,7 @@ export function FilterLabel({
removeFilter,
definitionFilter,
}: Props) {
- const { invertFilter } = useSeriesFilters({ seriesId });
+ const { invertFilter } = useSeriesFilters({ seriesId, series });
return indexPattern ? (
{
+ setSeries(seriesId, { ...series, color: colorN });
+ };
+
+ const color =
+ series.color ?? (theme.eui as unknown as Record)[`euiColorVis${seriesId}`];
+
+ const button = (
+
+ setIsOpen((prevState) => !prevState)} flush="both">
+
+
+
+ );
+
+ return (
+ setIsOpen(false)}>
+
+
+
+
+ );
+}
+
+const PICK_A_COLOR_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.pickColor',
+ {
+ defaultMessage: 'Pick a color',
+ }
+);
+
+const EDIT_SERIES_COLOR_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.editSeriesColor',
+ {
+ defaultMessage: 'Edit color for series',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
similarity index 54%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
index e21da424b58c8..e02f11dfc4954 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
@@ -6,11 +6,13 @@
*/
import { EuiSuperDatePicker } from '@elastic/eui';
-import React, { useEffect } from 'react';
-import { useHasData } from '../../../../hooks/use_has_data';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
-import { DEFAULT_TIME } from '../configurations/constants';
+import React from 'react';
+
+import { useHasData } from '../../../../../hooks/use_has_data';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges';
+import { SeriesUrl } from '../../types';
+import { ReportTypes } from '../../configurations/constants';
export interface TimePickerTime {
from: string;
@@ -22,28 +24,27 @@ export interface TimePickerQuickRange extends TimePickerTime {
}
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
}
-export function SeriesDatePicker({ seriesId }: Props) {
+export function SeriesDatePicker({ series, seriesId }: Props) {
const { onRefreshTimeRange } = useHasData();
const commonlyUsedRanges = useQuickTimeRanges();
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries, reportType, allSeries } = useSeriesStorage();
function onTimeChange({ start, end }: { start: string; end: string }) {
onRefreshTimeRange();
- setSeries(seriesId, { ...series, time: { from: start, to: end } });
- }
-
- useEffect(() => {
- if (!series || !series.time) {
- setSeries(seriesId, { ...series, time: DEFAULT_TIME });
+ if (reportType === ReportTypes.KPI) {
+ allSeries.forEach((currSeries, seriesIndex) => {
+ setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } });
+ });
+ } else {
+ setSeries(seriesId, { ...series, time: { from: start, to: end } });
}
- }, [series, seriesId, setSeries]);
+ }
return (
, { initSeries });
+ const { getByText } = render( , {
+ initSeries,
+ });
getByText('Last 30 minutes');
});
- it('should set defaults', async function () {
- const initSeries = {
- data: {
- 'uptime-pings-histogram': {
- reportType: 'kpi-over-time' as const,
- dataType: 'synthetics' as const,
- breakdown: 'monitor.status',
- },
- },
- };
- const { setSeries: setSeries1 } = render(
- ,
- { initSeries: initSeries as any }
- );
- expect(setSeries1).toHaveBeenCalledTimes(1);
- expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
- breakdown: 'monitor.status',
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- time: DEFAULT_TIME,
- });
- });
-
it('should set series data', async function () {
const initSeries = {
- data: {
- 'uptime-pings-histogram': {
+ data: [
+ {
+ name: 'uptime-pings-histogram',
dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
- },
+ ],
};
const { onRefreshTimeRange } = mockUseHasData();
- const { getByTestId, setSeries } = render( , {
- initSeries,
- });
+ const { getByTestId, setSeries } = render(
+ ,
+ {
+ initSeries,
+ }
+ );
await waitFor(function () {
fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton'));
@@ -76,10 +57,10 @@ describe('SeriesDatePicker', function () {
expect(onRefreshTimeRange).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
+ name: 'uptime-pings-histogram',
breakdown: 'monitor.status',
dataType: 'synthetics',
- reportType: 'kpi-over-time',
time: { from: 'now/d', to: 'now/d' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index ba1f2214223e3..bf5feb7d5863c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -94,6 +94,19 @@ export const DataViewLabels: Record = {
'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
};
+export enum ReportTypes {
+ KPI = 'kpi-over-time',
+ DISTRIBUTION = 'data-distribution',
+ CORE_WEB_VITAL = 'core-web-vitals',
+ DEVICE_DISTRIBUTION = 'device-data-distribution',
+}
+
+export enum DataTypes {
+ SYNTHETICS = 'synthetics',
+ UX = 'ux',
+ MOBILE = 'mobile',
+}
+
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
export const FILTER_RECORDS = 'FILTER_RECORDS';
export const TERMS_COLUMN = 'TERMS_COLUMN';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
index 6f990015fbc62..55ac75b47c056 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
@@ -8,10 +8,12 @@
export enum URL_KEYS {
DATA_TYPE = 'dt',
OPERATION_TYPE = 'op',
- REPORT_TYPE = 'rt',
SERIES_TYPE = 'st',
BREAK_DOWN = 'bd',
FILTERS = 'ft',
REPORT_DEFINITIONS = 'rdf',
SELECTED_METRIC = 'mt',
+ HIDDEN = 'h',
+ NAME = 'n',
+ COLOR = 'c',
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 574a9f6a2bc10..3f6551986527c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -15,6 +15,7 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config';
import { getMobileKPIConfig } from './mobile/kpi_over_time_config';
import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
+import { DataTypes, ReportTypes } from './constants';
interface Props {
reportType: ReportViewType;
@@ -24,24 +25,24 @@ interface Props {
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
switch (dataType) {
- case 'ux':
- if (reportType === 'data-distribution') {
+ case DataTypes.UX:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getRumDistributionConfig({ indexPattern });
}
- if (reportType === 'core-web-vitals') {
+ if (reportType === ReportTypes.CORE_WEB_VITAL) {
return getCoreWebVitalsConfig({ indexPattern });
}
return getKPITrendsLensConfig({ indexPattern });
- case 'synthetics':
- if (reportType === 'data-distribution') {
+ case DataTypes.SYNTHETICS:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getSyntheticsDistributionConfig({ indexPattern });
}
return getSyntheticsKPIConfig({ indexPattern });
- case 'mobile':
- if (reportType === 'data-distribution') {
+ case DataTypes.MOBILE:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getMobileKPIDistributionConfig({ indexPattern });
}
- if (reportType === 'device-data-distribution') {
+ if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
return getMobileDeviceDistributionConfig({ indexPattern });
}
return getMobileKPIConfig({ indexPattern });
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index 706c58609b7cb..9e7c5254b511f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -16,7 +16,7 @@ import {
} from './constants/elasticsearch_fieldnames';
import { buildExistsFilter, buildPhrasesFilter } from './utils';
import { sampleAttributeKpi } from './test_data/sample_attribute_kpi';
-import { REPORT_METRIC_FIELD } from './constants';
+import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants';
describe('Lens Attribute', () => {
mockAppIndexPattern();
@@ -38,6 +38,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
beforeEach(() => {
@@ -50,7 +53,7 @@ describe('Lens Attribute', () => {
it('should return expected json for kpi report type', function () {
const seriesConfigKpi = getDefaultConfigs({
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
dataType: 'ux',
indexPattern: mockIndexPattern,
});
@@ -63,6 +66,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: RECORDS_FIELD,
},
]);
@@ -135,6 +141,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1]);
@@ -383,7 +392,7 @@ describe('Lens Attribute', () => {
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
},
],
legend: { isVisible: true, position: 'right' },
@@ -403,6 +412,9 @@ describe('Lens Attribute', () => {
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
breakdown: USER_AGENT_NAME,
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1]);
@@ -423,7 +435,7 @@ describe('Lens Attribute', () => {
seriesType: 'line',
splitAccessor: 'breakdown-column-layer0',
xAccessor: 'x-axis-column-layer0',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
},
]);
@@ -589,6 +601,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 2778edc94838e..ec2e6b5066c87 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -37,10 +37,11 @@ import {
REPORT_METRIC_FIELD,
RECORDS_FIELD,
RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
} from './constants';
import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types';
import { PersistableFilter } from '../../../../../../lens/common';
-import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
+import { parseAbsoluteDate } from '../components/date_range_picker';
import { getDistributionInPercentageColumn } from './lens_columns/overall_column';
function getLayerReferenceName(layerId: string) {
@@ -74,14 +75,6 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF
timeScale = currField?.timeScale;
columnLabel = currField?.label;
}
- } else if (metricOptions?.[0].field || metricOptions?.[0].id) {
- const firstMetricOption = metricOptions?.[0];
-
- selectedMetricField = firstMetricOption.field || firstMetricOption.id;
- columnType = firstMetricOption.columnType;
- columnFilters = firstMetricOption.columnFilters;
- timeScale = firstMetricOption.timeScale;
- columnLabel = firstMetricOption.label;
}
return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel };
@@ -96,7 +89,9 @@ export interface LayerConfig {
reportDefinitions: URLReportDefinition;
time: { to: string; from: string };
indexPattern: IndexPattern;
- selectedMetricField?: string;
+ selectedMetricField: string;
+ color: string;
+ name: string;
}
export class LensAttributes {
@@ -467,14 +462,15 @@ export class LensAttributes {
getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
const {
filters,
- time: { from, to },
+ time,
seriesConfig: { baseFilters: layerFilters, reportType },
} = layerConfig;
let baseFilters = '';
- if (reportType !== 'kpi-over-time' && totalLayers > 1) {
+
+ if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) {
// for kpi over time, we don't need to add time range filters
// since those are essentially plotted along the x-axis
- baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`;
+ baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`;
}
layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
@@ -530,7 +526,11 @@ export class LensAttributes {
}
getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
- if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') {
+ if (
+ index === 0 ||
+ mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI ||
+ !layerConfig.time
+ ) {
return null;
}
@@ -542,11 +542,14 @@ export class LensAttributes {
time: { from },
} = layerConfig;
- const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
+ const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'));
if (inDays > 1) {
return inDays + 'd';
}
- const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
+ const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'));
+ if (inHours === 0) {
+ return null;
+ }
return inHours + 'h';
}
@@ -564,6 +567,8 @@ export class LensAttributes {
const { sourceField } = seriesConfig.xAxisColumn;
+ const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label;
+
layers[layerId] = {
columnOrder: [
`x-axis-column-${layerId}`,
@@ -577,7 +582,7 @@ export class LensAttributes {
[`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
[`y-axis-column-${layerId}`]: {
...mainYAxis,
- label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
+ label,
filter: { query: columnFilter, language: 'kuery' },
...(timeShift ? { timeShift } : {}),
},
@@ -621,7 +626,7 @@ export class LensAttributes {
seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType,
palette: layerConfig.seriesConfig.palette,
yConfig: layerConfig.seriesConfig.yConfig || [
- { forAccessor: `y-axis-column-layer${index}` },
+ { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color },
],
xAccessor: `x-axis-column-layer${index}`,
...(layerConfig.breakdown &&
@@ -635,7 +640,7 @@ export class LensAttributes {
};
}
- getJSON(): TypedLensByValueInput['attributes'] {
+ getJSON(refresh?: number): TypedLensByValueInput['attributes'] {
const uniqueIndexPatternsIds = Array.from(
new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
);
@@ -644,7 +649,7 @@ export class LensAttributes {
return {
title: 'Prefilled from exploratory view app',
- description: '',
+ description: String(refresh),
visualizationType: 'lnsXY',
references: [
...uniqueIndexPatternsIds.map((patternId) => ({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
index d1612a08f5551..4e178bba7e02a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants';
+import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants';
import { buildPhraseFilter } from '../utils';
import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels';
@@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'device-data-distribution',
+ reportType: ReportTypes.DEVICE_DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['bar', 'bar_horizontal'],
xAxisColumn: {
@@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps)
...MobileFields,
[SERVICE_NAME]: MOBILE_APP,
},
+ definitionFields: [SERVICE_NAME],
metricOptions: [
{
- id: 'labels.device_id',
field: 'labels.device_id',
+ id: 'labels.device_id',
label: NUMBER_OF_DEVICES,
},
],
- definitionFields: [SERVICE_NAME],
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
index 9b1c4c8da3e9b..1da27be4fcc95 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
@@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
index 945a631078a33..3ee5b3125fcda 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
@@ -6,7 +6,13 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import {
+ FieldLabels,
+ OPERATION_COLUMN,
+ RECORDS_FIELD,
+ REPORT_METRIC_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
@@ -26,7 +32,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar', 'area'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
index 07bb13f957e45..35e094996f6f2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
@@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers';
import { getDefaultConfigs } from '../default_configs';
import { LayerConfig, LensAttributes } from '../lens_attributes';
import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv';
-import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
+import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
describe('Core web vital config test', function () {
mockAppIndexPattern();
@@ -24,10 +24,13 @@ describe('Core web vital config test', function () {
const layerConfig: LayerConfig = {
seriesConfig,
+ color: 'green',
+ name: 'test-series',
+ breakdown: USER_AGENT_OS,
indexPattern: mockIndexPattern,
- reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
- breakdown: USER_AGENT_OS,
+ reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
+ selectedMetricField: LCP_FIELD,
};
beforeEach(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
index 62455df248085..e8d620388a89e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
@@ -11,6 +11,7 @@ import {
FieldLabels,
FILTER_RECORDS,
REPORT_METRIC_FIELD,
+ ReportTypes,
USE_BREAK_DOWN_COLUMN,
} from '../constants';
import { buildPhraseFilter } from '../utils';
@@ -38,7 +39,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
return {
defaultSeriesType: 'bar_horizontal_percentage_stacked',
- reportType: 'core-web-vitals',
+ reportType: ReportTypes.CORE_WEB_VITAL,
seriesTypes: ['bar_horizontal_percentage_stacked'],
xAxisColumn: {
sourceField: USE_BREAK_DOWN_COLUMN,
@@ -153,5 +154,6 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
{ color: statusPallete[1], forAccessor: 'y-axis-column-1' },
{ color: statusPallete[2], forAccessor: 'y-axis-column-2' },
],
+ query: { query: 'transaction.type: "page-load"', language: 'kuery' },
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
index f34c8db6c197d..de6f2c67b2aeb 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
@@ -6,7 +6,12 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
+import {
+ FieldLabels,
+ REPORT_METRIC_FIELD,
+ RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
@@ -41,7 +46,7 @@ import {
export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
index 5899b16d12b4f..9112778eadaa7 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
@@ -6,7 +6,13 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import {
+ FieldLabels,
+ OPERATION_COLUMN,
+ RECORDS_FIELD,
+ REPORT_METRIC_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
@@ -43,7 +49,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon
return {
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
xAxisColumn: {
sourceField: '@timestamp',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
index 730e742f9d8c5..da90f45d15201 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
@@ -6,7 +6,12 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
+import {
+ FieldLabels,
+ REPORT_METRIC_FIELD,
+ RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
+} from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
@@ -30,7 +35,7 @@ export function getSyntheticsDistributionConfig({
indexPattern,
}: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: series?.seriesType || 'line',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
index 4ee22181d4334..65b43a83a8fb5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants';
+import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
@@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down';
export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
index 596e7af4378ec..7e0ea1e575481 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
@@ -5,12 +5,18 @@
* 2.0.
*/
export const sampleAttribute = {
- title: 'Prefilled from exploratory view app',
- description: '',
- visualizationType: 'lnsXY',
+ description: 'undefined',
references: [
- { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-current-indexpattern',
+ type: 'index-pattern',
+ },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-layer-layer0',
+ type: 'index-pattern',
+ },
],
state: {
datasourceStates: {
@@ -28,17 +34,23 @@ export const sampleAttribute = {
],
columns: {
'x-axis-column-layer0': {
- sourceField: 'transaction.duration.us',
- label: 'Page load time',
dataType: 'number',
- operationType: 'range',
isBucketed: true,
- scale: 'interval',
+ label: 'Page load time',
+ operationType: 'range',
params: {
- type: 'histogram',
- ranges: [{ from: 0, to: 1000, label: '' }],
maxBars: 'auto',
+ ranges: [
+ {
+ from: 0,
+ label: '',
+ to: 1000,
+ },
+ ],
+ type: 'histogram',
},
+ scale: 'interval',
+ sourceField: 'transaction.duration.us',
},
'y-axis-column-layer0': {
dataType: 'number',
@@ -81,16 +93,16 @@ export const sampleAttribute = {
'y-axis-column-layer0X1': {
customLabel: true,
dataType: 'number',
- isBucketed: false,
- label: 'Part of count() / overall_sum(count())',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
+ isBucketed: false,
+ label: 'Part of count() / overall_sum(count())',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
},
'y-axis-column-layer0X2': {
customLabel: true,
@@ -140,27 +152,52 @@ export const sampleAttribute = {
},
},
},
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: 'transaction.duration.us < 60000000',
+ },
visualization: {
- legend: { isVisible: true, position: 'right' },
- valueLabels: 'hide',
- fittingFunction: 'Linear',
+ axisTitlesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
curveType: 'CURVE_MONOTONE_X',
- axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
- gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- preferredSeriesType: 'line',
+ fittingFunction: 'Linear',
+ gridlinesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
layers: [
{
accessors: ['y-axis-column-layer0'],
layerId: 'layer0',
layerType: 'data',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
xAccessor: 'x-axis-column-layer0',
+ yConfig: [
+ {
+ color: 'green',
+ forAccessor: 'y-axis-column-layer0',
+ },
+ ],
},
],
+ legend: {
+ isVisible: true,
+ position: 'right',
+ },
+ preferredSeriesType: 'line',
+ tickLabelsVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
+ valueLabels: 'hide',
},
- query: { query: 'transaction.duration.us < 60000000', language: 'kuery' },
- filters: [],
},
+ title: 'Prefilled from exploratory view app',
+ visualizationType: 'lnsXY',
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
index 56ceba8fc52de..dff3d6b3ad5ef 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
export const sampleAttributeCoreWebVital = {
- description: '',
+ description: 'undefined',
references: [
{
id: 'apm-*',
@@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = {
filters: [],
query: {
language: 'kuery',
- query: '',
+ query: 'transaction.type: "page-load"',
},
visualization: {
axisTitlesVisibilitySettings: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
index 72933573c410b..6ed9b4face6e3 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
@@ -5,12 +5,18 @@
* 2.0.
*/
export const sampleAttributeKpi = {
- title: 'Prefilled from exploratory view app',
- description: '',
- visualizationType: 'lnsXY',
+ description: 'undefined',
references: [
- { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-current-indexpattern',
+ type: 'index-pattern',
+ },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-layer-layer0',
+ type: 'index-pattern',
+ },
],
state: {
datasourceStates: {
@@ -20,25 +26,27 @@ export const sampleAttributeKpi = {
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
columns: {
'x-axis-column-layer0': {
- sourceField: '@timestamp',
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
- params: { interval: 'auto' },
+ params: {
+ interval: 'auto',
+ },
scale: 'interval',
+ sourceField: '@timestamp',
},
'y-axis-column-layer0': {
dataType: 'number',
+ filter: {
+ language: 'kuery',
+ query: 'transaction.type: page-load and processor.event: transaction',
+ },
isBucketed: false,
label: 'Page views',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
- filter: {
- query: 'transaction.type: page-load and processor.event: transaction',
- language: 'kuery',
- },
},
},
incompleteColumns: {},
@@ -46,27 +54,52 @@ export const sampleAttributeKpi = {
},
},
},
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: '',
+ },
visualization: {
- legend: { isVisible: true, position: 'right' },
- valueLabels: 'hide',
- fittingFunction: 'Linear',
+ axisTitlesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
curveType: 'CURVE_MONOTONE_X',
- axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
- gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- preferredSeriesType: 'line',
+ fittingFunction: 'Linear',
+ gridlinesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
layers: [
{
accessors: ['y-axis-column-layer0'],
layerId: 'layer0',
layerType: 'data',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
xAccessor: 'x-axis-column-layer0',
+ yConfig: [
+ {
+ color: 'green',
+ forAccessor: 'y-axis-column-layer0',
+ },
+ ],
},
],
+ legend: {
+ isVisible: true,
+ position: 'right',
+ },
+ preferredSeriesType: 'line',
+ tickLabelsVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
+ valueLabels: 'hide',
},
- query: { query: '', language: 'kuery' },
- filters: [],
},
+ title: 'Prefilled from exploratory view app',
+ visualizationType: 'lnsXY',
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
index c7d2d21581e7a..56e6cb5210356 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
import rison, { RisonValue } from 'rison-node';
-import type { SeriesUrl, UrlFilter } from '../types';
+import type { ReportViewType, SeriesUrl, UrlFilter } from '../types';
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
@@ -16,40 +16,43 @@ export function convertToShortUrl(series: SeriesUrl) {
const {
operationType,
seriesType,
- reportType,
breakdown,
filters,
reportDefinitions,
dataType,
selectedMetricField,
+ hidden,
+ name,
+ color,
...restSeries
} = series;
return {
[URL_KEYS.OPERATION_TYPE]: operationType,
- [URL_KEYS.REPORT_TYPE]: reportType,
[URL_KEYS.SERIES_TYPE]: seriesType,
[URL_KEYS.BREAK_DOWN]: breakdown,
[URL_KEYS.FILTERS]: filters,
[URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions,
[URL_KEYS.DATA_TYPE]: dataType,
[URL_KEYS.SELECTED_METRIC]: selectedMetricField,
+ [URL_KEYS.HIDDEN]: hidden,
+ [URL_KEYS.NAME]: name,
+ [URL_KEYS.COLOR]: color,
...restSeries,
};
}
-export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
- const allSeriesIds = Object.keys(allSeries);
-
- const allShortSeries: AllShortSeries = {};
-
- allSeriesIds.forEach((seriesKey) => {
- allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]);
- });
+export function createExploratoryViewUrl(
+ { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries },
+ baseHref = ''
+) {
+ const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series));
return (
baseHref +
- `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}`
+ `/app/observability/exploratory-view/#?reportType=${reportType}&sr=${rison.encode(
+ allShortSeries as unknown as RisonValue
+ )}`
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index a3b5130e9830b..8f061fcbfbf26 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -6,12 +6,18 @@
*/
import React from 'react';
-import { screen, waitFor } from '@testing-library/dom';
+import { screen } from '@testing-library/dom';
import { render, mockAppIndexPattern } from './rtl_helpers';
import { ExploratoryView } from './exploratory_view';
import * as obsvInd from './utils/observability_index_patterns';
+import * as pluginHook from '../../../hooks/use_plugin_context';
import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs';
+jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
+ appMountParameters: {
+ setHeaderActionMenu: jest.fn(),
+ },
+} as any);
describe('ExploratoryView', () => {
mockAppIndexPattern();
@@ -40,36 +46,22 @@ describe('ExploratoryView', () => {
});
it('renders exploratory view', async () => {
- render( );
+ render( , { initSeries: { data: [] } });
- expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
+ expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument();
+ expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument();
+ expect(await screen.findByText(/Refresh/i)).toBeInTheDocument();
expect(
await screen.findByRole('heading', { name: /Performance Distribution/i })
).toBeInTheDocument();
});
it('renders lens component when there is series', async () => {
- const initSeries = {
- data: {
- 'ux-series': {
- isNew: true,
- dataType: 'ux' as const,
- reportType: 'data-distribution' as const,
- breakdown: 'user_agent .name',
- reportDefinitions: { 'service.name': ['elastic-co'] },
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- render( , { initSeries });
+ render( );
- expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument();
expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument();
- await waitFor(() => {
- screen.getByRole('table', { name: /this table contains 1 rows\./i });
- });
+ expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index af04108c56790..faf064868dec5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -4,11 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { i18n } from '@kbn/i18n';
import React, { useEffect, useRef, useState } from 'react';
-import { EuiPanel, EuiTitle } from '@elastic/eui';
+import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
-import { isEmpty } from 'lodash';
+import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
@@ -16,40 +17,15 @@ import { useSeriesStorage } from './hooks/use_series_storage';
import { useLensAttributes } from './hooks/use_lens_attributes';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
-import { SeriesBuilder } from './series_builder/series_builder';
-import { SeriesUrl } from './types';
+import { SeriesViews } from './views/series_views';
import { LensEmbeddable } from './lens_embeddable';
import { EmptyView } from './components/empty_view';
-export const combineTimeRanges = (
- allSeries: Record,
- firstSeries?: SeriesUrl
-) => {
- let to: string = '';
- let from: string = '';
- if (firstSeries?.reportType === 'kpi-over-time') {
- return firstSeries.time;
- }
- Object.values(allSeries ?? {}).forEach((series) => {
- if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) {
- const seriesTo = new Date(series.time.to);
- const seriesFrom = new Date(series.time.from);
- if (!to || seriesTo > new Date(to)) {
- to = series.time.to;
- }
- if (!from || seriesFrom < new Date(from)) {
- from = series.time.from;
- }
- }
- });
- return { to, from };
-};
+export type PanelId = 'seriesPanel' | 'chartPanel';
export function ExploratoryView({
saveAttributes,
- multiSeries,
}: {
- multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
const {
@@ -69,20 +45,19 @@ export function ExploratoryView({
const { loadIndexPattern, loading } = useAppIndexPatternContext();
- const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage();
+ const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage();
const lensAttributesT = useLensAttributes();
const setHeightOffset = () => {
if (seriesBuilderRef?.current && wrapperRef.current) {
const headerOffset = wrapperRef.current.getBoundingClientRect().top;
- const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height;
- setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`);
+ setHeight(`calc(100vh - ${headerOffset + 40}px)`);
}
};
useEffect(() => {
- Object.values(allSeries).forEach((seriesT) => {
+ allSeries.forEach((seriesT) => {
loadIndexPattern({
dataType: seriesT.dataType,
});
@@ -96,38 +71,102 @@ export function ExploratoryView({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(lensAttributesT ?? {})]);
+ }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]);
useEffect(() => {
setHeightOffset();
});
+ const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>();
+
+ const [hiddenPanel, setHiddenPanel] = useState('');
+
+ const onCollapse = (panelId: string) => {
+ setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId));
+ };
+
+ const onChange = (panelId: PanelId) => {
+ onCollapse(panelId);
+ if (collapseFn.current) {
+ collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left');
+ }
+ };
+
return (
{lens ? (
<>
-
+
- {lensAttributes ? (
-
- ) : (
-
+
+ {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
+ collapseFn.current = (id, direction) => togglePanel?.(id, { direction });
+
+ return (
+ <>
+
+ {lensAttributes ? (
+
+ ) : (
+
+ )}
+
+
+
+ {hiddenPanel === 'chartPanel' ? (
+ onChange('chartPanel')} iconType="arrowDown">
+ {SHOW_CHART_LABEL}
+
+ ) : (
+ onChange('chartPanel')}
+ iconType="arrowUp"
+ color="text"
+ >
+ {HIDE_CHART_LABEL}
+
+ )}
+
+
+ >
+ );
+ }}
+
+ {hiddenPanel === 'seriesPanel' && (
+ onChange('seriesPanel')} iconType="arrowUp">
+ {PREVIEW_LABEL}
+
)}
-
>
) : (
-
- {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', {
- defaultMessage:
- 'Lens app is not available, please enable Lens to use exploratory view.',
- })}
-
+ {LENS_NOT_AVAILABLE}
)}
@@ -147,4 +186,39 @@ const Wrapper = styled(EuiPanel)`
margin: 0 auto;
width: 100%;
overflow-x: auto;
+ position: relative;
+`;
+
+const ShowPreview = styled(EuiButtonEmpty)`
+ position: absolute;
+ bottom: 34px;
+`;
+const HideChart = styled(EuiButtonEmpty)`
+ position: absolute;
+ top: -35px;
+ right: 50px;
`;
+const ShowChart = styled(EuiButtonEmpty)`
+ position: absolute;
+ top: -10px;
+ right: 50px;
+`;
+
+const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', {
+ defaultMessage: 'Hide chart',
+});
+
+const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', {
+ defaultMessage: 'Show chart',
+});
+
+const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', {
+ defaultMessage: 'Preview',
+});
+
+const LENS_NOT_AVAILABLE = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.lensDisabled',
+ {
+ defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
index 619ea0d21ae15..b8f16f3e5effb 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
@@ -23,14 +23,15 @@ describe('AddToCaseAction', function () {
it('should be able to click add to case button', async function () {
const initSeries = {
- data: {
- 'uptime-pings-histogram': {
+ data: [
+ {
+ name: 'test-series',
dataType: 'synthetics' as const,
reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
const { findByText, core } = render(
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
index 4fa8deb2700d0..bc813a4980e78 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
@@ -17,7 +17,7 @@ import { Case, SubCase } from '../../../../../../cases/common';
import { observabilityFeatureId } from '../../../../../common';
export interface AddToCaseProps {
- timeRange: { from: string; to: string };
+ timeRange?: { from: string; to: string };
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
@@ -54,6 +54,7 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) {
return (
<>
);
- getByText('Open in Lens');
- });
-
- it('should be able to click open in lens', function () {
- const initSeries = {
- data: {
- 'uptime-pings-histogram': {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- const { getByText, core } = render(
- ,
- { initSeries }
- );
- fireEvent.click(getByText('Open in Lens'));
-
- expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
- expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
- {
- attributes: { title: 'Performance distribution' },
- id: '',
- timeRange: {
- from: 'now-15m',
- to: 'now',
- },
- },
- { openInNewTab: true }
- );
+ getByText('Refresh');
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 7adef4779ea94..bec8673f88b4e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -5,44 +5,37 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public';
-import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { TypedLensByValueInput } from '../../../../../../lens/public';
import { DataViewLabels } from '../configurations/constants';
-import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
-import { combineTimeRanges } from '../exploratory_view';
-import { AddToCaseAction } from './add_to_case_action';
+import { LastUpdated } from './last_updated';
+import { combineTimeRanges } from '../lens_embeddable';
+import { ExpViewActionMenu } from '../components/action_menu';
interface Props {
- seriesId: string;
+ seriesId?: number;
+ lastUpdated?: number;
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
-export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
- const kServices = useKibana().services;
+export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) {
+ const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage();
- const { lens } = kServices;
+ const series = seriesId ? getSeries(seriesId) : undefined;
- const { getSeries, allSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const [isSaveOpen, setIsSaveOpen] = useState(false);
-
- const LensSaveModalComponent = lens.SaveModalComponent;
-
- const timeRange = combineTimeRanges(allSeries, series);
+ const timeRange = combineTimeRanges(reportType, allSeries, series);
return (
<>
+
- {DataViewLabels[series.reportType] ??
+ {DataViewLabels[reportType] ??
i18n.translate('xpack.observability.expView.heading.label', {
defaultMessage: 'Analyze data',
})}{' '}
@@ -58,58 +51,18 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
-
+
- {
- if (lensAttributes) {
- lens.navigateToPrefilledEditor(
- {
- id: '',
- timeRange,
- attributes: lensAttributes,
- },
- {
- openInNewTab: true,
- }
- );
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.openInLens', {
- defaultMessage: 'Open in Lens',
- })}
-
-
-
- {
- if (lensAttributes) {
- setIsSaveOpen(true);
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
- defaultMessage: 'Save',
- })}
+ setLastRefresh(Date.now())}>
+ {REFRESH_LABEL}
-
- {isSaveOpen && lensAttributes && (
- setIsSaveOpen(false)}
- onSave={() => {}}
- />
- )}
>
);
}
+
+const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', {
+ defaultMessage: 'Refresh',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
similarity index 55%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
index 874171de123d2..c352ec0423dd8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
@@ -8,6 +8,7 @@
import React, { useEffect, useState } from 'react';
import { EuiIcon, EuiText } from '@elastic/eui';
import moment from 'moment';
+import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
lastUpdated?: number;
@@ -18,20 +19,34 @@ export function LastUpdated({ lastUpdated }: Props) {
useEffect(() => {
const interVal = setInterval(() => {
setRefresh(Date.now());
- }, 1000);
+ }, 5000);
return () => {
clearInterval(interVal);
};
}, []);
+ useEffect(() => {
+ setRefresh(Date.now());
+ }, [lastUpdated]);
+
if (!lastUpdated) {
return null;
}
+ const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5;
+ const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10;
+
return (
-
- Last Updated: {moment(lastUpdated).from(refresh)}
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
index 5ec9e1d4ab4b5..d1e15aa916eed 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
@@ -25,7 +25,7 @@ async function addToCase(
http: HttpSetup,
theCase: Case | SubCase,
attributes: TypedLensByValueInput['attributes'],
- timeRange: { from: string; to: string }
+ timeRange?: { from: string; to: string }
) {
const apiPath = `/api/cases/${theCase?.id}/comments`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
index 88818665bbe2a..83a7ac1ae17dc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
@@ -27,7 +27,7 @@ interface ProviderProps {
}
type HasAppDataState = Record;
-type IndexPatternState = Record;
+export type IndexPatternState = Record;
type LoadingState = Record;
export function IndexPatternContextProvider({ children }: ProviderProps) {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
new file mode 100644
index 0000000000000..4f19a8131f669
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 { useCallback, useEffect, useState } from 'react';
+import { useKibana } from '../../../../utils/kibana_react';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { useAppIndexPatternContext } from './use_app_index_pattern';
+import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils';
+import { getFiltersFromDefs } from './use_lens_attributes';
+import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
+
+interface UseDiscoverLink {
+ seriesConfig?: SeriesConfig;
+ series: SeriesUrl;
+}
+
+export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => {
+ const kServices = useKibana().services;
+ const {
+ application: { navigateToUrl },
+ } = kServices;
+
+ const { indexPatterns } = useAppIndexPatternContext();
+
+ const urlGenerator = kServices.discover?.urlGenerator;
+ const [discoverUrl, setDiscoverUrl] = useState('');
+
+ useEffect(() => {
+ const indexPattern = indexPatterns?.[series.dataType];
+
+ const definitions = series.reportDefinitions ?? {};
+ const filters = [...(seriesConfig?.baseFilters ?? [])];
+
+ const definitionFilters = getFiltersFromDefs(definitions);
+
+ definitionFilters.forEach(({ field, values = [] }) => {
+ if (values.length > 1) {
+ filters.push(buildPhrasesFilter(field, values, indexPattern)[0]);
+ } else {
+ filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]);
+ }
+ });
+
+ const selectedMetricField = series.selectedMetricField;
+
+ if (
+ selectedMetricField &&
+ selectedMetricField !== RECORDS_FIELD &&
+ selectedMetricField !== RECORDS_PERCENTAGE_FIELD
+ ) {
+ filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]);
+ }
+
+ const getDiscoverUrl = async () => {
+ if (!urlGenerator?.createUrl) return;
+
+ const newUrl = await urlGenerator.createUrl({
+ filters,
+ indexPatternId: indexPattern?.id,
+ });
+ setDiscoverUrl(newUrl);
+ };
+ getDiscoverUrl();
+ }, [
+ indexPatterns,
+ series.dataType,
+ series.reportDefinitions,
+ series.selectedMetricField,
+ seriesConfig?.baseFilters,
+ urlGenerator,
+ ]);
+
+ const onClick = useCallback(
+ (event: React.MouseEvent) => {
+ if (discoverUrl) {
+ event.preventDefault();
+
+ return navigateToUrl(discoverUrl);
+ }
+ },
+ [discoverUrl, navigateToUrl]
+ );
+
+ return {
+ href: discoverUrl,
+ onClick,
+ };
+};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 8bb265b4f6d89..ef974d54e6cdc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -9,12 +9,18 @@ import { useMemo } from 'react';
import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../../lens/public';
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
-import { useSeriesStorage } from './use_series_storage';
+import {
+ AllSeries,
+ allSeriesKey,
+ convertAllShortSeries,
+ useSeriesStorage,
+} from './use_series_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { SeriesUrl, UrlFilter } from '../types';
import { useAppIndexPatternContext } from './use_app_index_pattern';
import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox';
+import { useTheme } from '../../../../hooks/use_theme';
export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => {
return Object.entries(reportDefinitions ?? {})
@@ -28,41 +34,54 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio
};
export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
- const { allSeriesIds, allSeries } = useSeriesStorage();
+ const { storage, allSeries, lastRefresh, reportType } = useSeriesStorage();
const { indexPatterns } = useAppIndexPatternContext();
+ const theme = useTheme();
+
return useMemo(() => {
- if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
+ if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) {
return null;
}
+ const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []);
+
const layerConfigs: LayerConfig[] = [];
- allSeriesIds.forEach((seriesIdT) => {
- const seriesT = allSeries[seriesIdT];
- const indexPattern = indexPatterns?.[seriesT?.dataType];
- if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
+ allSeriesT.forEach((series, seriesIndex) => {
+ const indexPattern = indexPatterns?.[series?.dataType];
+
+ if (
+ indexPattern &&
+ !isEmpty(series.reportDefinitions) &&
+ !series.hidden &&
+ series.selectedMetricField
+ ) {
const seriesConfig = getDefaultConfigs({
- reportType: seriesT.reportType,
- dataType: seriesT.dataType,
+ reportType,
indexPattern,
+ dataType: series.dataType,
});
- const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
- getFiltersFromDefs(seriesT.reportDefinitions)
+ const filters: UrlFilter[] = (series.filters ?? []).concat(
+ getFiltersFromDefs(series.reportDefinitions)
);
+ const color = `euiColorVis${seriesIndex}`;
+
layerConfigs.push({
filters,
indexPattern,
seriesConfig,
- time: seriesT.time,
- breakdown: seriesT.breakdown,
- seriesType: seriesT.seriesType,
- operationType: seriesT.operationType,
- reportDefinitions: seriesT.reportDefinitions ?? {},
- selectedMetricField: seriesT.selectedMetricField,
+ time: series.time,
+ name: series.name,
+ breakdown: series.breakdown,
+ seriesType: series.seriesType,
+ operationType: series.operationType,
+ reportDefinitions: series.reportDefinitions ?? {},
+ selectedMetricField: series.selectedMetricField,
+ color: series.color ?? (theme.eui as unknown as Record)[color],
});
}
});
@@ -73,6 +92,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null
const lensAttributes = new LensAttributes(layerConfigs);
- return lensAttributes.getJSON();
- }, [indexPatterns, allSeriesIds, allSeries]);
+ return lensAttributes.getJSON(lastRefresh);
+ }, [indexPatterns, allSeries, reportType, storage, theme, lastRefresh]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
index 2d2618bc46152..f2a6130cdc59d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
@@ -6,18 +6,16 @@
*/
import { useSeriesStorage } from './use_series_storage';
-import { UrlFilter } from '../types';
+import { SeriesUrl, UrlFilter } from '../types';
export interface UpdateFilter {
field: string;
- value: string;
+ value: string | string[];
negate?: boolean;
}
-export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => {
+ const { setSeries } = useSeriesStorage();
const filters = series.filters ?? [];
@@ -26,10 +24,14 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
.map((filter) => {
if (filter.field === field) {
if (negate) {
- const notValuesN = filter.notValues?.filter((val) => val !== value);
+ const notValuesN = filter.notValues?.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
return { ...filter, notValues: notValuesN };
} else {
- const valuesN = filter.values?.filter((val) => val !== value);
+ const valuesN = filter.values?.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
return { ...filter, values: valuesN };
}
}
@@ -43,9 +45,9 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
const addFilter = ({ field, value, negate }: UpdateFilter) => {
const currFilter: UrlFilter = { field };
if (negate) {
- currFilter.notValues = [value];
+ currFilter.notValues = value instanceof Array ? value : [value];
} else {
- currFilter.values = [value];
+ currFilter.values = value instanceof Array ? value : [value];
}
if (filters.length === 0) {
setSeries(seriesId, { ...series, filters: [currFilter] });
@@ -65,13 +67,26 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
const currNotValues = currFilter.notValues ?? [];
const currValues = currFilter.values ?? [];
- const notValues = currNotValues.filter((val) => val !== value);
- const values = currValues.filter((val) => val !== value);
+ const notValues = currNotValues.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
+
+ const values = currValues.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
if (negate) {
- notValues.push(value);
+ if (value instanceof Array) {
+ notValues.push(...value);
+ } else {
+ notValues.push(value);
+ }
} else {
- values.push(value);
+ if (value instanceof Array) {
+ values.push(...value);
+ } else {
+ values.push(value);
+ }
}
currFilter.notValues = notValues.length > 0 ? notValues : undefined;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
index c32acc47abd1b..ce6d7bd94d8e4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
@@ -6,37 +6,39 @@
*/
import React, { useEffect } from 'react';
-
-import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage';
+import { Route, Router } from 'react-router-dom';
import { render } from '@testing-library/react';
+import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage';
+import { getHistoryFromUrl } from '../rtl_helpers';
-const mockSingleSeries = {
- 'performance-distribution': {
- reportType: 'data-distribution',
+const mockSingleSeries = [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
-};
+];
-const mockMultipleSeries = {
- 'performance-distribution': {
- reportType: 'data-distribution',
+const mockMultipleSeries = [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- 'kpi-over-time': {
- reportType: 'kpi-over-time',
+ {
+ name: 'kpi-over-time',
dataType: 'synthetics',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
-};
+];
-describe('userSeries', function () {
+describe('userSeriesStorage', function () {
function setupTestComponent(seriesData: any) {
const setData = jest.fn();
+
function TestComponent() {
const data = useSeriesStorage();
@@ -48,11 +50,20 @@ describe('userSeries', function () {
}
render(
-
-
-
+
+
+ (key === 'sr' ? seriesData : null)),
+ set: jest.fn(),
+ }}
+ >
+
+
+
+
);
return setData;
@@ -63,22 +74,20 @@ describe('userSeries', function () {
expect(setData).toHaveBeenCalledTimes(2);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {
- 'performance-distribution': {
- breakdown: 'user_agent.name',
+ allSeries: [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- },
- allSeriesIds: ['performance-distribution'],
+ ],
firstSeries: {
- breakdown: 'user_agent.name',
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- firstSeriesId: 'performance-distribution',
})
);
});
@@ -89,42 +98,38 @@ describe('userSeries', function () {
expect(setData).toHaveBeenCalledTimes(2);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {
- 'performance-distribution': {
- breakdown: 'user_agent.name',
+ allSeries: [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- 'kpi-over-time': {
- reportType: 'kpi-over-time',
+ {
+ name: 'kpi-over-time',
dataType: 'synthetics',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- },
- allSeriesIds: ['performance-distribution', 'kpi-over-time'],
+ ],
firstSeries: {
- breakdown: 'user_agent.name',
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- firstSeriesId: 'performance-distribution',
})
);
});
it('should return expected result when there are no series', function () {
- const setData = setupTestComponent({});
+ const setData = setupTestComponent([]);
- expect(setData).toHaveBeenCalledTimes(2);
+ expect(setData).toHaveBeenCalledTimes(1);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {},
- allSeriesIds: [],
+ allSeries: [],
firstSeries: undefined,
- firstSeriesId: undefined,
})
);
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
index a47a124d14b4d..d9a5adc822140 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
@@ -22,13 +22,17 @@ import { OperationType, SeriesType } from '../../../../../../lens/public';
import { URL_KEYS } from '../configurations/constants/url_constants';
export interface SeriesContextValue {
- firstSeries: SeriesUrl;
- firstSeriesId: string;
- allSeriesIds: string[];
+ firstSeries?: SeriesUrl;
+ lastRefresh: number;
+ setLastRefresh: (val: number) => void;
+ applyChanges: () => void;
allSeries: AllSeries;
- setSeries: (seriesIdN: string, newValue: SeriesUrl) => void;
- getSeries: (seriesId: string) => SeriesUrl;
- removeSeries: (seriesId: string) => void;
+ setSeries: (seriesIndex: number, newValue: SeriesUrl) => void;
+ getSeries: (seriesIndex: number) => SeriesUrl | undefined;
+ removeSeries: (seriesIndex: number) => void;
+ setReportType: (reportType: string) => void;
+ storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
+ reportType: ReportViewType;
}
export const UrlStorageContext = createContext({} as SeriesContextValue);
@@ -36,72 +40,87 @@ interface ProviderProps {
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
}
-function convertAllShortSeries(allShortSeries: AllShortSeries) {
- const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = {};
- allSeriesIds.forEach((seriesKey) => {
- allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
- });
-
- return allSeriesN;
+export function convertAllShortSeries(allShortSeries: AllShortSeries) {
+ return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries));
}
+export const allSeriesKey = 'sr';
+const reportTypeKey = 'reportType';
+
export function UrlStorageContextProvider({
children,
storage,
}: ProviderProps & { children: JSX.Element }) {
- const allSeriesKey = 'sr';
-
- const [allShortSeries, setAllShortSeries] = useState(
- () => storage.get(allSeriesKey) ?? {}
- );
const [allSeries, setAllSeries] = useState(() =>
- convertAllShortSeries(storage.get(allSeriesKey) ?? {})
+ convertAllShortSeries(storage.get(allSeriesKey) ?? [])
);
- const [firstSeriesId, setFirstSeriesId] = useState('');
+
+ const [lastRefresh, setLastRefresh] = useState(() => Date.now());
+
+ const [reportType, setReportType] = useState(
+ () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? ''
+ );
+
const [firstSeries, setFirstSeries] = useState();
useEffect(() => {
- const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
+ const firstSeriesT = allSeries?.[0];
- setAllSeries(allSeriesN);
- setFirstSeriesId(allSeriesIds?.[0]);
- setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]);
- (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
- }, [allShortSeries, storage]);
+ setFirstSeries(firstSeriesT);
+ }, [allSeries, storage]);
- const setSeries = (seriesIdN: string, newValue: SeriesUrl) => {
- setAllShortSeries((prevState) => {
- prevState[seriesIdN] = convertToShortUrl(newValue);
- return { ...prevState };
- });
- };
+ const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => {
+ setAllSeries((prevAllSeries) => {
+ const newStateRest = prevAllSeries.map((series, index) => {
+ if (index === seriesIndex) {
+ return newValue;
+ }
+ return series;
+ });
+
+ if (prevAllSeries.length === seriesIndex) {
+ return [...newStateRest, newValue];
+ }
- const removeSeries = (seriesIdN: string) => {
- setAllShortSeries((prevState) => {
- delete prevState[seriesIdN];
- return { ...prevState };
+ return [...newStateRest];
});
- };
+ }, []);
- const allSeriesIds = Object.keys(allShortSeries);
+ useEffect(() => {
+ (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType);
+ }, [reportType, storage]);
+
+ const removeSeries = useCallback((seriesIndex: number) => {
+ setAllSeries((prevAllSeries) =>
+ prevAllSeries.filter((seriesT, index) => index !== seriesIndex)
+ );
+ }, []);
const getSeries = useCallback(
- (seriesId?: string) => {
- return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl);
+ (seriesIndex: number) => {
+ return allSeries[seriesIndex];
},
[allSeries]
);
+ const applyChanges = useCallback(() => {
+ const allShortSeries = allSeries.map((series) => convertToShortUrl(series));
+
+ (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
+ setLastRefresh(Date.now());
+ }, [allSeries, storage]);
+
const value = {
+ applyChanges,
storage,
getSeries,
setSeries,
removeSeries,
- firstSeriesId,
allSeries,
- allSeriesIds,
+ lastRefresh,
+ setLastRefresh,
+ setReportType,
+ reportType: storage.get(reportTypeKey) as ReportViewType,
firstSeries: firstSeries!,
};
return {children} ;
@@ -112,10 +131,9 @@ export function useSeriesStorage() {
}
function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
- const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue;
+ const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue;
return {
operationType: op,
- reportType: rt!,
seriesType: st,
breakdown: bd,
filters: ft!,
@@ -123,26 +141,31 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
reportDefinitions: rdf,
dataType: dt!,
selectedMetricField: mt,
+ hidden: h,
+ name: n,
+ color: c,
...restSeries,
};
}
interface ShortUrlSeries {
[URL_KEYS.OPERATION_TYPE]?: OperationType;
- [URL_KEYS.REPORT_TYPE]?: ReportViewType;
[URL_KEYS.DATA_TYPE]?: AppDataType;
[URL_KEYS.SERIES_TYPE]?: SeriesType;
[URL_KEYS.BREAK_DOWN]?: string;
[URL_KEYS.FILTERS]?: UrlFilter[];
[URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition;
[URL_KEYS.SELECTED_METRIC]?: string;
+ [URL_KEYS.HIDDEN]?: boolean;
+ [URL_KEYS.NAME]: string;
+ [URL_KEYS.COLOR]?: string;
time?: {
to: string;
from: string;
};
}
-export type AllShortSeries = Record;
-export type AllSeries = Record;
+export type AllShortSeries = ShortUrlSeries[];
+export type AllSeries = SeriesUrl[];
-export const NEW_SERIES_KEY = 'new-series-key';
+export const NEW_SERIES_KEY = 'new-series';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
index e55752ceb62ba..3de29b02853e8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
@@ -25,11 +25,9 @@ import { TypedLensByValueInput } from '../../../../../lens/public';
export function ExploratoryViewPage({
saveAttributes,
- multiSeries = false,
useSessionStorage = false,
}: {
useSessionStorage?: boolean;
- multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' });
@@ -61,7 +59,7 @@ export function ExploratoryViewPage({
-
+
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
index 4cb586fe94ceb..9e4d9486dc155 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
@@ -7,16 +7,51 @@
import { i18n } from '@kbn/i18n';
import React, { Dispatch, SetStateAction, useCallback } from 'react';
-import { combineTimeRanges } from './exploratory_view';
+import styled from 'styled-components';
+import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useSeriesStorage } from './hooks/use_series_storage';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { ReportViewType, SeriesUrl } from './types';
+import { ReportTypes } from './configurations/constants';
interface Props {
lensAttributes: TypedLensByValueInput['attributes'];
setLastUpdated: Dispatch>;
}
+export const combineTimeRanges = (
+ reportType: ReportViewType,
+ allSeries: SeriesUrl[],
+ firstSeries?: SeriesUrl
+) => {
+ let to: string = '';
+ let from: string = '';
+
+ if (reportType === ReportTypes.KPI) {
+ return firstSeries?.time;
+ }
+
+ allSeries.forEach((series) => {
+ if (
+ series.dataType &&
+ series.selectedMetricField &&
+ !isEmpty(series.reportDefinitions) &&
+ series.time
+ ) {
+ const seriesTo = new Date(series.time.to);
+ const seriesFrom = new Date(series.time.from);
+ if (!to || seriesTo > new Date(to)) {
+ to = series.time.to;
+ }
+ if (!from || seriesFrom < new Date(from)) {
+ from = series.time.from;
+ }
+ }
+ });
+
+ return { to, from };
+};
export function LensEmbeddable(props: Props) {
const { lensAttributes, setLastUpdated } = props;
@@ -27,9 +62,11 @@ export function LensEmbeddable(props: Props) {
const LensComponent = lens?.EmbeddableComponent;
- const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage();
+ const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage();
- const timeRange = combineTimeRanges(allSeries, series);
+ const firstSeriesId = 0;
+
+ const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null;
const onLensLoad = useCallback(() => {
setLastUpdated(Date.now());
@@ -37,9 +74,9 @@ export function LensEmbeddable(props: Props) {
const onBrushEnd = useCallback(
({ range }: { range: number[] }) => {
- if (series?.reportType !== 'data-distribution') {
+ if (reportType !== 'data-distribution' && firstSeries) {
setSeries(firstSeriesId, {
- ...series,
+ ...firstSeries,
time: {
from: new Date(range[0]).toISOString(),
to: new Date(range[1]).toISOString(),
@@ -53,16 +90,30 @@ export function LensEmbeddable(props: Props) {
);
}
},
- [notifications?.toasts, series, firstSeriesId, setSeries]
+ [reportType, setSeries, firstSeries, notifications?.toasts]
);
+ if (timeRange === null || !firstSeries) {
+ return null;
+ }
+
return (
-
+
+
+
);
}
+
+const LensWrapper = styled.div`
+ height: 100%;
+
+ &&& > div {
+ height: 100%;
+ }
+`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index a577a8df3e3d9..48a22f91eb7f6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -10,7 +10,7 @@ import React, { ReactElement } from 'react';
import { stringify } from 'query-string';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render as reactTestLibRender, RenderOptions } from '@testing-library/react';
-import { Router } from 'react-router-dom';
+import { Route, Router } from 'react-router-dom';
import { createMemoryHistory, History } from 'history';
import { CoreStart } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react';
@@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/
import { lensPluginMock } from '../../../../../lens/public/mocks';
import * as useAppIndexPatternHook from './hooks/use_app_index_pattern';
import { IndexPatternContextProvider } from './hooks/use_app_index_pattern';
-import { AllSeries, UrlStorageContext } from './hooks/use_series_storage';
+import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage';
import * as fetcherHook from '../../../hooks/use_fetcher';
import * as useSeriesFilterHook from './hooks/use_series_filters';
@@ -35,10 +35,12 @@ import indexPatternData from './configurations/test_data/test_index_pattern.json
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
+
+import { AppDataType, SeriesUrl, UrlFilter } from './types';
import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs';
-import { AppDataType, UrlFilter } from './types';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { ListItem } from '../../../hooks/use_values_list';
+import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames';
import { casesPluginMock } from '../../../../../cases/public/mocks';
interface KibanaProps {
@@ -157,9 +159,11 @@ export function MockRouter({
}: MockRouterProps) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
@@ -172,7 +176,7 @@ export function render(
core: customCore,
kibanaProps,
renderOptions,
- url,
+ url = '/app/observability/exploratory-view/',
initSeries = {},
}: RenderRouterOptions = {}
) {
@@ -202,7 +206,7 @@ export function render(
};
}
-const getHistoryFromUrl = (url: Url) => {
+export const getHistoryFromUrl = (url: Url) => {
if (typeof url === 'string') {
return createMemoryHistory({
initialEntries: [url],
@@ -251,6 +255,15 @@ export const mockUseValuesList = (values?: ListItem[]) => {
return { spy, onRefreshTimeRange };
};
+export const mockUxSeries = {
+ name: 'performance-distribution',
+ dataType: 'ux',
+ breakdown: 'user_agent.name',
+ time: { from: 'now-15m', to: 'now' },
+ reportDefinitions: { 'service.name': ['elastic-co'] },
+ selectedMetricField: TRANSACTION_DURATION,
+} as SeriesUrl;
+
function mockSeriesStorageContext({
data,
filters,
@@ -260,34 +273,34 @@ function mockSeriesStorageContext({
filters?: UrlFilter[];
breakdown?: string;
}) {
- const mockDataSeries = data || {
- 'performance-distribution': {
- reportType: 'data-distribution',
- dataType: 'ux',
- breakdown: breakdown || 'user_agent.name',
- time: { from: 'now-15m', to: 'now' },
- ...(filters ? { filters } : {}),
- },
+ const testSeries = {
+ ...mockUxSeries,
+ breakdown: breakdown || 'user_agent.name',
+ ...(filters ? { filters } : {}),
};
- const allSeriesIds = Object.keys(mockDataSeries);
- const firstSeriesId = allSeriesIds?.[0];
- const series = mockDataSeries[firstSeriesId];
+ const mockDataSeries = data || [testSeries];
const removeSeries = jest.fn();
const setSeries = jest.fn();
- const getSeries = jest.fn().mockReturnValue(series);
+ const getSeries = jest.fn().mockReturnValue(testSeries);
return {
- firstSeriesId,
- allSeriesIds,
removeSeries,
setSeries,
getSeries,
- firstSeries: mockDataSeries[firstSeriesId],
+ autoApply: true,
+ reportType: 'data-distribution',
+ lastRefresh: Date.now(),
+ setLastRefresh: jest.fn(),
+ setAutoApply: jest.fn(),
+ applyChanges: jest.fn(),
+ firstSeries: mockDataSeries[0],
allSeries: mockDataSeries,
- };
+ setReportType: jest.fn(),
+ storage: { get: jest.fn().mockReturnValue(mockDataSeries) } as any,
+ } as SeriesContextValue;
}
export function mockUseSeriesFilter() {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
deleted file mode 100644
index b10702ebded57..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ /dev/null
@@ -1,62 +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 { fireEvent, screen } from '@testing-library/react';
-import { mockAppIndexPattern, render } from '../../rtl_helpers';
-import { dataTypes, DataTypesCol } from './data_types_col';
-
-describe('DataTypesCol', function () {
- const seriesId = 'test-series-id';
-
- mockAppIndexPattern();
-
- it('should render properly', function () {
- const { getByText } = render( );
-
- dataTypes.forEach(({ label }) => {
- getByText(label);
- });
- });
-
- it('should set series on change', function () {
- const { setSeries } = render( );
-
- fireEvent.click(screen.getByText(/user experience \(rum\)/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'ux',
- isNew: true,
- time: {
- from: 'now-15m',
- to: 'now',
- },
- });
- });
-
- it('should set series on change on already selected', function () {
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- render( , { initSeries });
-
- const button = screen.getByRole('button', {
- name: /Synthetic Monitoring/i,
- });
-
- expect(button.classList).toContain('euiButton--fill');
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
deleted file mode 100644
index f386f62d9ed73..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ /dev/null
@@ -1,74 +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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import styled from 'styled-components';
-import { AppDataType } from '../../types';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-
-export const dataTypes: Array<{ id: AppDataType; label: string }> = [
- { id: 'synthetics', label: 'Synthetic Monitoring' },
- { id: 'ux', label: 'User Experience (RUM)' },
- { id: 'mobile', label: 'Mobile Experience' },
- // { id: 'infra_logs', label: 'Logs' },
- // { id: 'infra_metrics', label: 'Metrics' },
- // { id: 'apm', label: 'APM' },
-];
-
-export function DataTypesCol({ seriesId }: { seriesId: string }) {
- const { getSeries, setSeries, removeSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
- const { loading } = useAppIndexPatternContext();
-
- const onDataTypeChange = (dataType?: AppDataType) => {
- if (!dataType) {
- removeSeries(seriesId);
- } else {
- setSeries(seriesId || `${dataType}-series`, {
- dataType,
- isNew: true,
- time: series.time,
- } as any);
- }
- };
-
- const selectedDataType = series.dataType;
-
- return (
-
- {dataTypes.map(({ id: dataTypeId, label }) => (
-
- {
- onDataTypeChange(dataTypeId);
- }}
- >
- {label}
-
-
- ))}
-
- );
-}
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
-
-const Button = styled(EuiButton)`
- will-change: transform;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
deleted file mode 100644
index 6be78084ae195..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import styled from 'styled-components';
-import { SeriesDatePicker } from '../../series_date_picker';
-import { DateRangePicker } from '../../series_date_picker/date_range_picker';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-
-interface Props {
- seriesId: string;
-}
-export function DatePickerCol({ seriesId }: Props) {
- const { firstSeriesId, getSeries } = useSeriesStorage();
- const { reportType } = getSeries(firstSeriesId);
-
- return (
-
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
-
- ) : (
-
- )}
-
- );
-}
-
-const Wrapper = styled.div`
- .euiSuperDatePicker__flexWrapper {
- width: 100%;
- > .euiFlexItem {
- margin-right: 0px;
- }
- }
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
deleted file mode 100644
index a5e5ad3900ded..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ /dev/null
@@ -1,74 +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 { fireEvent, screen } from '@testing-library/react';
-import { getDefaultConfigs } from '../../configurations/default_configs';
-import { mockIndexPattern, render } from '../../rtl_helpers';
-import { ReportBreakdowns } from './report_breakdowns';
-import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
-
-describe('Series Builder ReportBreakdowns', function () {
- const seriesId = 'test-series-id';
- const dataViewSeries = getDefaultConfigs({
- reportType: 'data-distribution',
- dataType: 'ux',
- indexPattern: mockIndexPattern,
- });
-
- it('should render properly', function () {
- render( );
-
- screen.getByText('Select an option: , is selected');
- screen.getAllByText('Browser family');
- });
-
- it('should set new series breakdown on change', function () {
- const { setSeries } = render(
-
- );
-
- const btn = screen.getByRole('button', {
- name: /select an option: Browser family , is selected/i,
- hidden: true,
- });
-
- fireEvent.click(btn);
-
- fireEvent.click(screen.getByText(/operating system/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- breakdown: USER_AGENT_OS,
- dataType: 'ux',
- reportType: 'data-distribution',
- time: { from: 'now-15m', to: 'now' },
- });
- });
- it('should set undefined on new series on no select breakdown', function () {
- const { setSeries } = render(
-
- );
-
- const btn = screen.getByRole('button', {
- name: /select an option: Browser family , is selected/i,
- hidden: true,
- });
-
- fireEvent.click(btn);
-
- fireEvent.click(screen.getByText(/no breakdown/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- breakdown: undefined,
- dataType: 'ux',
- reportType: 'data-distribution',
- time: { from: 'now-15m', to: 'now' },
- });
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
deleted file mode 100644
index fa2d01691ce1d..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { Breakdowns } from '../../series_editor/columns/breakdowns';
-import { SeriesConfig } from '../../types';
-
-export function ReportBreakdowns({
- seriesId,
- seriesConfig,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- return (
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
deleted file mode 100644
index 7962bf2b924f7..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
+++ /dev/null
@@ -1,101 +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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
-import styled from 'styled-components';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { ReportMetricOptions } from '../report_metric_options';
-import { SeriesConfig } from '../../types';
-import { SeriesChartTypesSelect } from './chart_types';
-import { OperationTypeSelect } from './operation_type_select';
-import { DatePickerCol } from './date_picker_col';
-import { parseCustomFieldName } from '../../configurations/lens_attributes';
-import { ReportDefinitionField } from './report_definition_field';
-
-function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
- const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
-
- return columnType;
-}
-
-export function ReportDefinitionCol({
- seriesConfig,
- seriesId,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {};
-
- const { definitionFields, defaultSeriesType, hasOperationType, yAxisColumns, metricOptions } =
- seriesConfig;
-
- const onChange = (field: string, value?: string[]) => {
- if (!value?.[0]) {
- delete selectedReportDefinitions[field];
- setSeries(seriesId, {
- ...series,
- reportDefinitions: { ...selectedReportDefinitions },
- });
- } else {
- setSeries(seriesId, {
- ...series,
- reportDefinitions: { ...selectedReportDefinitions, [field]: value },
- });
- }
- };
-
- const columnType = getColumnType(seriesConfig, selectedMetricField);
-
- return (
-
-
-
-
-
- {definitionFields.map((field) => (
-
-
-
- ))}
- {metricOptions && (
-
-
-
- )}
- {(hasOperationType || columnType === 'operation') && (
-
-
-
- )}
-
-
-
-
- );
-}
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
deleted file mode 100644
index 0b183b5f20c03..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
+++ /dev/null
@@ -1,28 +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 { screen } from '@testing-library/react';
-import { ReportFilters } from './report_filters';
-import { getDefaultConfigs } from '../../configurations/default_configs';
-import { mockIndexPattern, render } from '../../rtl_helpers';
-
-describe('Series Builder ReportFilters', function () {
- const seriesId = 'test-series-id';
-
- const dataViewSeries = getDefaultConfigs({
- reportType: 'data-distribution',
- indexPattern: mockIndexPattern,
- dataType: 'ux',
- });
-
- it('should render properly', function () {
- render( );
-
- screen.getByText('Add filter');
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
deleted file mode 100644
index d5938c5387e8f..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
+++ /dev/null
@@ -1,29 +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 { SeriesFilter } from '../../series_editor/columns/series_filter';
-import { SeriesConfig } from '../../types';
-
-export function ReportFilters({
- seriesConfig,
- seriesId,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- return (
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
deleted file mode 100644
index 12ae8560453c9..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
+++ /dev/null
@@ -1,79 +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 { fireEvent, screen } from '@testing-library/react';
-import { mockAppIndexPattern, render } from '../../rtl_helpers';
-import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
-import { ReportTypes } from '../series_builder';
-import { DEFAULT_TIME } from '../../configurations/constants';
-
-describe('ReportTypesCol', function () {
- const seriesId = 'performance-distribution';
-
- mockAppIndexPattern();
-
- it('should render properly', function () {
- render( );
- screen.getByText('Performance distribution');
- screen.getByText('KPI over time');
- });
-
- it('should display empty message', function () {
- render( );
- screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT);
- });
-
- it('should set series on change', function () {
- const { setSeries } = render(
-
- );
-
- fireEvent.click(screen.getByText(/KPI over time/i));
-
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'ux',
- selectedMetricField: undefined,
- reportType: 'kpi-over-time',
- time: { from: 'now-15m', to: 'now' },
- });
- expect(setSeries).toHaveBeenCalledTimes(1);
- });
-
- it('should set selected as filled', function () {
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- isNew: true,
- },
- },
- };
-
- const { setSeries } = render(
- ,
- { initSeries }
- );
-
- const button = screen.getByRole('button', {
- name: /KPI over time/i,
- });
-
- expect(button.classList).toContain('euiButton--fill');
- fireEvent.click(button);
-
- // undefined on click selected
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'synthetics',
- time: DEFAULT_TIME,
- isNew: true,
- });
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
deleted file mode 100644
index c4eebbfaca3eb..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
+++ /dev/null
@@ -1,108 +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 { i18n } from '@kbn/i18n';
-import { map } from 'lodash';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import styled from 'styled-components';
-import { ReportViewType, SeriesUrl } from '../../types';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { DEFAULT_TIME } from '../../configurations/constants';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { ReportTypeItem } from '../series_builder';
-
-interface Props {
- seriesId: string;
- reportTypes: ReportTypeItem[];
-}
-
-export function ReportTypesCol({ seriesId, reportTypes }: Props) {
- const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
-
- const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
-
- const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
-
- if (!restSeries.dataType) {
- return (
-
- );
- }
-
- if (!loading && !hasData) {
- return (
-
- );
- }
-
- const disabledReportTypes: ReportViewType[] = map(
- reportTypes.filter(
- ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType
- ),
- 'reportType'
- );
-
- return reportTypes?.length > 0 ? (
-
- {reportTypes.map(({ reportType, label }) => (
-
- {
- if (reportType === selectedReportType) {
- setSeries(seriesId, {
- dataType: restSeries.dataType,
- time: DEFAULT_TIME,
- isNew: true,
- } as SeriesUrl);
- } else {
- setSeries(seriesId, {
- ...restSeries,
- reportType,
- selectedMetricField: undefined,
- breakdown: undefined,
- time: restSeries?.time ?? DEFAULT_TIME,
- });
- }
- }}
- >
- {label}
-
-
- ))}
-
- ) : (
- {SELECTED_DATA_TYPE_FOR_REPORT}
- );
-}
-
-export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate(
- 'xpack.observability.expView.reportType.noDataType',
- { defaultMessage: 'No data type selected.' }
-);
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
-
-const Button = styled(EuiButton)`
- will-change: transform;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
deleted file mode 100644
index a2a3e34c21834..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
+++ /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 React from 'react';
-import { EuiSuperSelect } from '@elastic/eui';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { SeriesConfig } from '../types';
-
-interface Props {
- seriesId: string;
- defaultValue?: string;
- options: SeriesConfig['metricOptions'];
-}
-
-export function ReportMetricOptions({ seriesId, options: opts }: Props) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const onChange = (value: string) => {
- setSeries(seriesId, {
- ...series,
- selectedMetricField: value,
- });
- };
-
- const options = opts ?? [];
-
- return (
- ({
- value: fd || id,
- inputDisplay: label,
- }))}
- valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id}
- onChange={(value) => onChange(value)}
- />
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
deleted file mode 100644
index 684cf3a210a51..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ /dev/null
@@ -1,303 +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, { RefObject, useEffect, useState } from 'react';
-import { isEmpty } from 'lodash';
-import { i18n } from '@kbn/i18n';
-import {
- EuiBasicTable,
- EuiButton,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiSwitch,
-} from '@elastic/eui';
-import { rgba } from 'polished';
-import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types';
-import { DataTypesCol } from './columns/data_types_col';
-import { ReportTypesCol } from './columns/report_types_col';
-import { ReportDefinitionCol } from './columns/report_definition_col';
-import { ReportFilters } from './columns/report_filters';
-import { ReportBreakdowns } from './columns/report_breakdowns';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { getDefaultConfigs } from '../configurations/default_configs';
-import { SeriesEditor } from '../series_editor/series_editor';
-import { SeriesActions } from '../series_editor/columns/series_actions';
-import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
-import { LastUpdated } from './last_updated';
-import {
- CORE_WEB_VITALS_LABEL,
- DEVICE_DISTRIBUTION_LABEL,
- KPI_OVER_TIME_LABEL,
- PERF_DIST_LABEL,
-} from '../configurations/constants/labels';
-
-export interface ReportTypeItem {
- id: string;
- reportType: ReportViewType;
- label: string;
-}
-
-export const ReportTypes: Record = {
- synthetics: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- ],
- ux: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
- ],
- mobile: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
- ],
- apm: [],
- infra_logs: [],
- infra_metrics: [],
-};
-
-interface BuilderItem {
- id: string;
- series: SeriesUrl;
- seriesConfig?: SeriesConfig;
-}
-
-export function SeriesBuilder({
- seriesBuilderRef,
- lastUpdated,
- multiSeries,
-}: {
- seriesBuilderRef: RefObject;
- lastUpdated?: number;
- multiSeries?: boolean;
-}) {
- const [editorItems, setEditorItems] = useState([]);
- const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
-
- const { loading, indexPatterns } = useAppIndexPatternContext();
-
- useEffect(() => {
- const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
- if (indexPatterns?.[dataType]) {
- return getDefaultConfigs({
- dataType,
- indexPattern: indexPatterns[dataType],
- reportType: reportType!,
- });
- }
- };
-
- const seriesToEdit: BuilderItem[] =
- allSeriesIds
- .filter((sId) => {
- return allSeries?.[sId]?.isNew;
- })
- .map((sId) => {
- const series = getSeries(sId);
- const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
-
- return { id: sId, series, seriesConfig };
- }) ?? [];
- const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }];
- setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries);
- }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]);
-
- const columns = [
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
- defaultMessage: 'Data Type',
- }),
- field: 'id',
- width: '15%',
- render: (seriesId: string) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
- defaultMessage: 'Report',
- }),
- width: '15%',
- field: 'id',
- render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', {
- defaultMessage: 'Definition',
- }),
- width: '30%',
- field: 'id',
- render: (
- seriesId: string,
- { series: { dataType, reportType }, seriesConfig }: BuilderItem
- ) => {
- if (dataType && seriesConfig) {
- return loading ? (
- LOADING_VIEW
- ) : reportType ? (
-
- ) : (
- SELECT_REPORT_TYPE
- );
- }
-
- return null;
- },
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', {
- defaultMessage: 'Filters',
- }),
- width: '20%',
- field: 'id',
- render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
- reportType && seriesConfig ? (
-
- ) : null,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', {
- defaultMessage: 'Breakdowns',
- }),
- width: '20%',
- field: 'id',
- render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
- reportType && seriesConfig ? (
-
- ) : null,
- },
- ...(multiSeries
- ? [
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (seriesId: string, item: BuilderItem) => (
-
- ),
- },
- ]
- : []),
- ];
-
- const applySeries = () => {
- editorItems.forEach(({ series, id: seriesId }) => {
- const { reportType, reportDefinitions, isNew, ...restSeries } = series;
-
- if (reportType && !isEmpty(reportDefinitions)) {
- const reportDefId = Object.values(reportDefinitions ?? {})[0];
- const newSeriesId = `${reportDefId}-${reportType}`;
-
- const newSeriesN: SeriesUrl = {
- ...restSeries,
- reportType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(seriesId);
- }
- });
- };
-
- const addSeries = () => {
- const prevSeries = allSeries?.[allSeriesIds?.[0]];
- setSeries(
- `${NEW_SERIES_KEY}-${editorItems.length + 1}`,
- prevSeries
- ? ({ isNew: true, time: prevSeries.time } as SeriesUrl)
- : ({ isNew: true } as SeriesUrl)
- );
- };
-
- return (
-
- {multiSeries && (
-
-
-
-
-
- {}}
- compressed
- />
-
-
- applySeries()} isDisabled={true} size="s">
- {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
- defaultMessage: 'Apply changes',
- })}
-
-
-
- addSeries()} size="s">
- {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
- defaultMessage: 'Add Series',
- })}
-
-
-
- )}
-
- {multiSeries && }
- {editorItems.length > 0 && (
-
- )}
-
-
-
- );
-}
-
-const Wrapper = euiStyled.div`
- max-height: 50vh;
- overflow-y: scroll;
- overflow-x: clip;
- &::-webkit-scrollbar {
- height: ${({ theme }) => theme.eui.euiScrollBar};
- width: ${({ theme }) => theme.eui.euiScrollBar};
- }
- &::-webkit-scrollbar-thumb {
- background-clip: content-box;
- background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
- border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
- }
- &::-webkit-scrollbar-corner,
- &::-webkit-scrollbar-track {
- background-color: transparent;
- }
-`;
-
-export const LOADING_VIEW = i18n.translate(
- 'xpack.observability.expView.seriesBuilder.loadingView',
- {
- defaultMessage: 'Loading view ...',
- }
-);
-
-export const SELECT_REPORT_TYPE = i18n.translate(
- 'xpack.observability.expView.seriesBuilder.selectReportType',
- {
- defaultMessage: 'No report type selected',
- }
-);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
deleted file mode 100644
index 207a53e13f1ad..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
+++ /dev/null
@@ -1,30 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { Breakdowns } from './columns/breakdowns';
-import { SeriesConfig } from '../types';
-import { ChartOptions } from './columns/chart_options';
-
-interface Props {
- seriesConfig: SeriesConfig;
- seriesId: string;
- breakdownFields: string[];
-}
-export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) {
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
index 84568e1c5068a..21b766227a562 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { Breakdowns } from './breakdowns';
-import { mockIndexPattern, render } from '../../rtl_helpers';
+import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
import { getDefaultConfigs } from '../../configurations/default_configs';
import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
@@ -20,13 +20,7 @@ describe('Breakdowns', function () {
});
it('should render properly', async function () {
- render(
-
- );
+ render( );
screen.getAllByText('Browser family');
});
@@ -36,9 +30,9 @@ describe('Breakdowns', function () {
const { setSeries } = render(
,
{ initSeries }
);
@@ -49,10 +43,14 @@ describe('Breakdowns', function () {
fireEvent.click(screen.getByText('Browser family'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
breakdown: 'user_agent.name',
dataType: 'ux',
- reportType: 'data-distribution',
+ name: 'performance-distribution',
+ reportDefinitions: {
+ 'service.name': ['elastic-co'],
+ },
+ selectedMetricField: 'transaction.duration.us',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
index 2237935d466ad..6003ddbf0290f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
@@ -10,18 +10,16 @@ import { EuiSuperSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
interface Props {
- seriesId: string;
- breakdowns: string[];
- seriesConfig: SeriesConfig;
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
}
-export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
- const { setSeries, getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
+ const { setSeries } = useSeriesStorage();
const selectedBreakdown = series.breakdown;
const NO_BREAKDOWN = 'no_breakdown';
@@ -40,9 +38,13 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
}
};
+ if (!seriesConfig) {
+ return null;
+ }
+
const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN;
- const items = breakdowns.map((breakdown) => ({
+ const items = seriesConfig.breakdownFields.map((breakdown) => ({
id: breakdown,
label: seriesConfig.labels[breakdown],
}));
@@ -50,14 +52,12 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
if (!hasUseBreakdownColumn) {
items.push({
id: NO_BREAKDOWN,
- label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', {
- defaultMessage: 'No breakdown',
- }),
+ label: NO_BREAK_DOWN_LABEL,
});
}
const options = items.map(({ id, label }) => ({
- inputDisplay: id === NO_BREAKDOWN ? label : {label} ,
+ inputDisplay: label,
value: id,
dropdownDisplay: label,
}));
@@ -66,15 +66,18 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN);
return (
-
- onOptionChange(value)}
- data-test-subj={'seriesBreakdown'}
- />
-
+ onOptionChange(value)}
+ data-test-subj={'seriesBreakdown'}
+ />
);
}
+
+export const NO_BREAK_DOWN_LABEL = i18n.translate(
+ 'xpack.observability.exp.breakDownFilter.noBreakdown',
+ {
+ defaultMessage: 'No breakdown',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
deleted file mode 100644
index f2a6377fd9b71..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
+++ /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 React from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { SeriesConfig } from '../../types';
-import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select';
-import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types';
-
-interface Props {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}
-
-export function ChartOptions({ seriesConfig, seriesId }: Props) {
- return (
-
-
-
-
- {seriesConfig.hasOperationType && (
-
-
-
- )}
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
new file mode 100644
index 0000000000000..6f88de5cc2afc
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiPopover, EuiToolTip, EuiButtonEmpty, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
+import { SeriesUrl, useFetcher } from '../../../../../index';
+import { SeriesConfig } from '../../types';
+import { SeriesChartTypesSelect } from './chart_types';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}
+
+export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) {
+ const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType;
+
+ const {
+ services: { lens },
+ } = useKibana();
+
+ const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]);
+
+ const icon = (data ?? []).find(({ id }) => id === seriesType)?.icon;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ return (
+ setIsPopoverOpen(false)}
+ button={
+
+ setIsPopoverOpen((prevState) => !prevState)}
+ flush="both"
+ >
+ {icon && (
+ id === seriesType)?.icon!} size="l" />
+ )}
+
+
+ }
+ >
+
+
+ );
+}
+
+const EDIT_CHART_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.editChartSeriesLabel',
+ {
+ defaultMessage: 'Edit chart type for series',
+ }
+);
+
+const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', {
+ defaultMessage: 'Chart type',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
similarity index 85%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
index c054853d9c877..8f196b8a05dda 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
@@ -7,12 +7,12 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { render } from '../../rtl_helpers';
+import { mockUxSeries, render } from '../../rtl_helpers';
import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types';
describe.skip('SeriesChartTypesSelect', function () {
it('should render properly', async function () {
- render( );
+ render( );
await waitFor(() => {
screen.getByText(/chart type/i);
@@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () {
it('should call set series on change', async function () {
const { setSeries } = render(
-
+
);
await waitFor(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
similarity index 77%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
index 50c2f91e6067d..27d846502dbe6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
@@ -6,11 +6,11 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
-import { useFetcher } from '../../../../..';
+import { SeriesUrl, useFetcher } from '../../../../..';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { SeriesType } from '../../../../../../../lens/public';
@@ -20,16 +20,14 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.
export function SeriesChartTypesSelect({
seriesId,
- seriesTypes,
+ series,
defaultChartType,
}: {
- seriesId: string;
- seriesTypes?: SeriesType[];
+ seriesId: number;
+ series: SeriesUrl;
defaultChartType: SeriesType;
}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries } = useSeriesStorage();
const seriesType = series?.seriesType ?? defaultChartType;
@@ -42,17 +40,15 @@ export function SeriesChartTypesSelect({
onChange={onChange}
value={seriesType}
excludeChartTypes={['bar_percentage_stacked']}
- includeChartTypes={
- seriesTypes || [
- 'bar',
- 'bar_horizontal',
- 'line',
- 'area',
- 'bar_stacked',
- 'area_stacked',
- 'bar_horizontal_percentage_stacked',
- ]
- }
+ includeChartTypes={[
+ 'bar',
+ 'bar_horizontal',
+ 'line',
+ 'area',
+ 'bar_stacked',
+ 'area_stacked',
+ 'bar_horizontal_percentage_stacked',
+ ]}
label={CHART_TYPE_LABEL}
/>
);
@@ -105,14 +101,14 @@ export function XYChartTypesSelect({
});
return (
-
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
new file mode 100644
index 0000000000000..fc96ad0741ec5
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, screen } from '@testing-library/react';
+import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
+import { DataTypesLabels, DataTypesSelect } from './data_type_select';
+import { DataTypes } from '../../configurations/constants';
+
+describe('DataTypeSelect', function () {
+ const seriesId = 0;
+
+ mockAppIndexPattern();
+
+ it('should render properly', function () {
+ render( );
+ });
+
+ it('should set series on change', async function () {
+ const seriesWithoutDataType = {
+ ...mockUxSeries,
+ dataType: undefined,
+ };
+ const { setSeries } = render(
+
+ );
+
+ fireEvent.click(await screen.findByText('Select data type'));
+ fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS]));
+
+ expect(setSeries).toHaveBeenCalledTimes(1);
+ expect(setSeries).toHaveBeenCalledWith(seriesId, {
+ dataType: 'synthetics',
+ name: 'synthetics-series-1',
+ time: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
new file mode 100644
index 0000000000000..71fd147e8e264
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiButton,
+ EuiPopover,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiBadge,
+ EuiToolTip,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { AppDataType, SeriesUrl } from '../../types';
+import { DataTypes, ReportTypes } from '../../configurations/constants';
+
+interface Props {
+ seriesId: number;
+ series: Omit & {
+ dataType?: SeriesUrl['dataType'];
+ };
+}
+
+export const DataTypesLabels = {
+ [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', {
+ defaultMessage: 'User experience (RUM)',
+ }),
+
+ [DataTypes.SYNTHETICS]: i18n.translate(
+ 'xpack.observability.overview.exploratoryView.syntheticsLabel',
+ {
+ defaultMessage: 'Synthetics monitoring',
+ }
+ ),
+
+ [DataTypes.MOBILE]: i18n.translate(
+ 'xpack.observability.overview.exploratoryView.mobileExperienceLabel',
+ {
+ defaultMessage: 'Mobile experience',
+ }
+ ),
+};
+
+export const dataTypes: Array<{ id: AppDataType; label: string }> = [
+ {
+ id: DataTypes.SYNTHETICS,
+ label: DataTypesLabels[DataTypes.SYNTHETICS],
+ },
+ {
+ id: DataTypes.UX,
+ label: DataTypesLabels[DataTypes.UX],
+ },
+ {
+ id: DataTypes.MOBILE,
+ label: DataTypesLabels[DataTypes.MOBILE],
+ },
+];
+
+const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE';
+
+export function DataTypesSelect({ seriesId, series }: Props) {
+ const { setSeries, reportType } = useSeriesStorage();
+ const [showOptions, setShowOptions] = useState(false);
+
+ const onDataTypeChange = (dataType: AppDataType) => {
+ if (String(dataType) !== SELECT_DATA_TYPE) {
+ setSeries(seriesId, {
+ dataType,
+ time: series.time,
+ name: `${dataType}-series-${seriesId + 1}`,
+ });
+ }
+ };
+
+ const options = dataTypes
+ .filter(({ id }) => {
+ if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
+ return id === DataTypes.MOBILE;
+ }
+ if (reportType === ReportTypes.CORE_WEB_VITAL) {
+ return id === DataTypes.UX;
+ }
+ return true;
+ })
+ .map(({ id, label }) => ({
+ value: id,
+ inputDisplay: label,
+ }));
+
+ return (
+ <>
+ {!series.dataType && (
+ setShowOptions((prevState) => !prevState)}
+ fill
+ size="s"
+ >
+ {SELECT_DATA_TYPE_LABEL}
+
+ }
+ isOpen={showOptions}
+ closePopover={() => setShowOptions((prevState) => !prevState)}
+ >
+
+ {options.map((option) => (
+ onDataTypeChange(option.value)}
+ label={option.inputDisplay}
+ />
+ ))}
+
+
+ )}
+ {series.dataType && (
+
+ {DataTypesLabels[series.dataType as DataTypes]}
+
+ )}
+ >
+ );
+}
+
+const SELECT_DATA_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.selectDataType',
+ {
+ defaultMessage: 'Select data type',
+ }
+);
+
+const SELECT_DATA_TYPE_TOOLTIP = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.selectDataTypeTooltip',
+ {
+ defaultMessage: 'Data type cannot be edited.',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
index 41e83f407af2b..b01010e4b81f9 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
@@ -6,24 +6,80 @@
*/
import React from 'react';
-import { SeriesDatePicker } from '../../series_date_picker';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { DateRangePicker } from '../../series_date_picker/date_range_picker';
+import { DateRangePicker } from '../../components/date_range_picker';
+import { SeriesDatePicker } from '../../components/series_date_picker';
+import { AppDataType, SeriesUrl } from '../../types';
+import { ReportTypes } from '../../configurations/constants';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data';
+import { MobileAddData } from '../../../add_data_buttons/mobile_add_data';
+import { UXAddData } from '../../../add_data_buttons/ux_add_data';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
}
-export function DatePickerCol({ seriesId }: Props) {
- const { firstSeriesId, getSeries } = useSeriesStorage();
- const { reportType } = getSeries(firstSeriesId);
+
+const AddDataComponents: Record = {
+ mobile: MobileAddData,
+ ux: UXAddData,
+ synthetics: SyntheticsAddData,
+ apm: null,
+ infra_logs: null,
+ infra_metrics: null,
+};
+
+export function DatePickerCol({ seriesId, series }: Props) {
+ const { reportType } = useSeriesStorage();
+
+ const { hasAppData } = useAppIndexPatternContext();
+
+ if (!series.dataType) {
+ return null;
+ }
+
+ const AddDataButton = AddDataComponents[series.dataType];
+ if (hasAppData[series.dataType] === false && AddDataButton !== null) {
+ return (
+
+
+
+ {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', {
+ defaultMessage: 'No {dataType} data available.',
+ values: {
+ dataType: series.dataType,
+ },
+ })}
+
+
+
+
+
+
+ );
+ }
return (
-
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
-
+
+ {seriesId === 0 || reportType !== ReportTypes.KPI ? (
+
) : (
-
+
)}
-
+
);
}
+
+const Wrapper = styled.div`
+ width: 100%;
+ .euiSuperDatePicker__flexWrapper {
+ width: 100%;
+ > .euiFlexItem {
+ margin-right: 0;
+ }
+ }
+`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
index 90a039f6b44d0..a88e2eadd10c9 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
@@ -8,20 +8,24 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { FilterExpanded } from './filter_expanded';
-import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
+import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('FilterExpanded', function () {
- it('should render properly', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
+
+ const mockSeries = { ...mockUxSeries, filters };
+
+ it('render', async () => {
+ const initSeries = { filters };
mockAppIndexPattern();
render(
,
{ initSeries }
@@ -33,15 +37,14 @@ describe('FilterExpanded', function () {
});
it('should call go back on click', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
- const goBack = jest.fn();
+ const initSeries = { filters };
render(
,
{ initSeries }
@@ -49,28 +52,23 @@ describe('FilterExpanded', function () {
await waitFor(() => {
fireEvent.click(screen.getByText('Browser Family'));
-
- expect(goBack).toHaveBeenCalledTimes(1);
- expect(goBack).toHaveBeenCalledWith();
});
});
- it('should call useValuesList on load', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ it('calls useValuesList on load', async () => {
+ const initSeries = { filters };
const { spy } = mockUseValuesList([
{ label: 'Chrome', count: 10 },
{ label: 'Firefox', count: 5 },
]);
- const goBack = jest.fn();
-
render(
,
{ initSeries }
@@ -87,8 +85,8 @@ describe('FilterExpanded', function () {
});
});
- it('should filter display values', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ it('filters display values', async () => {
+ const initSeries = { filters };
mockUseValuesList([
{ label: 'Chrome', count: 10 },
@@ -97,18 +95,20 @@ describe('FilterExpanded', function () {
render(
,
{ initSeries }
);
- expect(screen.getByText('Firefox')).toBeTruthy();
-
await waitFor(() => {
+ fireEvent.click(screen.getByText('Browser Family'));
+
+ expect(screen.queryByText('Firefox')).toBeTruthy();
+
fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } });
expect(screen.queryByText('Firefox')).toBeFalsy();
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
index 84c326f62f89d..693b79c6dc831 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
@@ -6,7 +6,14 @@
*/
import React, { useState, Fragment } from 'react';
-import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui';
+import {
+ EuiFieldSearch,
+ EuiSpacer,
+ EuiFilterGroup,
+ EuiText,
+ EuiPopover,
+ EuiFilterButton,
+} from '@elastic/eui';
import styled from 'styled-components';
import { rgba } from 'polished';
import { i18n } from '@kbn/i18n';
@@ -14,8 +21,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
import { map } from 'lodash';
import { ExistsFilter, isExistsFilter } from '@kbn/es-query';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { SeriesConfig, UrlFilter } from '../../types';
+import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
import { FilterValueButton } from './filter_value_btn';
import { useValuesList } from '../../../../../hooks/use_values_list';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
@@ -23,31 +29,33 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc
import { PersistableFilter } from '../../../../../../../lens/common';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
label: string;
field: string;
isNegated?: boolean;
- goBack: () => void;
nestedField?: string;
filters: SeriesConfig['baseFilters'];
}
+export interface NestedFilterOpen {
+ value: string;
+ negate: boolean;
+}
+
export function FilterExpanded({
seriesId,
+ series,
field,
label,
- goBack,
nestedField,
isNegated,
filters: defaultFilters,
}: Props) {
const [value, setValue] = useState('');
- const [isOpen, setIsOpen] = useState({ value: '', negate: false });
-
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false });
const queryFilters: ESFilter[] = [];
@@ -80,62 +88,71 @@ export function FilterExpanded({
);
return (
-
- goBack()}>
- {label}
-
- {
- setValue(evt.target.value);
- }}
- placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
- defaultMessage: 'Search for {label}',
- values: { label },
- })}
- />
-
-
- {displayValues.length === 0 && !loading && (
-
- {i18n.translate('xpack.observability.filters.expanded.noFilter', {
- defaultMessage: 'No filters found.',
- })}
-
- )}
- {displayValues.map((opt) => (
-
-
- {isNegated !== false && (
+ setIsOpen((prevState) => !prevState)} iconType="arrowDown">
+ {label}
+
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ >
+
+ {
+ setValue(evt.target.value);
+ }}
+ placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
+ defaultMessage: 'Search for {label}',
+ values: { label },
+ })}
+ />
+
+
+ {displayValues.length === 0 && !loading && (
+
+ {i18n.translate('xpack.observability.filters.expanded.noFilter', {
+ defaultMessage: 'No filters found.',
+ })}
+
+ )}
+ {displayValues.map((opt) => (
+
+
+ {isNegated !== false && (
+
+ )}
- )}
-
-
-
-
- ))}
-
-
+
+
+
+ ))}
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
index a9609abc70d69..764a27fd663f5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { FilterValueButton } from './filter_value_btn';
-import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
+import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
import {
USER_AGENT_NAME,
USER_AGENT_VERSION,
@@ -19,84 +19,98 @@ describe('FilterValueButton', function () {
render(
);
- screen.getByText('Chrome');
+ await waitFor(() => {
+ expect(screen.getByText('Chrome')).toBeInTheDocument();
+ });
});
- it('should render display negate state', async function () {
- render(
-
- );
+ describe('when negate is true', () => {
+ it('displays negate stats', async () => {
+ render(
+
+ );
- await waitFor(() => {
- screen.getByText('Not Chrome');
- screen.getByTitle('Not Chrome');
- const btn = screen.getByRole('button');
- expect(btn.classList).toContain('euiButtonEmpty--danger');
+ await waitFor(() => {
+ expect(screen.getByText('Not Chrome')).toBeInTheDocument();
+ expect(screen.getByTitle('Not Chrome')).toBeInTheDocument();
+ const btn = screen.getByRole('button');
+ expect(btn.classList).toContain('euiButtonEmpty--danger');
+ });
});
- });
- it('should call set filter on click', async function () {
- const { setFilter, removeFilter } = mockUseSeriesFilter();
+ it('calls setFilter on click', async () => {
+ const { setFilter, removeFilter } = mockUseSeriesFilter();
- render(
-
- );
+ render(
+
+ );
- await waitFor(() => {
fireEvent.click(screen.getByText('Not Chrome'));
- expect(removeFilter).toHaveBeenCalledTimes(0);
- expect(setFilter).toHaveBeenCalledTimes(1);
- expect(setFilter).toHaveBeenCalledWith({
- field: 'user_agent.name',
- negate: true,
- value: 'Chrome',
+
+ await waitFor(() => {
+ expect(removeFilter).toHaveBeenCalledTimes(0);
+ expect(setFilter).toHaveBeenCalledTimes(1);
+
+ expect(setFilter).toHaveBeenCalledWith({
+ field: 'user_agent.name',
+ negate: true,
+ value: 'Chrome',
+ });
});
});
});
- it('should remove filter on click if already selected', async function () {
- const { removeFilter } = mockUseSeriesFilter();
+ describe('when selected', () => {
+ it('removes the filter on click', async () => {
+ const { removeFilter } = mockUseSeriesFilter();
+
+ render(
+
+ );
- render(
-
- );
- await waitFor(() => {
fireEvent.click(screen.getByText('Chrome'));
- expect(removeFilter).toHaveBeenCalledWith({
- field: 'user_agent.name',
- negate: false,
- value: 'Chrome',
+
+ await waitFor(() => {
+ expect(removeFilter).toHaveBeenCalledWith({
+ field: 'user_agent.name',
+ negate: false,
+ value: 'Chrome',
+ });
});
});
});
@@ -107,12 +121,13 @@ describe('FilterValueButton', function () {
render(
);
@@ -134,13 +149,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -167,13 +183,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -203,13 +220,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -229,13 +247,14 @@ describe('FilterValueButton', function () {
render(
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
index bf4ca6eb83d94..11f29c0233ef5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
@@ -5,13 +5,15 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
+
import React, { useMemo } from 'react';
import { EuiFilterButton, hexToRgb } from '@elastic/eui';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
import { useSeriesFilters } from '../../hooks/use_series_filters';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import FieldValueSuggestions from '../../../field_value_suggestions';
+import { SeriesUrl } from '../../types';
+import { NestedFilterOpen } from './filter_expanded';
interface Props {
value: string;
@@ -19,12 +21,13 @@ interface Props {
allSelectedValues?: string[];
negate: boolean;
nestedField?: string;
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
isNestedOpen: {
value: string;
negate: boolean;
};
- setIsNestedOpen: (val: { value: string; negate: boolean }) => void;
+ setIsNestedOpen: (val: NestedFilterOpen) => void;
}
export function FilterValueButton({
@@ -34,16 +37,13 @@ export function FilterValueButton({
field,
negate,
seriesId,
+ series,
nestedField,
allSelectedValues,
}: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
const { indexPatterns } = useAppIndexPatternContext(series.dataType);
- const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
+ const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series });
const hasActiveFilters = (allSelectedValues ?? []).includes(value);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
new file mode 100644
index 0000000000000..4e1c385921908
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { isEmpty } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { EuiBadge } from '@elastic/eui';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { SeriesConfig, SeriesUrl } from '../../types';
+
+interface Props {
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+
+export function IncompleteBadge({ seriesConfig, series }: Props) {
+ const { loading } = useAppIndexPatternContext();
+
+ if (!seriesConfig) {
+ return null;
+ }
+ const { dataType, reportDefinitions, selectedMetricField } = series;
+ const { definitionFields, labels } = seriesConfig;
+ const isIncomplete =
+ (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading;
+
+ const incompleteDefinition = isEmpty(reportDefinitions)
+ ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', {
+ defaultMessage: 'Missing {reportDefinition}',
+ values: { reportDefinition: labels?.[definitionFields[0]] },
+ })
+ : '';
+
+ let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition;
+
+ if (!dataType) {
+ incompleteMessage = MISSING_DATA_TYPE_LABEL;
+ }
+
+ if (!isIncomplete) {
+ return null;
+ }
+
+ return {incompleteMessage} ;
+}
+
+const MISSING_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.missingReportMetric',
+ {
+ defaultMessage: 'Missing report metric',
+ }
+);
+
+const MISSING_DATA_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.missingDataType',
+ {
+ defaultMessage: 'Missing data type',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
similarity index 69%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
index 516f04e3812ba..ced4d3af057ff 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
@@ -7,62 +7,66 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
-import { render } from '../../rtl_helpers';
+import { mockUxSeries, render } from '../../rtl_helpers';
import { OperationTypeSelect } from './operation_type_select';
describe('OperationTypeSelect', function () {
it('should render properly', function () {
- render( );
+ render( );
screen.getByText('Select an option: , is selected');
});
it('should display selected value', function () {
const initSeries = {
- data: {
- 'performance-distribution': {
+ data: [
+ {
+ name: 'performance-distribution',
dataType: 'ux' as const,
- reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
- render( , { initSeries });
+ render( , {
+ initSeries,
+ });
screen.getByText('Median');
});
it('should call set series on change', function () {
const initSeries = {
- data: {
- 'series-id': {
+ data: [
+ {
+ name: 'performance-distribution',
dataType: 'ux' as const,
- reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
- const { setSeries } = render( , { initSeries });
+ const { setSeries } = render( , {
+ initSeries,
+ });
fireEvent.click(screen.getByTestId('operationTypeSelect'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
operationType: 'median',
dataType: 'ux',
- reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
+ name: 'performance-distribution',
});
fireEvent.click(screen.getByText('95th Percentile'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
operationType: '95th',
dataType: 'ux',
- reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
+ name: 'performance-distribution',
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
similarity index 91%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
index fce1383f30f34..4c10c9311704d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
@@ -11,17 +11,18 @@ import { EuiSuperSelect } from '@elastic/eui';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { OperationType } from '../../../../../../../lens/public';
+import { SeriesUrl } from '../../types';
export function OperationTypeSelect({
seriesId,
+ series,
defaultOperationType,
}: {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
defaultOperationType?: OperationType;
}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries } = useSeriesStorage();
const operationType = series?.operationType;
@@ -83,11 +84,7 @@ export function OperationTypeSelect({
return (
{
removeSeries(seriesId);
};
+
+ const isDisabled = seriesId === 0 && allSeries.length > 1;
+
return (
-
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
similarity index 65%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
index 3d156e0ee9c2b..544a294e021e2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
@@ -12,14 +12,14 @@ import {
mockAppIndexPattern,
mockIndexPattern,
mockUseValuesList,
+ mockUxSeries,
render,
} from '../../rtl_helpers';
import { ReportDefinitionCol } from './report_definition_col';
-import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Series Builder ReportDefinitionCol', function () {
mockAppIndexPattern();
- const seriesId = 'test-series-id';
+ const seriesId = 0;
const seriesConfig = getDefaultConfigs({
reportType: 'data-distribution',
@@ -27,36 +27,24 @@ describe('Series Builder ReportDefinitionCol', function () {
dataType: 'ux',
});
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'ux' as const,
- reportType: 'data-distribution' as const,
- time: { from: 'now-30d', to: 'now' },
- reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
- },
- },
- };
-
mockUseValuesList([{ label: 'elastic-co', count: 10 }]);
- it('should render properly', async function () {
- render( , {
- initSeries,
- });
+ it('renders', async () => {
+ render(
+
+ );
await waitFor(() => {
- screen.getByText('Web Application');
- screen.getByText('Environment');
- screen.getByText('Select an option: Page load time, is selected');
- screen.getByText('Page load time');
+ expect(screen.getByText('Web Application')).toBeInTheDocument();
+ expect(screen.getByText('Environment')).toBeInTheDocument();
+ expect(screen.getByText('Search Environment')).toBeInTheDocument();
});
});
it('should render selected report definitions', async function () {
- render( , {
- initSeries,
- });
+ render(
+
+ );
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
@@ -65,8 +53,7 @@ describe('Series Builder ReportDefinitionCol', function () {
it('should be able to remove selected definition', async function () {
const { setSeries } = render(
- ,
- { initSeries }
+
);
expect(
@@ -80,11 +67,14 @@ describe('Series Builder ReportDefinitionCol', function () {
fireEvent.click(removeBtn);
expect(setSeries).toHaveBeenCalledTimes(1);
+
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'ux',
+ name: 'performance-distribution',
+ breakdown: 'user_agent.name',
reportDefinitions: {},
- reportType: 'data-distribution',
- time: { from: 'now-30d', to: 'now' },
+ selectedMetricField: 'transaction.duration.us',
+ time: { from: 'now-15m', to: 'now' },
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
new file mode 100644
index 0000000000000..fbd7c34303d94
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { ReportDefinitionField } from './report_definition_field';
+
+export function ReportDefinitionCol({
+ seriesId,
+ series,
+ seriesConfig,
+}: {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}) {
+ const { setSeries } = useSeriesStorage();
+
+ const { reportDefinitions: selectedReportDefinitions = {} } = series;
+
+ const { definitionFields } = seriesConfig;
+
+ const onChange = (field: string, value?: string[]) => {
+ if (!value?.[0]) {
+ delete selectedReportDefinitions[field];
+ setSeries(seriesId, {
+ ...series,
+ reportDefinitions: { ...selectedReportDefinitions },
+ });
+ } else {
+ setSeries(seriesId, {
+ ...series,
+ reportDefinitions: { ...selectedReportDefinitions, [field]: value },
+ });
+ }
+ };
+
+ return (
+
+ {definitionFields.map((field) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
similarity index 69%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
index 8a83b5c2a8cb0..3651b4b7f075b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
@@ -6,30 +6,25 @@
*/
import React, { useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { ExistsFilter } from '@kbn/es-query';
import FieldValueSuggestions from '../../../field_value_suggestions';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch';
import { PersistableFilter } from '../../../../../../../lens/common';
import { buildPhrasesFilter } from '../../configurations/utils';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
field: string;
seriesConfig: SeriesConfig;
onChange: (field: string, value?: string[]) => void;
}
-export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
+export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) {
const { indexPattern } = useAppIndexPatternContext(series.dataType);
const { reportDefinitions: selectedReportDefinitions = {} } = series;
@@ -64,23 +59,26 @@ export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]);
+ if (!indexPattern) {
+ return null;
+ }
+
return (
-
-
- {indexPattern && (
- onChange(field, val)}
- filters={queryFilters}
- time={series.time}
- fullWidth={true}
- allowAllValuesSelection={true}
- />
- )}
-
-
+ onChange(field, val)}
+ filters={queryFilters}
+ time={series.time}
+ fullWidth={true}
+ asCombobox={true}
+ allowExclusions={false}
+ allowAllValuesSelection={true}
+ usePrependLabel={false}
+ compressed={false}
+ required={isEmpty(selectedReportDefinitions)}
+ />
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
new file mode 100644
index 0000000000000..31a8c7cb7bfae
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiSuperSelect } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { ReportViewType } from '../../types';
+import {
+ CORE_WEB_VITALS_LABEL,
+ DEVICE_DISTRIBUTION_LABEL,
+ KPI_OVER_TIME_LABEL,
+ PERF_DIST_LABEL,
+} from '../../configurations/constants/labels';
+
+const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE';
+
+export const reportTypesList: Array<{
+ reportType: ReportViewType | typeof SELECT_REPORT_TYPE;
+ label: string;
+}> = [
+ {
+ reportType: SELECT_REPORT_TYPE,
+ label: i18n.translate('xpack.observability.expView.reportType.selectLabel', {
+ defaultMessage: 'Select report type',
+ }),
+ },
+ { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
+ { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
+];
+
+export function ReportTypesSelect() {
+ const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage();
+
+ const onReportTypeChange = (reportType: ReportViewType) => {
+ setReportType(reportType);
+ };
+
+ const options = reportTypesList
+ .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true))
+ .map(({ reportType, label }) => ({
+ value: reportType,
+ inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label} ,
+ dropdownDisplay: label,
+ }));
+
+ return (
+ onReportTypeChange(value as ReportViewType)}
+ style={{ minWidth: 200 }}
+ isInvalid={!selectedReportType && allSeries.length > 0}
+ disabled={allSeries.length > 0}
+ />
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
similarity index 59%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
index eb76772a66c7e..64291f84f7662 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
@@ -7,10 +7,10 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
-import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
+import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
import { SelectedFilters } from './selected_filters';
-import { getDefaultConfigs } from '../configurations/default_configs';
-import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
+import { getDefaultConfigs } from '../../configurations/default_configs';
+import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('SelectedFilters', function () {
mockAppIndexPattern();
@@ -22,11 +22,19 @@ describe('SelectedFilters', function () {
});
it('should render properly', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
+ const initSeries = { filters };
- render( , {
- initSeries,
- });
+ render(
+ ,
+ {
+ initSeries,
+ }
+ );
await waitFor(() => {
screen.getByText('Chrome');
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx
new file mode 100644
index 0000000000000..3327ecf1fc9b6
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Fragment } from 'react';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FilterLabel } from '../../components/filter_label';
+import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { useSeriesFilters } from '../../hooks/use_series_filters';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}
+export function SelectedFilters({ seriesId, series, seriesConfig }: Props) {
+ const { setSeries } = useSeriesStorage();
+
+ const { labels } = seriesConfig;
+
+ const filters: UrlFilter[] = series.filters ?? [];
+
+ const { removeFilter } = useSeriesFilters({ seriesId, series });
+
+ const { indexPattern } = useAppIndexPatternContext(series.dataType);
+
+ if (filters.length === 0 || !indexPattern) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {filters.map(({ field, values, notValues }) => (
+
+ {(values ?? []).length > 0 && (
+
+ {
+ values?.forEach((val) => {
+ removeFilter({ field, value: val, negate: false });
+ });
+ }}
+ negate={false}
+ indexPattern={indexPattern}
+ />
+
+ )}
+ {(notValues ?? []).length > 0 && (
+
+ {
+ values?.forEach((val) => {
+ removeFilter({ field, value: val, negate: false });
+ });
+ }}
+ indexPattern={indexPattern}
+ />
+
+ )}
+
+ ))}
+
+ {(series.filters ?? []).length > 0 && (
+
+ {
+ setSeries(seriesId, { ...series, filters: undefined });
+ }}
+ size="xs"
+ >
+ {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
+ defaultMessage: 'Clear filters',
+ })}
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
index 51ebe6c6bd9d5..37b5b1571f84d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
@@ -6,98 +6,113 @@
*/
import React from 'react';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { isEmpty } from 'lodash';
import { RemoveSeries } from './remove_series';
import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { SeriesUrl } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { useDiscoverLink } from '../../hooks/use_discover_link';
interface Props {
- seriesId: string;
- editorMode?: boolean;
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+ onEditClick?: () => void;
}
-export function SeriesActions({ seriesId, editorMode = false }: Props) {
- const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
- const series = getSeries(seriesId);
- const onEdit = () => {
- setSeries(seriesId, { ...series, isNew: true });
- };
+export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: Props) {
+ const { setSeries, allSeries } = useSeriesStorage();
+
+ const { href: discoverHref } = useDiscoverLink({ series, seriesConfig });
const copySeries = () => {
- let copySeriesId: string = `${seriesId}-copy`;
- if (allSeriesIds.includes(copySeriesId)) {
- copySeriesId = copySeriesId + allSeriesIds.length;
+ let copySeriesId: string = `${series.name}-copy`;
+ if (allSeries.find(({ name }) => name === copySeriesId)) {
+ copySeriesId = copySeriesId + allSeries.length;
}
- setSeries(copySeriesId, series);
+ setSeries(allSeries.length, { ...series, name: copySeriesId });
};
- const { reportType, reportDefinitions, isNew, ...restSeries } = series;
- const isSaveAble = reportType && !isEmpty(reportDefinitions);
-
- const saveSeries = () => {
- if (isSaveAble) {
- const reportDefId = Object.values(reportDefinitions ?? {})[0];
- let newSeriesId = `${reportDefId}-${reportType}`;
-
- if (allSeriesIds.includes(newSeriesId)) {
- newSeriesId = `${newSeriesId}-${allSeriesIds.length}`;
- }
- const newSeriesN: SeriesUrl = {
- ...restSeries,
- reportType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(seriesId);
+ const toggleSeries = () => {
+ if (series.hidden) {
+ setSeries(seriesId, { ...series, hidden: undefined });
+ } else {
+ setSeries(seriesId, { ...series, hidden: true });
}
};
return (
-
- {!editorMode && (
-
+
+
+
+
+
+
+
+
-
- )}
- {editorMode && (
-
+
+
+
+
+
-
- )}
- {editorMode && (
-
+
+
+
+
+
-
- )}
+
+
);
}
+
+const EDIT_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.edit', {
+ defaultMessage: 'Edit series',
+});
+
+const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide', {
+ defaultMessage: 'Hide series',
+});
+
+const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', {
+ defaultMessage: 'Copy series',
+});
+
+const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate(
+ 'xpack.observability.seriesEditor.sampleDocuments',
+ {
+ defaultMessage: 'View sample documents in new tab',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
index 02144c6929b38..5b576d9da0172 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
@@ -5,29 +5,17 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-import React, { useState, Fragment } from 'react';
-import {
- EuiButton,
- EuiPopover,
- EuiSpacer,
- EuiButtonEmpty,
- EuiFlexItem,
- EuiFlexGroup,
-} from '@elastic/eui';
+import React from 'react';
+import { EuiFilterGroup, EuiSpacer } from '@elastic/eui';
import { FilterExpanded } from './filter_expanded';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
import { FieldLabels } from '../../configurations/constants/constants';
-import { SelectedFilters } from '../selected_filters';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SelectedFilters } from './selected_filters';
interface Props {
- seriesId: string;
- filterFields: SeriesConfig['filterFields'];
- baseFilters: SeriesConfig['baseFilters'];
+ seriesId: number;
seriesConfig: SeriesConfig;
- isNew?: boolean;
- labels?: Record;
+ series: SeriesUrl;
}
export interface Field {
@@ -37,119 +25,38 @@ export interface Field {
isNegated?: boolean;
}
-export function SeriesFilter({
- seriesConfig,
- isNew,
- seriesId,
- filterFields = [],
- baseFilters,
- labels,
-}: Props) {
- const [isPopoverVisible, setIsPopoverVisible] = useState(false);
-
- const [selectedField, setSelectedField] = useState();
-
- const options: Field[] = filterFields.map((field) => {
+export function SeriesFilter({ series, seriesConfig, seriesId }: Props) {
+ const options: Field[] = seriesConfig.filterFields.map((field) => {
if (typeof field === 'string') {
- return { label: labels?.[field] ?? FieldLabels[field], field };
+ return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field };
}
return {
field: field.field,
nested: field.nested,
isNegated: field.isNegated,
- label: labels?.[field.field] ?? FieldLabels[field.field],
+ label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field],
};
});
- const { setSeries, getSeries } = useSeriesStorage();
- const urlSeries = getSeries(seriesId);
-
- const button = (
- {
- setIsPopoverVisible((prevState) => !prevState);
- }}
- size="s"
- >
- {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', {
- defaultMessage: 'Add filter',
- })}
-
- );
-
- const mainPanel = (
+ return (
<>
+
+ {options.map((opt) => (
+
+ ))}
+
- {options.map((opt) => (
-
- {
- setSelectedField(opt);
- }}
- >
- {opt.label}
-
-
-
- ))}
+
>
);
-
- const childPanel = selectedField ? (
- {
- setSelectedField(undefined);
- }}
- filters={baseFilters}
- />
- ) : null;
-
- const closePopover = () => {
- setIsPopoverVisible(false);
- setSelectedField(undefined);
- };
-
- return (
-
-
-
-
- {!selectedField ? mainPanel : childPanel}
-
-
- {(urlSeries.filters ?? []).length > 0 && (
-
- {
- setSeries(seriesId, { ...urlSeries, filters: undefined });
- }}
- size="s"
- >
- {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
- defaultMessage: 'Clear filters',
- })}
-
-
- )}
-
- );
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
new file mode 100644
index 0000000000000..4c2e57e780550
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { SeriesColorPicker } from '../../components/series_color_picker';
+import { SeriesChartTypes } from './chart_type_select';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+
+export function SeriesInfo({ seriesId, series, seriesConfig }: Props) {
+ if (!seriesConfig) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+
+ return null;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
new file mode 100644
index 0000000000000..ccad461209313
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { mockUxSeries, render } from '../../rtl_helpers';
+import { SeriesName } from './series_name';
+
+describe.skip('SeriesChartTypesSelect', function () {
+ it('should render properly', async function () {
+ render( );
+
+ expect(screen.getByText(mockUxSeries.name)).toBeInTheDocument();
+ });
+
+ it('should display input when editing name', async function () {
+ render( );
+
+ let input = screen.queryByLabelText(mockUxSeries.name);
+
+ // read only
+ expect(input).not.toBeInTheDocument();
+
+ const editButton = screen.getByRole('button');
+ // toggle editing
+ fireEvent.click(editButton);
+
+ await waitFor(() => {
+ input = screen.getByLabelText(mockUxSeries.name);
+
+ expect(input).toBeInTheDocument();
+ });
+
+ // toggle readonly
+ fireEvent.click(editButton);
+
+ await waitFor(() => {
+ input = screen.getByLabelText(mockUxSeries.name);
+
+ expect(input).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
new file mode 100644
index 0000000000000..cff30a2b35059
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, ChangeEvent, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFieldText,
+ EuiText,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiOutsideClickDetector,
+} from '@elastic/eui';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesUrl } from '../../types';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+}
+
+export const StyledText = styled(EuiText)`
+ &.euiText.euiText--constrainedWidth {
+ max-width: 200px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+`;
+
+export function SeriesName({ series, seriesId }: Props) {
+ const { setSeries } = useSeriesStorage();
+
+ const [value, setValue] = useState(series.name);
+ const [isEditingEnabled, setIsEditingEnabled] = useState(false);
+ const inputRef = useRef(null);
+ const buttonRef = useRef(null);
+
+ const onChange = (e: ChangeEvent) => {
+ setValue(e.target.value);
+ };
+
+ const onSave = () => {
+ if (value !== series.name) {
+ setSeries(seriesId, { ...series, name: value });
+ }
+ };
+
+ const onOutsideClick = (event: Event) => {
+ if (event.target !== buttonRef.current) {
+ setIsEditingEnabled(false);
+ }
+ };
+
+ useEffect(() => {
+ setValue(series.name);
+ }, [series.name]);
+
+ useEffect(() => {
+ if (isEditingEnabled && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isEditingEnabled, inputRef]);
+
+ return (
+
+ {isEditingEnabled ? (
+
+
+
+
+
+ ) : (
+
+ {value}
+
+ )}
+
+ setIsEditingEnabled(!isEditingEnabled)}
+ iconType="pencil"
+ aria-label={i18n.translate('xpack.observability.expView.seriesEditor.editName', {
+ defaultMessage: 'Edit name',
+ })}
+ color="text"
+ buttonRef={buttonRef}
+ />
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
new file mode 100644
index 0000000000000..9f4de1b6dd519
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiHorizontalRule } from '@elastic/eui';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { ReportDefinitionCol } from './columns/report_definition_col';
+import { OperationTypeSelect } from './columns/operation_type_select';
+import { parseCustomFieldName } from '../configurations/lens_attributes';
+import { SeriesFilter } from './columns/series_filter';
+import { DatePickerCol } from './columns/date_picker_col';
+import { Breakdowns } from './columns/breakdowns';
+
+function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
+ const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
+
+ return columnType;
+}
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+export function ExpandedSeriesRow(seriesProps: Props) {
+ const { seriesConfig, series, seriesId } = seriesProps;
+
+ if (!seriesConfig) {
+ return null;
+ }
+
+ const { selectedMetricField } = series ?? {};
+
+ const { hasOperationType, yAxisColumns } = seriesConfig;
+
+ const columnType = getColumnType(seriesConfig, selectedMetricField);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(hasOperationType || columnType === 'operation') && (
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+const BREAKDOWNS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.breakdowns', {
+ defaultMessage: 'Breakdowns',
+});
+
+const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', {
+ defaultMessage: 'Filters',
+});
+
+const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', {
+ defaultMessage: 'Operation',
+});
+
+const DATE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.date', {
+ defaultMessage: 'Date',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
new file mode 100644
index 0000000000000..496e7a10f9c44
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiToolTip,
+ EuiPopover,
+ EuiButton,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiBadge,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useSeriesStorage } from '../hooks/use_series_storage';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ defaultValue?: string;
+ seriesConfig?: SeriesConfig;
+}
+
+export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) {
+ const { setSeries } = useSeriesStorage();
+ const [showOptions, setShowOptions] = useState(false);
+ const metricOptions = seriesConfig?.metricOptions;
+
+ const { indexPatterns } = useAppIndexPatternContext();
+
+ const onChange = (value?: string) => {
+ setSeries(seriesId, {
+ ...series,
+ selectedMetricField: value,
+ });
+ };
+
+ if (!series.dataType) {
+ return null;
+ }
+
+ const indexPattern = indexPatterns?.[series.dataType];
+
+ const options = (metricOptions ?? []).map(({ label, field, id }) => {
+ let disabled = false;
+
+ if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) {
+ disabled = !Boolean(indexPattern?.getFieldByName(field));
+ }
+ return {
+ disabled,
+ value: field || id,
+ dropdownDisplay: disabled ? (
+ {field},
+ }}
+ />
+ }
+ >
+ {label}
+
+ ) : (
+ label
+ ),
+ inputDisplay: label,
+ };
+ });
+
+ return (
+ <>
+ {!series.selectedMetricField && (
+ setShowOptions((prevState) => !prevState)}
+ fill
+ size="s"
+ >
+ {SELECT_REPORT_METRIC_LABEL}
+
+ }
+ isOpen={showOptions}
+ closePopover={() => setShowOptions((prevState) => !prevState)}
+ >
+
+ {options.map((option) => (
+ onChange(option.value)}
+ label={option.dropdownDisplay}
+ isDisabled={option.disabled}
+ />
+ ))}
+
+
+ )}
+ {series.selectedMetricField && (
+ onChange(undefined)}
+ iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL}
+ >
+ {
+ seriesConfig?.metricOptions?.find((option) => option.id === series.selectedMetricField)
+ ?.label
+ }
+
+ )}
+ >
+ );
+}
+
+const SELECT_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.selectReportMetric',
+ {
+ defaultMessage: 'Select report metric',
+ }
+);
+
+const REMOVE_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.removeReportMetric',
+ {
+ defaultMessage: 'Remove report metric',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
deleted file mode 100644
index 5d2ce6ba84951..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { FilterLabel } from '../components/filter_label';
-import { SeriesConfig, UrlFilter } from '../types';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { useSeriesFilters } from '../hooks/use_series_filters';
-import { getFiltersFromDefs } from '../hooks/use_lens_attributes';
-
-interface Props {
- seriesId: string;
- seriesConfig: SeriesConfig;
- isNew?: boolean;
-}
-export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const { reportDefinitions = {} } = series;
-
- const { labels } = seriesConfig;
-
- const filters: UrlFilter[] = series.filters ?? [];
-
- let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions);
-
- // we don't want to display report definition filters in new series view
- if (isNew) {
- definitionFilters = [];
- }
-
- const { removeFilter } = useSeriesFilters({ seriesId });
-
- const { indexPattern } = useAppIndexPatternContext(series.dataType);
-
- return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
-
-
- {filters.map(({ field, values, notValues }) => (
-
- {(values ?? []).map((val) => (
-
- removeFilter({ field, value: val, negate: false })}
- negate={false}
- indexPattern={indexPattern}
- />
-
- ))}
- {(notValues ?? []).map((val) => (
-
- removeFilter({ field, value: val, negate: true })}
- indexPattern={indexPattern}
- />
-
- ))}
-
- ))}
-
- {definitionFilters.map(({ field, values }) => (
-
- {(values ?? []).map((val) => (
-
- {
- // FIXME handle this use case
- }}
- negate={false}
- definitionFilter={true}
- indexPattern={indexPattern}
- />
-
- ))}
-
- ))}
-
-
- ) : null;
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
new file mode 100644
index 0000000000000..ea47ccd0b0426
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiAccordion, EuiSpacer } from '@elastic/eui';
+import { BuilderItem } from '../types';
+import { SeriesActions } from './columns/series_actions';
+import { SeriesInfo } from './columns/series_info';
+import { DataTypesSelect } from './columns/data_type_select';
+import { IncompleteBadge } from './columns/incomplete_badge';
+import { ExpandedSeriesRow } from './expanded_series_row';
+import { SeriesName } from './columns/series_name';
+import { ReportMetricOptions } from './report_metric_options';
+
+const StyledAccordion = styled(EuiAccordion)`
+ .euiAccordion__button {
+ width: auto;
+ flex-grow: 0;
+ }
+
+ .euiAccordion__optionalAction {
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+`;
+
+interface Props {
+ item: BuilderItem;
+ isExpanded: boolean;
+ toggleExpanded: () => void;
+}
+
+export function Series({ item, isExpanded, toggleExpanded }: Props) {
+ const { id } = item;
+ const seriesProps = {
+ ...item,
+ seriesId: id,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index c3cc8484d1751..d13857b5e9663 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -5,134 +5,226 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { SeriesFilter } from './columns/series_filter';
-import { SeriesConfig } from '../types';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
+import {
+ EuiSpacer,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiButtonEmpty,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+import { rgba } from 'polished';
+import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common';
+import { AppDataType, ReportViewType, BuilderItem } from '../types';
+import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage';
+import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
import { getDefaultConfigs } from '../configurations/default_configs';
-import { DatePickerCol } from './columns/date_picker_col';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { SeriesActions } from './columns/series_actions';
-import { ChartEditOptions } from './chart_edit_options';
+import { ReportTypesSelect } from './columns/report_type_select';
+import { ViewActions } from '../views/view_actions';
+import { Series } from './series';
-interface EditItem {
- seriesConfig: SeriesConfig;
+export interface ReportTypeItem {
id: string;
+ reportType: ReportViewType;
+ label: string;
}
-export function SeriesEditor() {
- const { allSeries, allSeriesIds } = useSeriesStorage();
-
- const columns = [
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.name', {
- defaultMessage: 'Name',
- }),
- field: 'id',
- width: '15%',
- render: (seriesId: string) => (
-
- {' '}
- {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
- defaultMessage: 'Filters',
- }),
- field: 'defaultFilters',
- width: '15%',
- render: (seriesId: string, { seriesConfig, id }: EditItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
- defaultMessage: 'Breakdowns',
- }),
- field: 'id',
- width: '25%',
- render: (seriesId: string, { seriesConfig, id }: EditItem) => (
-
- ),
- },
- {
- name: (
-
-
-
- ),
- width: '20%',
- field: 'id',
- align: 'right' as const,
- render: (seriesId: string, item: EditItem) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (seriesId: string, item: EditItem) => ,
- },
- ];
-
- const { indexPatterns } = useAppIndexPatternContext();
- const items: EditItem[] = [];
-
- allSeriesIds.forEach((seriesKey) => {
- const series = allSeries[seriesKey];
- if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
- items.push({
- id: seriesKey,
- seriesConfig: getDefaultConfigs({
- indexPattern: indexPatterns[series.dataType],
- reportType: series.reportType,
- dataType: series.dataType,
- }),
+type ExpandedRowMap = Record;
+
+export const getSeriesToEdit = ({
+ indexPatterns,
+ allSeries,
+ reportType,
+}: {
+ allSeries: SeriesContextValue['allSeries'];
+ indexPatterns: IndexPatternState;
+ reportType: ReportViewType;
+}): BuilderItem[] => {
+ const getDataViewSeries = (dataType: AppDataType) => {
+ if (indexPatterns?.[dataType]) {
+ return getDefaultConfigs({
+ dataType,
+ reportType,
+ indexPattern: indexPatterns[dataType],
});
}
+ };
+
+ return allSeries.map((series, seriesIndex) => {
+ const seriesConfig = getDataViewSeries(series.dataType)!;
+
+ return { id: seriesIndex, series, seriesConfig };
});
+};
- if (items.length === 0 && allSeriesIds.length > 0) {
- return null;
- }
+export const SeriesEditor = React.memo(function () {
+ const [editorItems, setEditorItems] = useState([]);
+
+ const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({});
+
+ const [{ prevCount, curCount }, setSeriesCount] = useState<{
+ prevCount?: number;
+ curCount: number;
+ }>({
+ curCount: allSeries.length,
+ });
+
+ useEffect(() => {
+ setSeriesCount((oldParams) => ({ prevCount: oldParams.curCount, curCount: allSeries.length }));
+ if (typeof prevCount !== 'undefined' && !isNaN(prevCount) && prevCount < curCount) {
+ setItemIdToExpandedRowMap({});
+ }
+ }, [allSeries.length, curCount, prevCount]);
+
+ useEffect(() => {
+ const newExpandRows: ExpandedRowMap = {};
+
+ setEditorItems((prevState) => {
+ const newEditorItems = getSeriesToEdit({
+ reportType,
+ allSeries,
+ indexPatterns,
+ });
+
+ newEditorItems.forEach(({ series, id }) => {
+ const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id);
+ if (
+ prevSeriesItem &&
+ series.selectedMetricField &&
+ prevSeriesItem.series.selectedMetricField !== series.selectedMetricField
+ ) {
+ newExpandRows[id] = true;
+ }
+ });
+ return [...newEditorItems];
+ });
+
+ setItemIdToExpandedRowMap((prevState) => {
+ return { ...prevState, ...newExpandRows };
+ });
+ }, [allSeries, getSeries, indexPatterns, loading, reportType]);
+
+ const toggleDetails = (item: BuilderItem) => {
+ const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
+ if (itemIdToExpandedRowMapValues[item.id]) {
+ delete itemIdToExpandedRowMapValues[item.id];
+ } else {
+ itemIdToExpandedRowMapValues[item.id] = true;
+ }
+ setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
+ };
+
+ const resetView = () => {
+ const totalSeries = allSeries.length;
+ for (let i = totalSeries; i >= 0; i--) {
+ removeSeries(i);
+ }
+ setEditorItems([]);
+ setItemIdToExpandedRowMap({});
+ };
return (
- <>
-
-
-
- >
+
+
+
+
+
+
+
+
+ {reportType && (
+
+ resetView()} color="text">
+ {RESET_LABEL}
+
+
+ )}
+
+
+
+
+
+
+ {editorItems.map((item) => (
+
+ toggleDetails(item)}
+ isExpanded={itemIdToExpandedRowMap[item.id]}
+ />
+
+
+ ))}
+
+
+
);
-}
+});
+
+const Wrapper = euiStyled.div`
+ &::-webkit-scrollbar {
+ height: ${({ theme }) => theme.eui.euiScrollBar};
+ width: ${({ theme }) => theme.eui.euiScrollBar};
+ }
+ &::-webkit-scrollbar-thumb {
+ background-clip: content-box;
+ background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
+ border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
+ }
+ &::-webkit-scrollbar-corner,
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &&& {
+ .euiTableRow-isExpandedRow .euiTableRowCell {
+ border-top: none;
+ background-color: #FFFFFF;
+ border-bottom: 2px solid #d3dae6;
+ border-right: 2px solid rgb(211, 218, 230);
+ border-left: 2px solid rgb(211, 218, 230);
+ }
+
+ .isExpanded {
+ border-right: 2px solid rgb(211, 218, 230);
+ border-left: 2px solid rgb(211, 218, 230);
+ .euiTableRowCell {
+ border-bottom: none;
+ }
+ }
+ .isIncomplete .euiTableRowCell {
+ background-color: rgba(254, 197, 20, 0.1);
+ }
+ }
+`;
+
+export const LOADING_VIEW = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.loadingView',
+ {
+ defaultMessage: 'Loading view ...',
+ }
+);
+
+export const SELECT_REPORT_TYPE = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.selectReportType',
+ {
+ defaultMessage: 'No report type selected',
+ }
+);
+
+export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', {
+ defaultMessage: 'Reset',
+});
+
+export const REPORT_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.reportType',
+ {
+ defaultMessage: 'Report type',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 9817899412ce3..f3592a749a2c0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -6,7 +6,7 @@
*/
import { PaletteOutput } from 'src/plugins/charts/public';
-import { ExistsFilter } from '@kbn/es-query';
+import { ExistsFilter, PhraseFilter } from '@kbn/es-query';
import {
LastValueIndexPatternColumn,
DateHistogramIndexPatternColumn,
@@ -42,7 +42,7 @@ export interface MetricOption {
field?: string;
label: string;
description?: string;
- columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN';
+ columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count';
columnFilters?: ColumnFilter[];
timeScale?: string;
}
@@ -55,7 +55,7 @@ export interface SeriesConfig {
defaultSeriesType: SeriesType;
filterFields: Array;
seriesTypes: SeriesType[];
- baseFilters?: PersistableFilter[] | ExistsFilter[];
+ baseFilters?: Array;
definitionFields: string[];
metricOptions?: MetricOption[];
labels: Record;
@@ -69,6 +69,7 @@ export interface SeriesConfig {
export type URLReportDefinition = Record;
export interface SeriesUrl {
+ name: string;
time: {
to: string;
from: string;
@@ -76,12 +77,12 @@ export interface SeriesUrl {
breakdown?: string;
filters?: UrlFilter[];
seriesType?: SeriesType;
- reportType: ReportViewType;
operationType?: OperationType;
dataType: AppDataType;
reportDefinitions?: URLReportDefinition;
selectedMetricField?: string;
- isNew?: boolean;
+ hidden?: boolean;
+ color?: string;
}
export interface UrlFilter {
@@ -116,3 +117,9 @@ export interface FieldFormat {
params: FieldFormatParams;
};
}
+
+export interface BuilderItem {
+ id: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
new file mode 100644
index 0000000000000..978296a295efc
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 { screen, waitFor, fireEvent } from '@testing-library/dom';
+import { render } from '../rtl_helpers';
+import { AddSeriesButton } from './add_series_button';
+import { DEFAULT_TIME, ReportTypes } from '../configurations/constants';
+import * as hooks from '../hooks/use_series_storage';
+
+const setSeries = jest.fn();
+
+describe('AddSeriesButton', () => {
+ beforeEach(() => {
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: [],
+ setSeries,
+ reportType: ReportTypes.KPI,
+ });
+ setSeries.mockClear();
+ });
+
+ it('renders AddSeriesButton', async () => {
+ render( );
+
+ expect(screen.getByText(/Add series/i)).toBeInTheDocument();
+ });
+
+ it('calls setSeries when AddSeries Button is clicked', async () => {
+ const { rerender } = render( );
+ let addSeriesButton = screen.getByText(/Add series/i);
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(1);
+ expect(setSeries).toBeCalledWith(0, { name: 'new-series-1', time: DEFAULT_TIME });
+ });
+
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: new Array(1),
+ setSeries,
+ reportType: ReportTypes.KPI,
+ });
+
+ rerender( );
+
+ addSeriesButton = screen.getByText(/Add series/i);
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(1);
+ expect(setSeries).toBeCalledWith(1, { name: 'new-series-2', time: DEFAULT_TIME });
+ });
+ });
+
+ it.each([ReportTypes.DEVICE_DISTRIBUTION, ReportTypes.CORE_WEB_VITAL])(
+ 'does not allow adding more than 1 series for core web vitals or device distribution',
+ async (reportType) => {
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: new Array(1), // mock array of length 1
+ setSeries,
+ reportType,
+ });
+
+ render( );
+ const addSeriesButton = screen.getByText(/Add series/i);
+ expect(addSeriesButton.closest('button')).toBeDisabled();
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(0);
+ });
+ }
+ );
+
+ it('does not allow adding a series when the report type is undefined', async () => {
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: [],
+ setSeries,
+ });
+
+ render( );
+ const addSeriesButton = screen.getByText(/Add series/i);
+ expect(addSeriesButton.closest('button')).toBeDisabled();
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
new file mode 100644
index 0000000000000..71b16c9c0e682
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+
+import { EuiToolTip, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { SeriesUrl, BuilderItem } from '../types';
+import { getSeriesToEdit } from '../series_editor/series_editor';
+import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
+import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { DEFAULT_TIME, ReportTypes } from '../configurations/constants';
+
+export function AddSeriesButton() {
+ const [editorItems, setEditorItems] = useState([]);
+ const { getSeries, allSeries, setSeries, reportType } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ useEffect(() => {
+ setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType }));
+ }, [allSeries, getSeries, indexPatterns, loading, reportType]);
+
+ const addSeries = () => {
+ const prevSeries = allSeries?.[0];
+ const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`;
+ const nextSeries = { name } as SeriesUrl;
+
+ const nextSeriesId = allSeries.length;
+
+ if (reportType === 'data-distribution') {
+ setSeries(nextSeriesId, {
+ ...nextSeries,
+ time: prevSeries?.time || DEFAULT_TIME,
+ } as SeriesUrl);
+ } else {
+ setSeries(
+ nextSeriesId,
+ prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl)
+ );
+ }
+ };
+
+ const isAddDisabled =
+ !reportType ||
+ ((reportType === ReportTypes.CORE_WEB_VITAL ||
+ reportType === ReportTypes.DEVICE_DISTRIBUTION) &&
+ allSeries.length > 0);
+
+ return (
+
+ addSeries()}
+ isDisabled={isAddDisabled}
+ iconType="plusInCircle"
+ size="s"
+ >
+ {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
+ defaultMessage: 'Add series',
+ })}
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
new file mode 100644
index 0000000000000..00fbc8c0e522f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { RefObject } from 'react';
+
+import { SeriesEditor } from '../series_editor/series_editor';
+import { AddSeriesButton } from './add_series_button';
+import { PanelId } from '../exploratory_view';
+
+export function SeriesViews({
+ seriesBuilderRef,
+}: {
+ seriesBuilderRef: RefObject;
+ onSeriesPanelCollapse: (panel: PanelId) => void;
+}) {
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
new file mode 100644
index 0000000000000..f4416ef60441d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { isEqual } from 'lodash';
+import { allSeriesKey, convertAllShortSeries, useSeriesStorage } from '../hooks/use_series_storage';
+
+export function ViewActions() {
+ const { allSeries, storage, applyChanges } = useSeriesStorage();
+
+ const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? []));
+
+ return (
+
+
+ applyChanges()} isDisabled={noChanges} fill size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
+ defaultMessage: 'Apply changes',
+ })}
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
index fc562fa80e26d..0735df53888aa 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
@@ -6,15 +6,24 @@
*/
import React, { useEffect, useState } from 'react';
-import { union } from 'lodash';
-import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui';
+import { union, isEmpty } from 'lodash';
+import {
+ EuiComboBox,
+ EuiFormControlLayout,
+ EuiComboBoxOptionOption,
+ EuiFormRow,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { FieldValueSelectionProps } from './types';
export const ALL_VALUES_SELECTED = 'ALL_VALUES';
const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => {
const uniqueValues = Array.from(
- new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values)
+ new Set(
+ allowAllValuesSelection && (values ?? []).length > 0
+ ? ['ALL_VALUES', ...(values ?? [])]
+ : values
+ )
);
return (uniqueValues ?? []).map((label) => ({
@@ -30,7 +39,9 @@ export function FieldValueCombobox({
loading,
values,
setQuery,
+ usePrependLabel = true,
compressed = true,
+ required = true,
allowAllValuesSelection,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
@@ -54,29 +65,35 @@ export function FieldValueCombobox({
onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl));
};
- return (
+ const comboBox = (
+ {
+ setQuery(searchVal);
+ }}
+ options={options}
+ selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))}
+ onChange={onChange}
+ isInvalid={required && isEmpty(selectedValue)}
+ />
+ );
+
+ return usePrependLabel ? (
- {
- setQuery(searchVal);
- }}
- options={options}
- selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))}
- onChange={onChange}
- />
+ {comboBox}
+ ) : (
+
+ {comboBox}
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
index aca29c4723688..dfcd917cf534b 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
@@ -70,8 +70,8 @@ export function FieldValueSelection({
values = [],
selectedValue,
excludedValue,
- compressed = true,
allowExclusions = true,
+ compressed = true,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
const [options, setOptions] = useState(() =>
@@ -174,8 +174,8 @@ export function FieldValueSelection({
}}
options={options}
onChange={onChange}
- isLoading={loading && !query && options.length === 0}
allowExclusions={allowExclusions}
+ isLoading={loading && !query && options.length === 0}
>
{(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
index 556a8e7052347..6671c43dd8c7b 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
@@ -95,6 +95,7 @@ describe('FieldValueSuggestions', () => {
selectedValue={[]}
filters={[]}
asCombobox={false}
+ allowExclusions={true}
/>
);
@@ -119,6 +120,7 @@ describe('FieldValueSuggestions', () => {
excludedValue={['Pak']}
filters={[]}
asCombobox={false}
+ allowExclusions={true}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
index 3de158ba0622f..1c5da15dd33df 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
@@ -28,9 +28,11 @@ export function FieldValueSuggestions({
singleSelection,
compressed,
asFilterButton,
+ usePrependLabel,
allowAllValuesSelection,
+ required,
+ allowExclusions = true,
cardinalityField,
- allowExclusions,
asCombobox = true,
onChange: onSelectionChange,
}: FieldValueSuggestionsProps) {
@@ -67,8 +69,10 @@ export function FieldValueSuggestions({
width={width}
compressed={compressed}
asFilterButton={asFilterButton}
- allowAllValuesSelection={allowAllValuesSelection}
+ usePrependLabel={usePrependLabel}
allowExclusions={allowExclusions}
+ allowAllValuesSelection={allowAllValuesSelection}
+ required={required}
/>
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
index 046f98748cdf2..b6de2bafdd852 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
@@ -23,10 +23,11 @@ interface CommonProps {
compressed?: boolean;
asFilterButton?: boolean;
showCount?: boolean;
+ usePrependLabel?: boolean;
+ allowExclusions?: boolean;
allowAllValuesSelection?: boolean;
cardinalityField?: string;
required?: boolean;
- allowExclusions?: boolean;
}
export type FieldValueSuggestionsProps = CommonProps & {
diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
index 01d727071770d..9e7b96b02206f 100644
--- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
@@ -18,21 +18,25 @@ export function buildFilterLabel({
negate,
}: {
label: string;
- value: string;
+ value: string | string[];
negate: boolean;
field: string;
indexPattern: IndexPattern;
}) {
const indexField = indexPattern.getFieldByName(field)!;
- const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern);
+ const filter =
+ value instanceof Array && value.length > 1
+ ? esFilters.buildPhrasesFilter(indexField, value, indexPattern)
+ : esFilters.buildPhraseFilter(indexField, value as string, indexPattern);
- filter.meta.value = value;
+ filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase';
+
+ filter.meta.value = value as string;
filter.meta.key = label;
filter.meta.alias = null;
filter.meta.negate = negate;
filter.meta.disabled = false;
- filter.meta.type = 'phrase';
return filter;
}
@@ -40,10 +44,10 @@ export function buildFilterLabel({
interface Props {
field: string;
label: string;
- value: string;
+ value: string | string[];
negate: boolean;
- removeFilter: (field: string, value: string, notVal: boolean) => void;
- invertFilter: (val: { field: string; value: string; negate: boolean }) => void;
+ removeFilter: (field: string, value: string | string[], notVal: boolean) => void;
+ invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void;
indexPattern: IndexPattern;
allowExclusion?: boolean;
}
diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx
index 9d557a40b7987..afc053604fcdf 100644
--- a/x-pack/plugins/observability/public/components/shared/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/index.tsx
@@ -6,6 +6,7 @@
*/
import React, { lazy, Suspense } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
import type { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type { FieldValueSuggestionsProps } from './field_value_suggestions/types';
@@ -26,7 +27,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal'));
export function HeaderMenuPortal(props: HeaderMenuPortalProps) {
return (
-
+ }>
);
diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
index 82a0fc39b8519..198b4092b0ed6 100644
--- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
+++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
@@ -7,7 +7,7 @@
import { useUiSetting } from '../../../../../src/plugins/kibana_react/public';
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
-import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker';
+import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker';
export function useQuickTimeRanges() {
const timePickerQuickRanges = useUiSetting
(
diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts
index 118f0783f9688..10843bbd1d5b5 100644
--- a/x-pack/plugins/observability/public/plugin.ts
+++ b/x-pack/plugins/observability/public/plugin.ts
@@ -24,6 +24,7 @@ import type {
DataPublicPluginSetup,
DataPublicPluginStart,
} from '../../../../src/plugins/data/public';
+import type { DiscoverStart } from '../../../../src/plugins/discover/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type {
HomePublicPluginSetup,
@@ -58,6 +59,7 @@ export interface ObservabilityPublicPluginsStart {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
data: DataPublicPluginStart;
lens: LensPublicStart;
+ discover: DiscoverStart;
}
export type ObservabilityPublicStart = ReturnType;
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
index 00e487da7f9b7..ff03379e39963 100644
--- a/x-pack/plugins/observability/public/routes/index.tsx
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -99,7 +99,7 @@ export const routes = {
}),
},
},
- '/exploratory-view': {
+ '/exploratory-view/': {
handler: () => {
return ;
},
@@ -112,18 +112,4 @@ export const routes = {
}),
},
},
- // enable this to test multi series architecture
- // '/exploratory-view/multi': {
- // handler: () => {
- // return ;
- // },
- // params: {
- // query: t.partial({
- // rangeFrom: t.string,
- // rangeTo: t.string,
- // refreshPaused: jsonRt.pipe(t.boolean),
- // refreshInterval: jsonRt.pipe(t.number),
- // }),
- // },
- // },
};
diff --git a/x-pack/plugins/osquery/public/packs/common/pack_queries_table.tsx b/x-pack/plugins/osquery/public/packs/common/pack_queries_table.tsx
index ce30238effa2b..bf57f818dc3d9 100644
--- a/x-pack/plugins/osquery/public/packs/common/pack_queries_table.tsx
+++ b/x-pack/plugins/osquery/public/packs/common/pack_queries_table.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-/* eslint-disable @typescript-eslint/no-shadow, react-perf/jsx-no-new-object-as-prop, react/jsx-no-bind, react/display-name, react-perf/jsx-no-new-function-as-prop, react-perf/jsx-no-new-array-as-prop */
+/* eslint-disable @typescript-eslint/no-shadow, react-perf/jsx-no-new-object-as-prop, react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop, react-perf/jsx-no-new-array-as-prop */
import { find } from 'lodash/fp';
import React, { useState } from 'react';
diff --git a/x-pack/plugins/osquery/public/packs/list/pack_table_queries_table.tsx b/x-pack/plugins/osquery/public/packs/list/pack_table_queries_table.tsx
index 5470dc6ba569c..14110275b9cb4 100644
--- a/x-pack/plugins/osquery/public/packs/list/pack_table_queries_table.tsx
+++ b/x-pack/plugins/osquery/public/packs/list/pack_table_queries_table.tsx
@@ -24,7 +24,6 @@ const columns = [
{
field: 'query',
name: 'Query',
- // eslint-disable-next-line react/display-name
render: (query: string) => (
{query}
diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx
index 0e683acf660a2..c59cd6281a364 100644
--- a/x-pack/plugins/osquery/public/results/results_table.tsx
+++ b/x-pack/plugins/osquery/public/results/results_table.tsx
@@ -120,6 +120,7 @@ const ResultsTableComponent: React.FC = ({
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
() =>
+ // eslint-disable-next-line react/display-name
({ rowIndex, columnId }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const data = useContext(DataContext);
diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx
index 9cc55a65ce2bc..57f96c1f9bc03 100644
--- a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx
+++ b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx
@@ -8,6 +8,7 @@
import React, { lazy, Suspense } from 'react';
// @ts-expect-error update types
+// eslint-disable-next-line react/display-name
export const getLazyOsqueryAction = (services) => (props) => {
const OsqueryAction = lazy(() => import('./osquery_action'));
return (
diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts
index a3fc285c702a8..b1d69d17334be 100644
--- a/x-pack/plugins/reporting/server/config/create_config.ts
+++ b/x-pack/plugins/reporting/server/config/create_config.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n/';
+import { i18n } from '@kbn/i18n';
import crypto from 'crypto';
import { upperFirst } from 'lodash';
import { Observable } from 'rxjs';
diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts
index 611e7bd456da3..a4025b619365f 100644
--- a/x-pack/plugins/security/server/audit/audit_events.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.ts
@@ -10,7 +10,7 @@ import type { EcsEventOutcome, EcsEventType, KibanaRequest, LogMeta } from 'src/
import type { AuthenticationResult } from '../authentication/authentication_result';
/**
- * Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.9/index.html
+ * Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.12/index.html
*
* If you add additional fields to the schema ensure you update the Kibana Filebeat module:
* https://github.com/elastic/beats/tree/master/filebeat/module/kibana
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index d77a555991df8..e91f74320c026 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -7,9 +7,10 @@
import type { TransformConfigSchema } from './transforms/types';
import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
-import { metadataTransformPattern } from './endpoint/constants';
+import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants';
export const APP_ID = 'securitySolution';
+export const CASES_FEATURE_ID = 'securitySolutionCases';
export const SERVER_APP_ID = 'siem';
export const APP_NAME = 'Security';
export const APP_ICON = 'securityAnalyticsApp';
@@ -330,6 +331,23 @@ export const showAllOthersBucket: string[] = [
*/
export const ELASTIC_NAME = 'estc';
-export const TRANSFORM_STATS_URL = `/api/transform/transforms/${metadataTransformPattern}-*/_stats`;
+export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`;
export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest';
+
+export const TRANSFORM_STATES = {
+ ABORTING: 'aborting',
+ FAILED: 'failed',
+ INDEXING: 'indexing',
+ STARTED: 'started',
+ STOPPED: 'stopped',
+ STOPPING: 'stopping',
+ WAITING: 'waiting',
+};
+
+export const WARNING_TRANSFORM_STATES = new Set([
+ TRANSFORM_STATES.ABORTING,
+ TRANSFORM_STATES.FAILED,
+ TRANSFORM_STATES.STOPPED,
+ TRANSFORM_STATES.STOPPING,
+]);
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index a38266c414e6b..c7949299c68db 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -20,10 +20,13 @@ export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*'
/** The metadata Transform Name prefix with NO (package) version) */
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
-/** The metadata Transform Name prefix with NO namespace and NO (package) version) */
-export const metadataTransformPattern = 'endpoint.metadata_current-*';
+// metadata transforms pattern for matching all metadata transform ids
+export const METADATA_TRANSFORMS_PATTERN = 'endpoint.metadata_*';
+// united metadata transform id
export const METADATA_UNITED_TRANSFORM = 'endpoint.metadata_united-default';
+
+// united metadata transform destination index
export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts
index 985fa2c4aadcf..e19cffb808464 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint.ts
@@ -110,15 +110,20 @@ export const installOrUpgradeEndpointFleetPackage = async (
);
}
- if (isFleetBulkInstallError(bulkResp[0])) {
- if (bulkResp[0].error instanceof Error) {
+ const firstError = bulkResp[0];
+
+ if (isFleetBulkInstallError(firstError)) {
+ if (firstError.error instanceof Error) {
throw new EndpointDataLoadingError(
- `Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting`,
+ `Installing the Endpoint package failed: ${firstError.error.message}, exiting`,
bulkResp
);
}
- throw new EndpointDataLoadingError(bulkResp[0].error, bulkResp);
+ // Ignore `409` (conflicts due to Concurrent install or upgrades of package) errors
+ if (firstError.statusCode !== 409) {
+ throw new EndpointDataLoadingError(firstError.error, bulkResp);
+ }
}
return bulkResp[0] as BulkInstallPackageInfo;
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts
index 4c6d2f6037356..9815bc3535de4 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts
@@ -32,7 +32,7 @@ export type GetTrustedAppsListRequest = TypeOf;
-export interface GetTrustedListAppsResponse {
+export interface GetTrustedAppsListResponse {
per_page: number;
page: number;
total: number;
diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts
new file mode 100644
index 0000000000000..c20e6cf6b6370
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { esArchiverCCSLoad } from '../../tasks/es_archiver';
+import { getCCSEqlRule } from '../../objects/rule';
+
+import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts';
+
+import {
+ filterByCustomRules,
+ goToRuleDetails,
+ waitForRulesTableToBeLoaded,
+} from '../../tasks/alerts_detection_rules';
+import { createSignalsIndex, createEventCorrelationRule } from '../../tasks/api_calls/rules';
+import { cleanKibana } from '../../tasks/common';
+import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule';
+import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
+
+import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
+
+describe('Detection rules', function () {
+ const expectedNumberOfAlerts = '1 alert';
+
+ beforeEach('Reset signals index', function () {
+ cleanKibana();
+ createSignalsIndex();
+ });
+
+ it('EQL rule on remote indices generates alerts', function () {
+ esArchiverCCSLoad('linux_process');
+ this.rule = getCCSEqlRule();
+ createEventCorrelationRule(this.rule);
+
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
+ waitForRulesTableToBeLoaded();
+ filterByCustomRules();
+ goToRuleDetails();
+ waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
+
+ cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
+ cy.get(ALERT_DATA_GRID)
+ .invoke('text')
+ .then((text) => {
+ cy.log('ALERT_DATA_GRID', text);
+ expect(text).contains(this.rule.name);
+ expect(text).contains(this.rule.severity.toLowerCase());
+ expect(text).contains(this.rule.riskScore);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts
index 4d6c60e93ee20..23016ecc512b1 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts
@@ -81,6 +81,7 @@ const secAll: Role = {
{
feature: {
siem: ['all'],
+ securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
},
@@ -110,7 +111,8 @@ const secReadCasesAll: Role = {
kibana: [
{
feature: {
- siem: ['minimal_read', 'cases_all'],
+ siem: ['read'],
+ securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
},
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 173bfa524e66e..db76bfc3cf4df 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -72,6 +72,10 @@ export interface OverrideRule extends CustomRule {
timestampOverride: string;
}
+export interface EventCorrelationRule extends CustomRule {
+ language: string;
+}
+
export interface ThreatIndicatorRule extends CustomRule {
indicatorIndexPattern: string[];
indicatorMappingField: string;
@@ -326,6 +330,25 @@ export const getEqlRule = (): CustomRule => ({
maxSignals: 100,
});
+export const getCCSEqlRule = (): EventCorrelationRule => ({
+ customQuery: 'any where process.name == "run-parts"',
+ name: 'New EQL Rule',
+ index: [`${ccsRemoteName}:run-parts`],
+ description: 'New EQL rule description.',
+ severity: 'High',
+ riskScore: '17',
+ tags: ['test', 'newRule'],
+ referenceUrls: ['http://example.com/', 'https://example.com/'],
+ falsePositivesExamples: ['False1', 'False2'],
+ mitre: [getMitre1(), getMitre2()],
+ note: '# test markdown',
+ runsEvery: getRunsEvery(),
+ lookBack: getLookBack(),
+ timeline: getTimeline(),
+ maxSignals: 100,
+ language: 'eql',
+});
+
export const getEqlSequenceRule = (): CustomRule => ({
customQuery:
'sequence with maxspan=30s\
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
index 33bd8a06b9985..130467cde053d 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { CustomRule, ThreatIndicatorRule } from '../../objects/rule';
+import { CustomRule, EventCorrelationRule, ThreatIndicatorRule } from '../../objects/rule';
export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') =>
cy.request({
@@ -29,6 +29,27 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte
failOnStatusCode: false,
});
+export const createEventCorrelationRule = (rule: EventCorrelationRule, ruleId = 'rule_testing') =>
+ cy.request({
+ method: 'POST',
+ url: 'api/detection_engine/rules',
+ body: {
+ rule_id: ruleId,
+ risk_score: parseInt(rule.riskScore, 10),
+ description: rule.description,
+ interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
+ from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
+ name: rule.name,
+ severity: rule.severity.toLocaleLowerCase(),
+ type: 'eql',
+ index: rule.index,
+ query: rule.customQuery,
+ language: 'eql',
+ enabled: true,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
+
export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') =>
cy.request({
method: 'POST',
@@ -107,6 +128,14 @@ export const deleteCustomRule = (ruleId = '1') => {
});
};
+export const createSignalsIndex = () => {
+ cy.request({
+ method: 'POST',
+ url: 'api/detection_engine/index',
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
+};
+
export const removeSignalsIndex = () => {
cy.request({ url: '/api/detection_engine/index', failOnStatusCode: false }).then((response) => {
if (response.status === 200) {
diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts
index 5bcc598c7be5e..a3dc6565b19c6 100644
--- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts
+++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts
@@ -8,6 +8,7 @@ import { getDeepLinks, PREMIUM_DEEP_LINK_IDS } from '.';
import { AppDeepLink, Capabilities } from '../../../../../../src/core/public';
import { SecurityPageName } from '../types';
import { mockGlobalState } from '../../common/mock';
+import { CASES_FEATURE_ID } from '../../../common/constants';
const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null =>
deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => {
@@ -58,7 +59,7 @@ describe('deepLinks', () => {
it('should return case links for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
- siem: { read_cases: true, crud_cases: false },
+ [CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
} as unknown as Capabilities);
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeTruthy();
@@ -67,7 +68,7 @@ describe('deepLinks', () => {
it('should return case links with NO deepLinks for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
- siem: { read_cases: true, crud_cases: false },
+ [CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
} as unknown as Capabilities);
expect(findDeepLink(SecurityPageName.case, basicLinks)?.deepLinks?.length === 0).toBeTruthy();
});
@@ -75,7 +76,7 @@ describe('deepLinks', () => {
it('should return case links with deepLinks for basic license with crud_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
- siem: { read_cases: true, crud_cases: true },
+ [CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
} as unknown as Capabilities);
expect(
@@ -86,7 +87,7 @@ describe('deepLinks', () => {
it('should return NO case links for basic license with NO read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
- siem: { read_cases: false, crud_cases: false },
+ [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
} as unknown as Capabilities);
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeFalsy();
diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts
index e8d4f5d09e5f7..aaa8ce789591f 100644
--- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts
+++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts
@@ -48,6 +48,7 @@ import {
TRUSTED_APPS_PATH,
EVENT_FILTERS_PATH,
UEBA_PATH,
+ CASES_FEATURE_ID,
HOST_ISOLATION_EXCEPTIONS_PATH,
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';
@@ -362,7 +363,7 @@ export function getDeepLinks(
return false;
}
if (deepLink.id === SecurityPageName.case) {
- return capabilities == null || capabilities.siem.read_cases === true;
+ return capabilities == null || capabilities[CASES_FEATURE_ID].read_cases === true;
}
if (deepLink.id === SecurityPageName.ueba) {
return enableExperimental.uebaEnabled;
@@ -373,7 +374,7 @@ export function getDeepLinks(
if (
deepLink.id === SecurityPageName.case &&
capabilities != null &&
- capabilities.siem.crud_cases === false
+ capabilities[CASES_FEATURE_ID].crud_cases === false
) {
return {
...deepLink,
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
index ad15f0a5fa9fb..c24e41d096546 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
@@ -21,7 +21,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
index 878a6de89747b..4dd6fa32db0ab 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
@@ -21,7 +21,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
index de4d348bfb8f5..17b70a9903590 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
@@ -20,7 +20,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
index d27ad96ff3c4f..c5cf2b6ea3379 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
@@ -23,7 +23,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
index ddb9d17c40ead..ee62be0d88ae1 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
@@ -95,10 +95,8 @@ type RenderFunctionProp = (
interface Props {
dataProvider: DataProvider;
- disabled?: boolean;
hideTopN?: boolean;
isDraggable?: boolean;
- inline?: boolean;
render: RenderFunctionProp;
timelineId?: string;
truncate?: boolean;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
index b91978489f64e..dbb52ade2652c 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import { EuiPanel, EuiText } from '@elastic/eui';
import { get } from 'lodash';
import memoizeOne from 'memoize-one';
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
index 4c31db473ae32..a05a9e36a24e9 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
@@ -22,7 +22,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index d4cc68b12d786..632197aa6b219 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -121,7 +121,6 @@ interface Props {
end: string;
filters: Filter[];
headerFilterGroup?: React.ReactNode;
- height?: number;
id: TimelineId;
indexNames: string[];
indexPattern: IIndexPattern;
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index 768137c9d731d..d60db8a4bc461 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -37,7 +37,6 @@ const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
const leadingControlColumns: ControlColumnProps[] = [
{
...defaultControlColumn,
- // eslint-disable-next-line react/display-name
headerCellRender: () => <>{i18n.ACTIONS}>,
},
];
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
index 1546966c37cd8..f70f96a26c83a 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
@@ -6,17 +6,17 @@
*/
import React, { useCallback, memo } from 'react';
-import { EuiToolTip, EuiLink, EuiMarkdownAstNodePosition } from '@elastic/eui';
+import { EuiToolTip, EuiLink } from '@elastic/eui';
import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click';
import { TimelineProps } from './types';
import * as i18n from './translations';
-export const TimelineMarkDownRendererComponent: React.FC<
- TimelineProps & {
- position: EuiMarkdownAstNodePosition;
- }
-> = ({ id, title, graphEventId }) => {
+export const TimelineMarkDownRendererComponent: React.FC = ({
+ id,
+ title,
+ graphEventId,
+}) => {
const handleTimelineClick = useTimelineClick();
const onClickTimeline = useCallback(
() => handleTimelineClick(id ?? '', graphEventId),
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
index 5fa84affe8f99..6c8f77a279a4a 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
@@ -19,6 +19,7 @@ interface Props {
const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => {
const MarkdownLinkProcessingComponent: React.FC = useMemo(
+ // eslint-disable-next-line react/display-name
() => (props) => ,
[disableLinks]
);
@@ -38,4 +39,6 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks })
);
};
+MarkdownRendererComponent.displayName = 'MarkdownRendererComponent';
+
export const MarkdownRenderer = memo(MarkdownRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
index 7c23faa433494..2260651287bd8 100644
--- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
index f08edb114b9a9..587d518bc11f0 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
@@ -19,7 +19,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx
index 197f88479347d..468cb416a2f9b 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Columns } from '../../paginated_table';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
index bc383ccefa453..4c4e131a9d467 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx
index 3eb2d1eda1ead..5f39ddda2ca34 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React, { useEffect, useState } from 'react';
import {
diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
index e2961de91c448..5f2c76632aba9 100644
--- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { ThemeProvider } from 'styled-components';
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
index ae2e509a7d94e..ec28326f7d170 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { Provider } from 'react-redux';
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx
index f1d1b09f45f60..968bd8679b23b 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx
@@ -21,7 +21,8 @@ jest.mock('../components/user_privileges', () => {
});
jest.mock('../lib/kibana');
-describe('When using the `useUpgradeSecurityPackages()` hook', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/112910
+describe.skip('When using the `useUpgradeSecurityPackages()` hook', () => {
let renderResult: RenderHookResult;
let renderHook: () => RenderHookResult;
let kibana: ReturnType;
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts
index adc06468d9a02..fdaed64ba91d7 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts
@@ -12,7 +12,12 @@ import { i18n } from '@kbn/i18n';
import { camelCase, isArray, isObject } from 'lodash';
import { set } from '@elastic/safer-lodash-set';
-import { APP_ID, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
+import {
+ APP_ID,
+ CASES_FEATURE_ID,
+ DEFAULT_DATE_FORMAT,
+ DEFAULT_DATE_FORMAT_TZ,
+} from '../../../../common/constants';
import { errorToToaster, useStateToaster } from '../../components/toasters';
import { AuthenticatedUser } from '../../../../../security/common/model';
import { NavigateToAppOptions } from '../../../../../../../src/core/public';
@@ -151,13 +156,9 @@ export const useGetUserCasesPermissions = () => {
const uiCapabilities = useKibana().services.application.capabilities;
useEffect(() => {
- const capabilitiesCanUserCRUD: boolean =
- typeof uiCapabilities.siem.crud_cases === 'boolean' ? uiCapabilities.siem.crud_cases : false;
- const capabilitiesCanUserRead: boolean =
- typeof uiCapabilities.siem.read_cases === 'boolean' ? uiCapabilities.siem.read_cases : false;
setCasesPermissions({
- crud: capabilitiesCanUserCRUD,
- read: capabilitiesCanUserRead,
+ crud: !!uiCapabilities[CASES_FEATURE_ID].crud_cases,
+ read: !!uiCapabilities[CASES_FEATURE_ID].read_cases,
});
}, [uiCapabilities]);
diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
index 3f2d837530747..ab48ec0b6e006 100644
--- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
+++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx
@@ -99,4 +99,6 @@ export const SpyRouteComponent = memo<
}
);
+SpyRouteComponent.displayName = 'SpyRouteComponent';
+
export const SpyRoute = withRouter(SpyRouteComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
index 1793b31197f7d..2e4b866b3017b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
@@ -35,7 +35,7 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name, @typescript-eslint/no-explicit-any
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
EuiFieldText: (props: any) => {
const { isInvalid, isLoading, fullWidth, inputRef, isDisabled, ...validInputProps } = props;
return ;
diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx
index e7d726ed89e6f..223701a2f7f12 100644
--- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
import styled from 'styled-components';
import { EuiButtonIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index c6145a70ec8d2..ad5a85a03464e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import {
EuiBasicTableColumn,
EuiTableActionsColumnType,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
index 1ef3c3d3c5414..6d6fd974b20f5 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React from 'react';
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
index 2fedd6160af2c..a7db7ab57f6c2 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import {
EuiBasicTable,
EuiPanel,
diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
index 453bb5c81dd58..b83853eec69a1 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import { has } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
index 2cd4ed1f57f84..29d3f110e8181 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
@@ -26,7 +26,6 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
- // eslint-disable-next-line react/display-name
EuiScreenReaderOnly: () => <>>,
};
});
diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx
index 72dd7d7d88e8a..0541f2f1d403d 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-/* eslint-disable react/display-name */
-
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
diff --git a/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx b/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx
index 7d46c7c80677d..c2f9e32f61afb 100644
--- a/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx
@@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n';
import {
ContextMenuItemNavByRouter,
ContextMenuItemNavByRouterProps,
-} from '../context_menu_with_router_support/context_menu_item_nav_by_rotuer';
+} from '../context_menu_with_router_support/context_menu_item_nav_by_router';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
export interface ActionsContextMenuProps {
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx
new file mode 100644
index 0000000000000..a44076c8ad112
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
+import { cloneDeep } from 'lodash';
+import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
+import React from 'react';
+import { ArtifactCardGrid, ArtifactCardGridProps } from './artifact_card_grid';
+
+// FIXME:PT refactor helpers below after merge of PR https://github.com/elastic/kibana/pull/113363
+
+const getCommonItemDataOverrides = () => {
+ return {
+ name: 'some internal app',
+ description: 'this app is trusted by the company',
+ created_at: new Date('2021-07-01').toISOString(),
+ };
+};
+
+const getTrustedAppProvider = () =>
+ new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides());
+
+const getExceptionProvider = () => {
+ // cloneDeep needed because exception mock generator uses state across instances
+ return cloneDeep(
+ getExceptionListItemSchemaMock({
+ ...getCommonItemDataOverrides(),
+ os_types: ['windows'],
+ updated_at: new Date().toISOString(),
+ created_by: 'Justa',
+ updated_by: 'Mara',
+ entries: [
+ {
+ field: 'process.hash.*',
+ operator: 'included',
+ type: 'match',
+ value: '1234234659af249ddf3e40864e9fb241',
+ },
+ {
+ field: 'process.executable.caseless',
+ operator: 'included',
+ type: 'match',
+ value: '/one/two/three',
+ },
+ ],
+ tags: ['policy:all'],
+ })
+ );
+};
+
+describe.each([
+ ['trusted apps', getTrustedAppProvider],
+ ['exceptions/event filters', getExceptionProvider],
+])('when using the ArtifactCardGrid component %s', (_, generateItem) => {
+ let appTestContext: AppContextTestRender;
+ let renderResult: ReturnType;
+ let render: (
+ props?: Partial
+ ) => ReturnType;
+ let items: ArtifactCardGridProps['items'];
+ let pageChangeHandler: jest.Mock;
+ let expandCollapseHandler: jest.Mock;
+ let cardComponentPropsProvider: Required['cardComponentProps'];
+
+ beforeEach(() => {
+ items = Array.from({ length: 5 }, () => generateItem());
+ pageChangeHandler = jest.fn();
+ expandCollapseHandler = jest.fn();
+ cardComponentPropsProvider = jest.fn().mockReturnValue({});
+
+ appTestContext = createAppRootMockRenderer();
+ render = (props = {}) => {
+ renderResult = appTestContext.render(
+
+ );
+ return renderResult;
+ };
+ });
+
+ it('should render the cards', () => {
+ render();
+
+ expect(renderResult.getAllByTestId('testGrid-card')).toHaveLength(5);
+ });
+
+ it.each([
+ ['header', 'testGrid-header'],
+ ['expand/collapse placeholder', 'testGrid-header-expandCollapsePlaceHolder'],
+ ['name column', 'testGrid-header-layout-title'],
+ ['description column', 'testGrid-header-layout-description'],
+ ['description column', 'testGrid-header-layout-cardActionsPlaceholder'],
+ ])('should display the Grid Header - %s', (__, selector) => {
+ render();
+
+ expect(renderResult.getByTestId(selector)).not.toBeNull();
+ });
+
+ it.todo('should call onPageChange callback when paginating');
+
+ it.todo('should use the props provided by cardComponentProps callback');
+
+ describe('and when cards are expanded/collapsed', () => {
+ it.todo('should call onExpandCollapse callback');
+
+ it.todo('should provide list of cards that are expanded and collapsed');
+
+ it.todo('should show card expanded if card props defined it as such');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx
new file mode 100644
index 0000000000000..9e9082ccc54e7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx
@@ -0,0 +1,143 @@
+/*
+ * 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, { ComponentType, memo, useCallback, useMemo } from 'react';
+import {
+ AnyArtifact,
+ ArtifactEntryCollapsibleCard,
+ ArtifactEntryCollapsibleCardProps,
+} from '../artifact_entry_card';
+import { PaginatedContent as _PaginatedContent, PaginatedContentProps } from '../paginated_content';
+import { GridHeader } from './components/grid_header';
+import { MaybeImmutable } from '../../../../common/endpoint/types';
+import { useTestIdGenerator } from '../hooks/use_test_id_generator';
+
+const PaginatedContent: ArtifactsPaginatedComponent = _PaginatedContent;
+
+type ArtifactsPaginatedContentProps = PaginatedContentProps<
+ AnyArtifact,
+ typeof ArtifactEntryCollapsibleCard
+>;
+
+type ArtifactsPaginatedComponent = ComponentType;
+
+interface CardExpandCollapseState {
+ expanded: MaybeImmutable;
+ collapsed: MaybeImmutable;
+}
+
+export type ArtifactCardGridCardComponentProps = Omit<
+ ArtifactEntryCollapsibleCardProps,
+ 'onExpandCollapse' | 'item'
+>;
+export type ArtifactCardGridProps = Omit<
+ ArtifactsPaginatedContentProps,
+ 'ItemComponent' | 'itemComponentProps' | 'items' | 'onChange'
+> & {
+ items: MaybeImmutable;
+
+ /** Callback to handle pagination changes */
+ onPageChange: ArtifactsPaginatedContentProps['onChange'];
+
+ /** callback for handling changes to the card's expand/collapse state */
+ onExpandCollapse: (state: CardExpandCollapseState) => void;
+
+ /**
+ * Callback to provide additional props for the `ArtifactEntryCollapsibleCard`
+ */
+ cardComponentProps?: (item: MaybeImmutable