')
- .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/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 34e44d37bc4b8..1b83bcac5837f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -5628,7 +5628,6 @@
"visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント",
"visTypeVislib.controls.gaugeOptions.alignmentLabel": "アラインメント",
"visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張",
- "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "警告を表示",
"visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。",
"visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "ゲージタイプ",
"visTypeVislib.controls.gaugeOptions.labelsTitle": "ラベル",
@@ -5639,7 +5638,6 @@
"visTypeVislib.controls.gaugeOptions.showScaleLabel": "縮尺を表示",
"visTypeVislib.controls.gaugeOptions.styleTitle": "スタイル",
"visTypeVislib.controls.gaugeOptions.subTextLabel": "サブラベル",
- "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "警告のオン/オフを切り替えます。オンにすると、すべてのラベルを表示できない際に警告が表示されます。",
"visTypeVislib.controls.heatmapOptions.colorLabel": "色",
"visTypeVislib.controls.heatmapOptions.colorScaleLabel": "カラースケール",
"visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "色の数",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1ec387f8f0f30..c29557eebd3f8 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -5674,7 +5674,6 @@
"visTypeVislib.aggResponse.allDocsTitle": "所有文档",
"visTypeVislib.controls.gaugeOptions.alignmentLabel": "对齐方式",
"visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围",
- "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "显示警告",
"visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到数据中的最大值。",
"visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "仪表类型",
"visTypeVislib.controls.gaugeOptions.labelsTitle": "标签",
@@ -5685,7 +5684,6 @@
"visTypeVislib.controls.gaugeOptions.showScaleLabel": "显示比例",
"visTypeVislib.controls.gaugeOptions.styleTitle": "样式",
"visTypeVislib.controls.gaugeOptions.subTextLabel": "子标签",
- "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "打开/关闭警告。打开时,如果标签没有全部显示,则显示警告。",
"visTypeVislib.controls.heatmapOptions.colorLabel": "颜色",
"visTypeVislib.controls.heatmapOptions.colorScaleLabel": "色阶",
"visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "颜色个数",
From e0894f3dc22c8c2f07b4a845794638815baf17b7 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 4 Oct 2021 11:45:33 +0300
Subject: [PATCH 23/98] Unskips the visualize reporting functional test suite
(#113535)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/test/functional/apps/visualize/reporting.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts
index 9491416f328eb..f08d242f4024f 100644
--- a/x-pack/test/functional/apps/visualize/reporting.ts
+++ b/x-pack/test/functional/apps/visualize/reporting.ts
@@ -42,8 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/113496
- describe.skip('Print PDF button', () => {
+ describe('Print PDF button', () => {
it('is available if new', async () => {
await PageObjects.common.navigateToUrl('visualize', 'new', { useActualUrl: true });
await PageObjects.visualize.clickAggBasedVisualizations();
@@ -54,6 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('becomes available when saved', async () => {
+ await PageObjects.timePicker.timePickerExists();
const fromTime = 'Apr 27, 2019 @ 23:56:51.374';
const toTime = 'Aug 23, 2019 @ 16:18:51.821';
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
From 93522e5fa8b4d73dffaf1aaaa3e1d5d9703b29d4 Mon Sep 17 00:00:00 2001
From: Diana Derevyankina
<54894989+DziyanaDzeraviankina@users.noreply.github.com>
Date: Mon, 4 Oct 2021 12:41:56 +0300
Subject: [PATCH 24/98] [Lens] Rename Index pattern to Data view (#110252)
* [Lens] Rename Index pattern to Data view
* Update tests expected labels
* Remove couple of unused translations
* Remove unused translation
* Revert accidentally not applied string because of merge conflict
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../editor_frame/config_panel/add_layer.tsx | 2 +-
.../workspace_panel/workspace_panel.tsx | 12 ++++++------
.../editor_frame_service/error_helper.ts | 5 ++---
.../change_indexpattern.tsx | 4 ++--
.../indexpattern_datasource/datapanel.tsx | 18 +++++++++---------
.../dimensions_editor_helpers.tsx | 2 +-
.../indexpattern_datasource/field_item.tsx | 8 ++++----
.../indexpattern_datasource/indexpattern.tsx | 4 ++--
.../indexpattern_datasource/layerpanel.tsx | 4 ++--
.../no_fields_callout.test.tsx | 2 +-
.../no_fields_callout.tsx | 2 +-
.../operations/definitions/last_value.test.tsx | 2 +-
.../operations/definitions/last_value.tsx | 8 ++++----
.../operations/layer_helpers.ts | 4 +---
.../plugins/lens/server/routes/field_stats.ts | 2 +-
.../translations/translations/ja-JP.json | 16 ----------------
.../translations/translations/zh-CN.json | 18 ------------------
17 files changed, 38 insertions(+), 75 deletions(-)
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 e052e06f1b2f1..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,7 +57,7 @@ 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/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index c34e3c4137368..b3b9345344116 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(', '),
@@ -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/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/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/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) {
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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 1b83bcac5837f..c2d46fa5762d2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -238,8 +238,6 @@
"xpack.lens.editorFrame.expressionMissingDatasource": "ビジュアライゼーションのデータソースが見つかりませんでした",
"xpack.lens.editorFrame.expressionMissingVisualizationType": "ビジュアライゼーションタイプが見つかりません。",
"xpack.lens.editorFrame.goToForums": "リクエストとフィードバック",
- "xpack.lens.editorFrame.indexPatternNotFound": "インデックスパターンが見つかりませんでした",
- "xpack.lens.editorFrame.indexPatternReconfigure": "インデックスパターン管理ページで再作成",
"xpack.lens.editorFrame.invisibleIndicatorLabel": "このディメンションは現在グラフに表示されません",
"xpack.lens.editorFrame.networkErrorMessage": "ネットワークエラーです。しばらくたってから再試行するか、管理者に連絡してください。",
"xpack.lens.editorFrame.noColorIndicatorLabel": "このディメンションには個別の色がありません",
@@ -366,7 +364,6 @@
"xpack.lens.indexPattern.cardinality": "ユニークカウント",
"xpack.lens.indexPattern.cardinality.signature": "フィールド:文字列",
"xpack.lens.indexPattern.cardinalityOf": "{name} のユニークカウント",
- "xpack.lens.indexPattern.changeIndexPatternTitle": "インデックスパターン",
"xpack.lens.indexPattern.chooseField": "フィールドを選択",
"xpack.lens.indexPattern.chooseFieldLabel": "この関数を使用するには、フィールドを選択してください。",
"xpack.lens.indexPattern.chooseSubFunction": "サブ関数を選択",
@@ -406,7 +403,6 @@
"xpack.lens.indexPattern.derivative": "差異",
"xpack.lens.indexPattern.derivativeOf": "{name} の差異",
"xpack.lens.indexPattern.differences.signature": "メトリック:数値",
- "xpack.lens.indexPattern.editFieldLabel": "インデックスパターンフィールドを編集",
"xpack.lens.indexPattern.emptyDimensionButton": "空のディメンション",
"xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド",
"xpack.lens.indexPattern.emptyFieldsLabelHelp": "空のフィールドには、フィルターに基づく最初の 500 件のドキュメントの値が含まれていませんでした。",
@@ -467,15 +463,12 @@
"xpack.lens.indexPattern.functionsLabel": "関数を選択",
"xpack.lens.indexPattern.groupByDropdown": "グループ分けの条件",
"xpack.lens.indexPattern.incompleteOperation": "(未完了)",
- "xpack.lens.indexPattern.indexPatternLoadError": "インデックスパターンの読み込み中にエラーが発生",
"xpack.lens.indexPattern.intervals": "間隔",
- "xpack.lens.indexPattern.invalidFieldLabel": "無効なフィールドです。インデックスパターンを確認するか、別のフィールドを選択してください。",
"xpack.lens.indexPattern.invalidInterval": "無効な間隔値",
"xpack.lens.indexPattern.invalidOperationLabel": "選択した関数はこのフィールドで動作しません。",
"xpack.lens.indexPattern.invalidReferenceConfiguration": "ディメンション\"{dimensionLabel}\"の構成が正しくありません",
"xpack.lens.indexPattern.invalidTimeShift": "無効な時間シフトです。正の整数の後に単位s、m、h、d、w、M、yのいずれかを入力します。例:3時間は3hです",
"xpack.lens.indexPattern.lastValue": "最終値",
- "xpack.lens.indexPattern.lastValue.disabled": "この関数には、インデックスの日付フィールドが必要です",
"xpack.lens.indexPattern.lastValue.invalidTypeSortField": "フィールド {invalidField} は日付フィールドではないため、並べ替えで使用できません",
"xpack.lens.indexPattern.lastValue.signature": "フィールド:文字列",
"xpack.lens.indexPattern.lastValue.sortField": "日付フィールドで並べ替え",
@@ -511,8 +504,6 @@
"xpack.lens.indexPattern.movingAverage.windowLimitations": "ウィンドウには現在の値が含まれません。",
"xpack.lens.indexPattern.movingAverageOf": "{name} の移動平均",
"xpack.lens.indexPattern.multipleDateHistogramsError": "\"{dimensionLabel}\"は唯一の日付ヒストグラムではありません。時間シフトを使用するときには、1つの日付ヒストグラムのみを使用していることを確認してください。",
- "xpack.lens.indexPattern.noPatternsDescription": "インデックスパターンを作成するか、別のデータソースに切り替えてください",
- "xpack.lens.indexPattern.noPatternsLabel": "インデックスパターンがありません",
"xpack.lens.indexPattern.numberFormatLabel": "数字",
"xpack.lens.indexPattern.ofDocumentsLabel": "ドキュメント",
"xpack.lens.indexPattern.otherDocsLabel": "その他",
@@ -556,7 +547,6 @@
"xpack.lens.indexPattern.referenceFunctionPlaceholder": "サブ関数",
"xpack.lens.indexPattern.removeColumnAriaLabel": "フィールドを追加するか、{groupLabel}までドラッグアンドドロップします",
"xpack.lens.indexPattern.removeColumnLabel": "「{groupLabel}」から構成を削除",
- "xpack.lens.indexPattern.removeFieldLabel": "インデックスパターンを削除",
"xpack.lens.indexPattern.sortField.invalid": "無効なフィールドです。インデックスパターンを確認するか、別のフィールドを選択してください。",
"xpack.lens.indexpattern.suggestions.nestingChangeLabel": "各 {outerOperation} の {innerOperation}",
"xpack.lens.indexpattern.suggestions.overallLabel": "全体の {operation}",
@@ -602,12 +592,8 @@
"xpack.lens.indexPattern.timeShiftSmallWarning": "{label}は{columnTimeShift}の時間シフトを使用しています。これは{interval}の日付ヒストグラム間隔よりも小さいです。不一致のデータを防止するには、時間シフトとして{interval}を使用します。",
"xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
"xpack.lens.indexPattern.useAsTopLevelAgg": "最初にこのフィールドでグループ化",
- "xpack.lens.indexPatterns.actionsPopoverLabel": "インデックスパターン設定",
- "xpack.lens.indexPatterns.addFieldButton": "フィールドをインデックスパターンに追加",
"xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去",
"xpack.lens.indexPatterns.fieldFiltersLabel": "タイプでフィルタリング",
- "xpack.lens.indexPatterns.filterByNameLabel": "検索フィールド名",
- "xpack.lens.indexPatterns.manageFieldButton": "インデックスパターンを管理",
"xpack.lens.indexPatterns.noAvailableDataLabel": "データを含むフィールドはありません。",
"xpack.lens.indexPatterns.noDataLabel": "フィールドがありません。",
"xpack.lens.indexPatterns.noEmptyDataLabel": "空のフィールドがありません。",
@@ -615,14 +601,12 @@
"xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet": "別のフィールドフィルターを使用",
"xpack.lens.indexPatterns.noFields.globalFiltersBullet": "グローバルフィルターを変更",
"xpack.lens.indexPatterns.noFields.tryText": "試行対象:",
- "xpack.lens.indexPatterns.noFieldsLabel": "このインデックスパターンにはフィールドがありません。",
"xpack.lens.indexPatterns.noFilteredFieldsLabel": "選択したフィルターと一致するフィールドはありません。",
"xpack.lens.indexPatterns.noMetaDataLabel": "メタフィールドがありません。",
"xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示",
"xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示",
"xpack.lens.labelInput.label": "ラベル",
"xpack.lens.layerPanel.layerVisualizationType": "レイヤービジュアライゼーションタイプ",
- "xpack.lens.layerPanel.missingIndexPattern": "インデックスパターンが見つかりませんでした",
"xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション",
"xpack.lens.metric.addLayer": "ビジュアライゼーションレイヤーを追加",
"xpack.lens.metric.groupLabel": "表形式の値と単一の値",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c29557eebd3f8..e3f53a34449ef 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -241,11 +241,8 @@
"xpack.lens.editorFrame.expressionFailureMessage": "请求错误:{type},{reason}",
"xpack.lens.editorFrame.expressionFailureMessageWithContext": "请求错误:{type},{context} 中的 {reason}",
"xpack.lens.editorFrame.expressionMissingDatasource": "无法找到可视化的数据源",
- "xpack.lens.editorFrame.expressionMissingIndexPattern": "找不到{count, plural, other {索引模式}}:{ids}",
"xpack.lens.editorFrame.expressionMissingVisualizationType": "找不到可视化类型。",
"xpack.lens.editorFrame.goToForums": "提出请求并提供反馈",
- "xpack.lens.editorFrame.indexPatternNotFound": "未找到索引模式",
- "xpack.lens.editorFrame.indexPatternReconfigure": "在索引模式管理页面中重新创建",
"xpack.lens.editorFrame.invisibleIndicatorLabel": "此维度当前在图表中不可见",
"xpack.lens.editorFrame.networkErrorMessage": "网络错误,请稍后重试或联系管理员。",
"xpack.lens.editorFrame.noColorIndicatorLabel": "此维度没有单独的颜色",
@@ -374,7 +371,6 @@
"xpack.lens.indexPattern.cardinality": "唯一计数",
"xpack.lens.indexPattern.cardinality.signature": "field: string",
"xpack.lens.indexPattern.cardinalityOf": "{name} 的唯一计数",
- "xpack.lens.indexPattern.changeIndexPatternTitle": "索引模式",
"xpack.lens.indexPattern.chooseField": "选择字段",
"xpack.lens.indexPattern.chooseFieldLabel": "要使用此函数,请选择字段。",
"xpack.lens.indexPattern.chooseSubFunction": "选择子函数",
@@ -414,7 +410,6 @@
"xpack.lens.indexPattern.derivative": "差异",
"xpack.lens.indexPattern.derivativeOf": "{name} 的差异",
"xpack.lens.indexPattern.differences.signature": "指标:数字",
- "xpack.lens.indexPattern.editFieldLabel": "编辑索引模式字段",
"xpack.lens.indexPattern.emptyDimensionButton": "空维度",
"xpack.lens.indexPattern.emptyFieldsLabel": "空字段",
"xpack.lens.indexPattern.emptyFieldsLabelHelp": "空字段在基于您的筛选的前 500 个文档中不包含任何值。",
@@ -476,15 +471,12 @@
"xpack.lens.indexPattern.functionsLabel": "选择函数",
"xpack.lens.indexPattern.groupByDropdown": "分组依据",
"xpack.lens.indexPattern.incompleteOperation": "(不完整)",
- "xpack.lens.indexPattern.indexPatternLoadError": "加载索引模式时出错",
"xpack.lens.indexPattern.intervals": "时间间隔",
- "xpack.lens.indexPattern.invalidFieldLabel": "字段无效。检查索引模式或选取其他字段。",
"xpack.lens.indexPattern.invalidInterval": "时间间隔值无效",
"xpack.lens.indexPattern.invalidOperationLabel": "此字段不适用于选定函数。",
"xpack.lens.indexPattern.invalidReferenceConfiguration": "维度“{dimensionLabel}”配置不正确",
"xpack.lens.indexPattern.invalidTimeShift": "时间偏移无效。输入正整数数量,后跟以下单位之一:s、m、h、d、w、M、y。例如,3h 表示 3 小时",
"xpack.lens.indexPattern.lastValue": "最后值",
- "xpack.lens.indexPattern.lastValue.disabled": "此功能要求索引中存在日期字段",
"xpack.lens.indexPattern.lastValue.invalidTypeSortField": "字段 {invalidField} 不是日期字段,不能用于排序",
"xpack.lens.indexPattern.lastValue.signature": "field: string",
"xpack.lens.indexPattern.lastValue.sortField": "按日期字段排序",
@@ -504,7 +496,6 @@
"xpack.lens.indexPattern.min.description": "单值指标聚合,返回从聚合文档提取的数值中的最小值。",
"xpack.lens.indexPattern.minOf": "{name} 的最小值",
"xpack.lens.indexPattern.missingFieldLabel": "缺失字段",
- "xpack.lens.indexPattern.missingIndexPattern": "找不到{count, plural, other {索引模式}} ({count, plural, other {id}}:{indexpatterns})",
"xpack.lens.indexPattern.missingReferenceError": "“{dimensionLabel}”配置不完整",
"xpack.lens.indexPattern.moveToWorkspace": "将 {field} 添加到工作区",
"xpack.lens.indexPattern.moveToWorkspaceDisabled": "此字段无法自动添加到工作区。您仍可以在配置面板中直接使用它。",
@@ -521,8 +512,6 @@
"xpack.lens.indexPattern.movingAverage.windowLimitations": "时间窗不包括当前值。",
"xpack.lens.indexPattern.movingAverageOf": "{name} 的移动平均值",
"xpack.lens.indexPattern.multipleDateHistogramsError": "“{dimensionLabel}”不是唯一的 Date Histogram。使用时间偏移时,请确保仅使用一个 Date Histogram。",
- "xpack.lens.indexPattern.noPatternsDescription": "请创建索引模式或切换到其他数据源",
- "xpack.lens.indexPattern.noPatternsLabel": "无索引模式",
"xpack.lens.indexPattern.numberFormatLabel": "数字",
"xpack.lens.indexPattern.ofDocumentsLabel": "文档",
"xpack.lens.indexPattern.operationsNotFound": "未找到{operationLength, plural, other {运算}} {operationsList}",
@@ -567,7 +556,6 @@
"xpack.lens.indexPattern.referenceFunctionPlaceholder": "子函数",
"xpack.lens.indexPattern.removeColumnAriaLabel": "将字段添加或拖放到 {groupLabel}",
"xpack.lens.indexPattern.removeColumnLabel": "从“{groupLabel}”中删除配置",
- "xpack.lens.indexPattern.removeFieldLabel": "移除索引模式字段",
"xpack.lens.indexPattern.sortField.invalid": "字段无效。检查索引模式或选取其他字段。",
"xpack.lens.indexpattern.suggestions.nestingChangeLabel": "每个 {outerOperation} 的 {innerOperation}",
"xpack.lens.indexpattern.suggestions.overallLabel": "总体 {operation}",
@@ -613,13 +601,9 @@
"xpack.lens.indexPattern.timeShiftSmallWarning": "{label} 使用的时间偏移 {columnTimeShift} 小于 Date Histogram 时间间隔 {interval} 。要防止数据不匹配,请使用 {interval} 的倍数作为时间偏移。",
"xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
"xpack.lens.indexPattern.useAsTopLevelAgg": "先按此字段分组",
- "xpack.lens.indexPatterns.actionsPopoverLabel": "索引模式设置",
- "xpack.lens.indexPatterns.addFieldButton": "将字段添加到索引模式",
"xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选",
"xpack.lens.indexPatterns.fieldFiltersLabel": "按类型筛选",
"xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} 个可用{availableFields, plural, other {字段}}。{emptyFields} 个空{emptyFields, plural, other {字段}}。{metaFields} 个元{metaFields, plural,other {字段}}。",
- "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段名称",
- "xpack.lens.indexPatterns.manageFieldButton": "管理索引模式字段",
"xpack.lens.indexPatterns.noAvailableDataLabel": "没有包含数据的可用字段。",
"xpack.lens.indexPatterns.noDataLabel": "无字段。",
"xpack.lens.indexPatterns.noEmptyDataLabel": "无空字段。",
@@ -627,14 +611,12 @@
"xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet": "使用不同的字段筛选",
"xpack.lens.indexPatterns.noFields.globalFiltersBullet": "更改全局筛选",
"xpack.lens.indexPatterns.noFields.tryText": "尝试:",
- "xpack.lens.indexPatterns.noFieldsLabel": "在此索引模式中不存在任何字段。",
"xpack.lens.indexPatterns.noFilteredFieldsLabel": "没有字段匹配选定筛选。",
"xpack.lens.indexPatterns.noMetaDataLabel": "无元字段。",
"xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}",
"xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}",
"xpack.lens.labelInput.label": "标签",
"xpack.lens.layerPanel.layerVisualizationType": "图层可视化类型",
- "xpack.lens.layerPanel.missingIndexPattern": "未找到索引模式",
"xpack.lens.lensSavedObjectLabel": "Lens 可视化",
"xpack.lens.metric.addLayer": "添加可视化图层",
"xpack.lens.metric.groupLabel": "表和单值",
From 9b7b3228ecc8050a021d2d0b1c5d8b5952e00871 Mon Sep 17 00:00:00 2001
From: Uladzislau Lasitsa
Date: Mon, 4 Oct 2021 12:55:29 +0300
Subject: [PATCH 25/98] [TSVB] Removes less support from markdown editor
(#110985)
* Remove less from markdown
* Fix migration script
* Fix migration script
* fix lint
* Fix test
* Fix comments
* Fix lint
* Fix comments
* Fix yarn.lock file
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/user/dashboard/tsvb.asciidoc | 2 +-
package.json | 1 -
.../components/panel_config/markdown.tsx | 29 +--
.../components/vis_types/markdown/vis.js | 38 ++--
.../visualize_embeddable_factory.ts | 9 +
.../visualization_common_migrations.ts | 29 +++
...ualization_saved_object_migrations.test.ts | 32 +++
.../visualization_saved_object_migrations.ts | 25 +++
.../api_integration/apis/maps/migrations.js | 2 +-
yarn.lock | 185 +-----------------
10 files changed, 123 insertions(+), 229 deletions(-)
diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc
index 80138990c4f6e..0537677ee3ad9 100644
--- a/docs/user/dashboard/tsvb.asciidoc
+++ b/docs/user/dashboard/tsvb.asciidoc
@@ -122,7 +122,7 @@ To change this behavior, click *Panel options*, then specify a URL in the *Item
[[tsvb-markdown]]
===== Markdown
-The *Markdown* visualization supports Markdown with Handlebar (mustache) syntax to insert dynamic data, and supports custom CSS using the LESS syntax.
+The *Markdown* visualization supports Markdown with Handlebar (mustache) syntax to insert dynamic data, and supports custom CSS.
[float]
[[tsvb-function-reference]]
diff --git a/package.json b/package.json
index c12e5ca9f060e..ac30b5de6f486 100644
--- a/package.json
+++ b/package.json
@@ -275,7 +275,6 @@
"jsonwebtoken": "^8.5.1",
"jsts": "^1.6.2",
"kea": "^2.4.2",
- "less": "npm:@elastic/less@2.7.3-kibana",
"load-json-file": "^6.2.0",
"loader-utils": "^1.2.3",
"lodash": "^4.17.21",
diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx
index b099209af4348..c5d412f823d0f 100644
--- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx
+++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx
@@ -21,12 +21,8 @@ import {
EuiTitle,
EuiHorizontalRule,
} from '@elastic/eui';
-// @ts-expect-error
-import less from 'less/lib/less-browser';
-import 'brace/mode/less';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import type { Writable } from '@kbn/utility-types';
// @ts-expect-error not typed yet
import { SeriesEditor } from '../series_editor';
@@ -41,11 +37,8 @@ import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { VisDataContext } from '../../contexts/vis_data_context';
import { PanelConfigProps, PANEL_CONFIG_TABS } from './types';
-import { TimeseriesVisParams } from '../../../types';
import { CodeEditor, CssLang } from '../../../../../../kibana_react/public';
-const lessC = less(window, { env: 'production' });
-
export class MarkdownPanelConfig extends Component<
PanelConfigProps,
{ selectedTab: PANEL_CONFIG_TABS }
@@ -61,21 +54,7 @@ export class MarkdownPanelConfig extends Component<
}
handleCSSChange(value: string) {
- const { model } = this.props;
- const lessSrc = `#markdown-${model.id} {${value}}`;
- lessC.render(
- lessSrc,
- { compress: true, javascriptEnabled: false },
- (e: unknown, output: any) => {
- const parts: Writable> = {
- markdown_less: value,
- };
- if (output) {
- parts.markdown_css = output.css;
- }
- this.props.onChange(parts);
- }
- );
+ this.props.onChange({ markdown_css: value });
}
render() {
@@ -275,8 +254,8 @@ export class MarkdownPanelConfig extends Component<
@@ -285,7 +264,7 @@ export class MarkdownPanelConfig extends Component<
height="500px"
languageId={CssLang.ID}
options={{ fontSize: 14 }}
- value={model.markdown_less ?? ''}
+ value={model.markdown_css ?? ''}
onChange={this.handleCSSChange}
/>
diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js
index 49fdbcd98501c..17a293627b314 100644
--- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js
+++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js
@@ -9,8 +9,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
-import uuid from 'uuid';
import { get } from 'lodash';
+import { ClassNames } from '@emotion/react';
import { Markdown } from '../../../../../../../../plugins/kibana_react/public';
import { ErrorComponent } from '../../error';
@@ -18,19 +18,15 @@ import { replaceVars } from '../../lib/replace_vars';
import { convertSeriesToVars } from '../../lib/convert_series_to_vars';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
-const getMarkdownId = (id) => `markdown-${id}`;
-
function MarkdownVisualization(props) {
const { backgroundColor, model, visData, getConfig, fieldFormatMap } = props;
const series = get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model, getConfig, fieldFormatMap);
- const markdownElementId = getMarkdownId(uuid.v1());
const panelBackgroundColor = model.background_color || backgroundColor;
const style = { backgroundColor: panelBackgroundColor };
let markdown;
- let markdownCss = '';
if (model.markdown) {
const markdownSource = replaceVars(
@@ -42,13 +38,6 @@ function MarkdownVisualization(props) {
}
);
- if (model.markdown_css) {
- markdownCss = model.markdown_css.replace(
- new RegExp(getMarkdownId(model.id), 'g'),
- markdownElementId
- );
- }
-
const markdownClasses = classNames('kbnMarkdown__body', {
'kbnMarkdown__body--reversed': isBackgroundInverted(panelBackgroundColor),
});
@@ -65,17 +54,20 @@ function MarkdownVisualization(props) {
markdown = (
{markdownError &&
}
-
-
-
- {!markdownError && (
-
- )}
-
-
+
+ {({ css, cx }) => (
+
+
+ {!markdownError && (
+
+ )}
+
+
+ )}
+
);
}
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/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js
index 37dc9c32958ff..47747467ae550 100644
--- a/x-pack/test/api_integration/apis/maps/migrations.js
+++ b/x-pack/test/api_integration/apis/maps/migrations.js
@@ -76,7 +76,7 @@ export default function ({ getService }) {
}
expect(panels.length).to.be(1);
expect(panels[0].type).to.be('map');
- expect(panels[0].version).to.be('7.16.0');
+ expect(panels[0].version).to.be('8.0.0');
});
});
});
diff --git a/yarn.lock b/yarn.lock
index 6d491e4b8ba49..14cf34cae847b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7780,14 +7780,6 @@ ajv-keywords@^3.5.2:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
-ajv@^4.9.1:
- version "4.11.8"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
- integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.5.5:
version "6.12.4"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
@@ -8402,11 +8394,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
-assert-plus@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
- integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ=
-
assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
@@ -8627,21 +8614,11 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
dependencies:
array-filter "^1.0.0"
-aws-sign2@~0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
- integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8=
-
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
-aws4@^1.2.1:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
- integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
-
aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
@@ -9438,13 +9415,6 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
-boom@2.x.x:
- version "2.10.1"
- resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
- integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=
- dependencies:
- hoek "2.x.x"
-
bottleneck@^2.15.3:
version "2.18.0"
resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.18.0.tgz#41fa63ae185b65435d789d1700334bc48222dacf"
@@ -10907,7 +10877,7 @@ combine-source-map@^0.8.0, combine-source-map@~0.8.0:
lodash.memoize "~3.0.3"
source-map "~0.5.3"
-combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.5, combined-stream@~1.0.6:
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -11518,13 +11488,6 @@ crypt@~0.0.1:
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
-cryptiles@2.x.x:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
- integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=
- dependencies:
- boom "2.x.x"
-
crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -13696,7 +13659,7 @@ err-code@^2.0.2:
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
-errno@^0.1.1, errno@^0.1.3, errno@~0.1.7:
+errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
@@ -14603,7 +14566,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
-extend@^3.0.0, extend@~3.0.0, extend@~3.0.2:
+extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -15316,15 +15279,6 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
-form-data@~2.1.1:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
- integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.5"
- mime-types "^2.1.12"
-
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -16329,24 +16283,11 @@ handlebars@4.7.7, handlebars@^4.7.7:
optionalDependencies:
uglify-js "^3.1.4"
-har-schema@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
- integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=
-
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
-har-validator@~4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
- integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio=
- dependencies:
- ajv "^4.9.1"
- har-schema "^1.0.5"
-
har-validator@~5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
@@ -16625,16 +16566,6 @@ hat@0.0.3:
resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=
-hawk@~3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
- integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=
- dependencies:
- boom "2.x.x"
- cryptiles "2.x.x"
- hoek "2.x.x"
- sntp "1.x.x"
-
hdr-histogram-js@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-1.2.0.tgz#1213c0b317f39b9c05bc4f208cb7931dbbc192ae"
@@ -16694,11 +16625,6 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
-hoek@2.x.x:
- version "2.16.3"
- resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
- integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
-
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -16982,15 +16908,6 @@ http-proxy@^1.17.0, http-proxy@^1.18.1:
follow-redirects "^1.0.0"
requires-port "^1.0.0"
-http-signature@~1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
- integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=
- dependencies:
- assert-plus "^0.2.0"
- jsprim "^1.2.2"
- sshpk "^1.7.0"
-
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -17143,11 +17060,6 @@ image-size@^0.8.2:
dependencies:
queue "6.0.1"
-image-size@~0.5.0:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
- integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
-
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@@ -19545,20 +19457,6 @@ lead@^1.0.0:
dependencies:
flush-write-stream "^1.0.2"
-"less@npm:@elastic/less@2.7.3-kibana":
- version "2.7.3-kibana"
- resolved "https://registry.yarnpkg.com/@elastic/less/-/less-2.7.3-kibana.tgz#3de5e0b06bb095b1cc1149043d67f8dc36272d23"
- integrity sha512-Okm31ZKE28/m3bH0h0mNpQH0zqVWNFqRKDlsBd1AYHGdM1yBq4mzeO6IRUykB81XDGlqL0m4ThSA7mc3hy+LVg==
- optionalDependencies:
- errno "^0.1.1"
- graceful-fs "^4.1.2"
- image-size "~0.5.0"
- mime "^1.2.11"
- mkdirp "^0.5.0"
- promise "^7.1.1"
- request "2.81.0"
- source-map "^0.5.3"
-
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -20746,11 +20644,6 @@ mime-db@1.44.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
-mime-db@1.45.0:
- version "1.45.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
- integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
-
mime-db@^1.28.0:
version "1.49.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
@@ -20763,14 +20656,7 @@ mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, m
dependencies:
mime-db "1.44.0"
-mime-types@~2.1.7:
- version "2.1.28"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
- integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
- dependencies:
- mime-db "1.45.0"
-
-mime@1.6.0, mime@^1.2.11, mime@^1.3.4, mime@^1.4.1:
+mime@1.6.0, mime@^1.3.4, mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -21935,11 +21821,6 @@ nyc@^15.0.1:
test-exclude "^6.0.0"
yargs "^15.0.2"
-oauth-sign@~0.8.1:
- version "0.8.2"
- resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
- integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=
-
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@@ -24027,7 +23908,7 @@ punycode@1.3.2:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
-punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
+punycode@^1.2.4, punycode@^1.3.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
@@ -24109,11 +23990,6 @@ qs@^6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
-qs@~6.4.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
- integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=
-
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -25757,34 +25633,6 @@ request-promise@^4.2.2:
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
-request@2.81.0:
- version "2.81.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
- integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=
- dependencies:
- aws-sign2 "~0.6.0"
- aws4 "^1.2.1"
- caseless "~0.12.0"
- combined-stream "~1.0.5"
- extend "~3.0.0"
- forever-agent "~0.6.1"
- form-data "~2.1.1"
- har-validator "~4.2.1"
- hawk "~3.1.3"
- http-signature "~1.1.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.7"
- oauth-sign "~0.8.1"
- performance-now "^0.2.0"
- qs "~6.4.0"
- safe-buffer "^5.0.1"
- stringstream "~0.0.4"
- tough-cookie "~2.3.0"
- tunnel-agent "^0.6.0"
- uuid "^3.0.0"
-
request@^2.44.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@@ -26886,13 +26734,6 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^2.0.0"
-sntp@1.x.x:
- version "1.0.9"
- resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
- integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=
- dependencies:
- hoek "2.x.x"
-
sockjs-client@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
@@ -27046,7 +26887,7 @@ source-map@^0.4.2:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
+source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -27675,11 +27516,6 @@ stringify-entities@^3.0.1:
is-decimal "^1.0.2"
is-hexadecimal "^1.0.0"
-stringstream@~0.0.4:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
- integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
-
strip-ansi@*, strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
@@ -28824,13 +28660,6 @@ tough-cookie@^4.0.0:
punycode "^2.1.1"
universalify "^0.1.2"
-tough-cookie@~2.3.0:
- version "2.3.4"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
- integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==
- dependencies:
- punycode "^1.4.1"
-
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -29904,7 +29733,7 @@ uuid@^2.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
-uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
+uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
From 8e25f5cc0fcb87abb73602176cb2f4723123b503 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 4 Oct 2021 13:01:14 +0200
Subject: [PATCH 26/98] [Lens] Threshold: add padding to avoid axis label
collision with threshold markers (#112952)
* :bug: Add padding to the tick label to fit threshold markers
* :bug: Better icon detection
* :bug: Fix edge cases with no title or labels
* :camera_flash: Update snapshots
* :sparkles: Add icon placement flag
* :sparkles: Sync padding computation with marker positioning
* :ok_hand: Make disabled when no icon is selected
* :bug: Fix some edge cases with auto positioning
* Update x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
Co-authored-by: Michael Marcialis
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Michael Marcialis
---
.../expressions/xy_chart/axis_config.ts | 7 +
.../shared_components/tooltip_wrapper.tsx | 1 +
.../__snapshots__/expression.test.tsx.snap | 35 +++++
.../public/xy_visualization/expression.tsx | 65 +++++++--
.../expression_thresholds.tsx | 131 +++++++++++++++++-
.../public/xy_visualization/to_expression.ts | 1 +
.../xy_config_panel/index.tsx | 2 +-
.../xy_config_panel/threshold_panel.tsx | 115 ++++++++++++++-
8 files changed, 340 insertions(+), 17 deletions(-)
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/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/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/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index c4315b7ccea85..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 {
@@ -314,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
@@ -330,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;
@@ -510,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={{
@@ -545,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,
},
}}
/>
@@ -568,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)}
/>
);
@@ -839,10 +881,15 @@ export function XYChart({
syncColors={syncColors}
paletteService={paletteService}
formatters={{
- left: yAxesConfiguration.find(({ groupId }) => 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 d9c0b36702639..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,25 +8,144 @@
import React from 'react';
import { groupBy } from 'lodash';
import { EuiIcon } from '@elastic/eui';
-import { RectAnnotation, AnnotationDomainType, LineAnnotation } from '@elastic/charts';
+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 { euiLightVars } from '@kbn/ui-shared-deps-src/theme';
-import type { LayerArgs } from '../../common/expressions';
+import type { LayerArgs, YConfig } from '../../common/expressions';
import type { LensMultiTable } from '../../common/types';
+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,
formatters,
paletteService,
syncColors,
+ axesMap,
+ isHorizontal,
}: {
thresholdLayers: LayerArgs[];
data: LensMultiTable;
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
paletteService: PaletteRegistry;
syncColors: boolean;
+ axesMap: Record<'left' | 'right', boolean>;
+ isHorizontal: boolean;
}) => {
return (
<>
@@ -63,7 +182,13 @@ export const ThresholdAnnotations = ({
const props = {
groupId,
- marker: yConfig.icon ? : undefined,
+ marker: hasIcon(yConfig.icon) ? : undefined,
+ markerPosition: getIconPlacement(
+ yConfig.iconPosition,
+ yConfig.axisMode,
+ axesMap,
+ isHorizontal
+ ),
};
const annotations = [];
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 b66f0ca4687b8..2fce7c6a612ae 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -340,6 +340,7 @@ export const buildExpression = (
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/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 087eee9005c06..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
@@ -14,11 +14,11 @@ import type { VisualizationDimensionEditorProps } 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,13 +109,82 @@ 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 { state, setState, layerId, accessor, isHorizontal } = props;
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({
value: state,
@@ -265,7 +334,7 @@ export const ThresholdPanel = (
@@ -276,6 +345,44 @@ export const ThresholdPanel = (
}}
/>
+
+
+ {
+ const newMode = id.replace(idPrefix, '') as IconPosition;
+ setYConfig({ forAccessor: accessor, iconPosition: newMode });
+ }}
+ />
+
+
>
);
};
From 6bfa2a4c2ce4a32fa6857cfb5b3393b2cf89da09 Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Mon, 4 Oct 2021 13:32:36 +0200
Subject: [PATCH 27/98] [Lens] move from slice to reducers/actions and simplify
loading (#113324)
* structure changes
* tests & fix for sessionId
* share mocks in time_range_middleware
* make switchVisualization and selectSuggestion one reducer as it's very similar
* CR
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../lens/public/app_plugin/app.test.tsx | 48 +-
.../config_panel/config_panel.test.tsx | 129 ++----
.../config_panel/layer_actions.test.ts | 10 +-
.../editor_frame/editor_frame.test.tsx | 32 +-
.../editor_frame/editor_frame.tsx | 2 +-
.../editor_frame/suggestion_helpers.test.ts | 60 +--
.../editor_frame/suggestion_helpers.ts | 29 +-
.../editor_frame/suggestion_panel.test.tsx | 13 +-
.../workspace_panel/chart_switch.test.tsx | 62 ++-
.../workspace_panel/chart_switch.tsx | 2 +-
.../workspace_panel/workspace_panel.test.tsx | 63 +--
.../workspace_panel/workspace_panel.tsx | 2 +-
x-pack/plugins/lens/public/mocks.tsx | 114 +++--
.../__snapshots__/load_initial.test.tsx.snap | 4 +-
.../lens/public/state_management/index.ts | 14 +-
.../init_middleware/load_initial.test.tsx | 410 ------------------
.../init_middleware/load_initial.ts | 119 ++---
.../state_management/lens_slice.test.ts | 26 +-
.../public/state_management/lens_slice.ts | 276 ++++++++----
.../state_management/load_initial.test.tsx | 323 ++++++++++++++
.../time_range_middleware.test.ts | 115 +----
21 files changed, 845 insertions(+), 1008 deletions(-)
rename x-pack/plugins/lens/public/state_management/{init_middleware => }/__snapshots__/load_initial.test.tsx.snap (94%)
delete mode 100644 x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx
create mode 100644 x-pack/plugins/lens/public/state_management/load_initial.test.tsx
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/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/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/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 4be9de78dedce..d289b69f4105e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -146,7 +146,6 @@ describe('editor_frame', () => {
};
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/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 b3b9345344116..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
@@ -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]);
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index 402440f3302f6..cb9ab11998105 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -39,7 +39,12 @@ import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/p
import type { LensAttributeService } from './lens_attribute_service';
import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public';
-import { makeConfigureStore, LensAppState, LensState } from './state_management/index';
+import {
+ makeConfigureStore,
+ LensAppState,
+ LensState,
+ LensStoreDeps,
+} from './state_management/index';
import { getResolvedDateRange } from './utils';
import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
import {
@@ -48,6 +53,8 @@ import {
Visualization,
FramePublicAPI,
FrameDatasourceAPI,
+ DatasourceMap,
+ VisualizationMap,
} from './types';
export function mockDatasourceStates() {
@@ -59,7 +66,7 @@ export function mockDatasourceStates() {
};
}
-export function createMockVisualization(id = 'vis1'): jest.Mocked {
+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/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/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/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',
From 36ce6bda672c55551a175888fac0cf5131f5fd7f Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 4 Oct 2021 07:48:24 -0400
Subject: [PATCH 28/98] [Security Solution][Endpoint] Show list of trusted
application on the Policy Details (#112182)
* New Artifact Collapsible card and Grid generic components
* Fleet setup test data loader - ignore 409 concurrent installs in data loader for fleet setup
* Adds `ContextMenuWithRouterSupport` prop for `maxWidth` and `truncateText` prop for `ContextMenuItemNaByRouter`
* trustedApps generator loader - use existing policies (if any) when loading TAs
* `CardCompressedHeaderLayout` support for `flushTop` prop
---
.../data_loaders/setup_fleet_for_endpoint.ts | 13 +-
.../common/endpoint/types/trusted_apps.ts | 2 +-
.../actions_context_menu.tsx | 2 +-
.../artifact_card_grid.test.tsx | 123 +++++++++++
.../artifact_card_grid/artifact_card_grid.tsx | 143 +++++++++++++
.../components/grid_header.tsx | 71 +++++++
.../components/artifact_card_grid/index.ts | 8 +
.../artifact_entry_card.test.tsx | 2 +-
.../artifact_entry_card.tsx | 72 ++-----
.../artifact_entry_collapsible_card.tsx | 65 ++++++
.../components/card_actions_flex_item.tsx | 26 +++
.../components/card_compressed_header.tsx | 183 +++++++++++++++++
.../components/card_container_panel.tsx | 32 +++
.../components/card_expand_button.tsx | 29 +++
.../components/card_header.tsx | 18 +-
.../components/card_section_panel.tsx | 19 ++
.../components/card_sub_header.tsx | 2 +-
.../components/effect_scope.tsx | 8 +-
.../components/text_value_display.tsx | 18 +-
.../components/translations.ts | 14 ++
.../hooks/use_collapsed_css_class_names.ts | 21 ++
.../hooks/use_normalized_artifact.ts | 43 +---
.../hooks/use_policy_nav_links.ts | 33 +++
.../components/artifact_entry_card/index.ts | 4 +
.../components/artifact_entry_card/types.ts | 5 +
.../artifact_entry_card/utils/index.ts | 9 +
.../utils/is_trusted_app.ts | 17 ++
.../utils/map_to_artifact_info.ts | 40 ++++
...sx => context_menu_item_nav_by_router.tsx} | 22 +-
.../context_menu_with_router_support.tsx | 30 ++-
.../view/components/table_row_actions.tsx | 2 +-
.../view/details/components/actions_menu.tsx | 2 +-
.../view/hooks/use_endpoint_action_items.tsx | 2 +-
.../action/policy_trusted_apps_action.ts | 28 ++-
.../store/policy_details/middleware/index.ts | 13 +-
.../middleware/policy_settings_middleware.ts | 2 +-
.../policy_trusted_apps_middleware.ts | 183 ++++++++++++++---
.../reducer/initial_policy_details_state.ts | 2 +
.../reducer/trusted_apps_reducer.test.ts | 12 +-
.../reducer/trusted_apps_reducer.ts | 37 ++++
.../store/policy_details/selectors/index.ts | 1 +
.../selectors/policy_common_selectors.ts | 50 +++++
.../selectors/policy_settings_selectors.ts | 16 +-
.../selectors/trusted_apps_selectors.test.ts | 9 +-
.../selectors/trusted_apps_selectors.ts | 118 +++++++++--
.../pages/policy/test_utils/index.ts | 4 +-
.../public/management/pages/policy/types.ts | 64 +++---
.../policy_artifacts_assignable_list.tsx | 4 +-
.../pages/policy/view/tabs/policy_tabs.tsx | 8 +-
.../pages/policy/view/trusted_apps/index.ts | 8 +
.../layout/policy_trusted_apps_layout.tsx | 4 +-
.../list/policy_trusted_apps_list.tsx | 192 ++++++++++++++++++
.../pages/trusted_apps/service/index.ts | 6 +-
.../__snapshots__/index.test.tsx.snap | 120 +++++------
.../view/trusted_apps_page.test.tsx | 8 +-
.../scripts/endpoint/trusted_apps/index.ts | 49 +++--
.../endpoint/routes/trusted_apps/service.ts | 4 +-
57 files changed, 1695 insertions(+), 327 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_card_grid/index.ts
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_actions_flex_item.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_container_panel.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_expand_button.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_collapsed_css_class_names.ts
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_policy_nav_links.ts
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/index.ts
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/is_trusted_app.ts
create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts
rename x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/{context_menu_item_nav_by_rotuer.tsx => context_menu_item_nav_by_router.tsx} (65%)
create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts
create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts
create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
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/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) => ArtifactCardGridCardComponentProps;
+};
+
+export const ArtifactCardGrid = memo(
+ ({
+ items: _items,
+ cardComponentProps,
+ onPageChange,
+ onExpandCollapse,
+ 'data-test-subj': dataTestSubj,
+ ...paginatedContentProps
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ const items = _items as AnyArtifact[];
+
+ // The list of card props that the caller can define
+ type PartialCardProps = Map;
+ const callerDefinedCardProps = useMemo(() => {
+ const cardProps: PartialCardProps = new Map();
+
+ for (const artifact of items) {
+ cardProps.set(artifact, cardComponentProps ? cardComponentProps(artifact) : {});
+ }
+
+ return cardProps;
+ }, [cardComponentProps, items]);
+
+ // Handling of what is expanded or collapsed is done by looking at the at what the caller card's
+ // `expanded` prop value was and then invert it for the card that the user clicked expand/collapse
+ const handleCardExpandCollapse = useCallback(
+ (item: AnyArtifact) => {
+ const expanded = [];
+ const collapsed = [];
+
+ for (const [artifact, currentCardProps] of callerDefinedCardProps) {
+ const currentExpandedState = Boolean(currentCardProps.expanded);
+ const newExpandedState = artifact === item ? !currentExpandedState : currentExpandedState;
+
+ if (newExpandedState) {
+ expanded.push(artifact);
+ } else {
+ collapsed.push(artifact);
+ }
+ }
+
+ onExpandCollapse({ expanded, collapsed });
+ },
+ [callerDefinedCardProps, onExpandCollapse]
+ );
+
+ // Full list of card props that includes the actual artifact and the callbacks
+ type FullCardProps = Map;
+ const fullCardProps = useMemo(() => {
+ const newFullCardProps: FullCardProps = new Map();
+
+ for (const [artifact, cardProps] of callerDefinedCardProps) {
+ newFullCardProps.set(artifact, {
+ ...cardProps,
+ item: artifact,
+ onExpandCollapse: () => handleCardExpandCollapse(artifact),
+ 'data-test-subj': cardProps['data-test-subj'] ?? getTestId('card'),
+ });
+ }
+
+ return newFullCardProps;
+ }, [callerDefinedCardProps, getTestId, handleCardExpandCollapse]);
+
+ const handleItemComponentProps = useCallback(
+ (item: AnyArtifact): ArtifactEntryCollapsibleCardProps => {
+ return fullCardProps.get(item)!;
+ },
+ [fullCardProps]
+ );
+
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+);
+ArtifactCardGrid.displayName = 'ArtifactCardGrid';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx
new file mode 100644
index 0000000000000..03fde724b89a5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx
@@ -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 React, { memo, useMemo } from 'react';
+import { CommonProps, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import styled from 'styled-components';
+import { CardCompressedHeaderLayout, CardSectionPanel } from '../../artifact_entry_card';
+import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
+
+const GridHeaderContainer = styled(CardSectionPanel)`
+ padding-top: 0;
+ padding-bottom: ${({ theme }) => theme.eui.paddingSizes.s};
+`;
+
+export type GridHeaderProps = Pick;
+export const GridHeader = memo(({ 'data-test-subj': dataTestSubj }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ const expandToggleElement = useMemo(
+ () =>
,
+ [getTestId]
+ );
+
+ return (
+
+
+
+
+
+
+ }
+ description={
+
+
+
+
+
+ }
+ effectScope={
+
+
+
+
+
+ }
+ actionMenu={false}
+ />
+
+ );
+});
+GridHeader.displayName = 'GridHeader';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/index.ts b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/index.ts
new file mode 100644
index 0000000000000..3c29df28a82df
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/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 * from './artifact_card_grid';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
index 31e49aef0ac19..52f0eb5fc8982 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
@@ -10,7 +10,7 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../common
import { ArtifactEntryCard, ArtifactEntryCardProps } from './artifact_entry_card';
import { act, fireEvent, getByTestId } from '@testing-library/react';
import { AnyArtifact } from './types';
-import { isTrustedApp } from './hooks/use_normalized_artifact';
+import { isTrustedApp } from './utils';
import { getTrustedAppProvider, getExceptionProvider } from './test_utils';
describe.each([
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx
index 4adb81411395a..d9709c64e092d 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.tsx
@@ -5,27 +5,22 @@
* 2.0.
*/
-import React, { memo, useMemo } from 'react';
-import { CommonProps, EuiHorizontalRule, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
-import styled from 'styled-components';
+import React, { memo } from 'react';
+import { CommonProps, EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui';
import { CardHeader, CardHeaderProps } from './components/card_header';
import { CardSubHeader } from './components/card_sub_header';
import { getEmptyValue } from '../../../common/components/empty_value';
import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
-import { EffectScopeProps } from './components/effect_scope';
-import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_rotuer';
-import { AnyArtifact } from './types';
+import { AnyArtifact, MenuItemPropsByPolicyId } from './types';
import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
-
-const CardContainerPanel = styled(EuiPanel)`
- &.artifactEntryCard + &.artifactEntryCard {
- margin-top: ${({ theme }) => theme.eui.spacerSizes.l};
- }
-`;
+import { CardContainerPanel } from './components/card_container_panel';
+import { CardSectionPanel } from './components/card_section_panel';
+import { usePolicyNavLinks } from './hooks/use_policy_nav_links';
+import { MaybeImmutable } from '../../../../common/endpoint/types';
export interface ArtifactEntryCardProps extends CommonProps {
- item: AnyArtifact;
+ item: MaybeImmutable;
/**
* The list of actions for the card. Will display an icon with the actions in a menu if defined.
*/
@@ -33,52 +28,25 @@ export interface ArtifactEntryCardProps extends CommonProps {
/**
* Information about the policies that are assigned to the `item`'s `effectScope` and that will be
- * use to create a navigation link
+ * used to create the items in the popup context menu. This is a
+ * `Record`.
*/
- policies?: {
- [policyId: string]: ContextMenuItemNavByRouterProps;
- };
+ policies?: MenuItemPropsByPolicyId;
}
/**
* Display Artifact Items (ex. Trusted App, Event Filter, etc) as a card.
* This component is a TS Generic that allows you to set what the Item type is
*/
-export const ArtifactEntryCard = memo(
- ({
- item,
- policies,
- actions,
- 'data-test-subj': dataTestSubj,
- ...commonProps
- }: ArtifactEntryCardProps) => {
- const artifact = useNormalizedArtifact(item);
+export const ArtifactEntryCard = memo(
+ ({ item, policies, actions, 'data-test-subj': dataTestSubj, ...commonProps }) => {
+ const artifact = useNormalizedArtifact(item as AnyArtifact);
const getTestId = useTestIdGenerator(dataTestSubj);
-
- // create the policy links for each policy listed in the artifact record by grabbing the
- // navigation data from the `policies` prop (if any)
- const policyNavLinks = useMemo(() => {
- return artifact.effectScope.type === 'policy'
- ? artifact?.effectScope.policies.map((id) => {
- return policies && policies[id]
- ? policies[id]
- : // else, unable to build a nav link, so just show id
- {
- children: id,
- };
- })
- : undefined;
- }, [artifact.effectScope, policies]);
+ const policyNavLinks = usePolicyNavLinks(artifact, policies);
return (
-
-
+
+
-
+
-
+
-
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx
new file mode 100644
index 0000000000000..43572ea234d31
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiHorizontalRule } from '@elastic/eui';
+import { ArtifactEntryCardProps } from './artifact_entry_card';
+import { CardContainerPanel } from './components/card_container_panel';
+import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
+import { useTestIdGenerator } from '../hooks/use_test_id_generator';
+import { CardSectionPanel } from './components/card_section_panel';
+import { CriteriaConditions, CriteriaConditionsProps } from './components/criteria_conditions';
+import { CardCompressedHeader } from './components/card_compressed_header';
+
+export interface ArtifactEntryCollapsibleCardProps extends ArtifactEntryCardProps {
+ onExpandCollapse: () => void;
+ expanded?: boolean;
+}
+
+export const ArtifactEntryCollapsibleCard = memo(
+ ({
+ item,
+ onExpandCollapse,
+ policies,
+ actions,
+ expanded = false,
+ 'data-test-subj': dataTestSubj,
+ ...commonProps
+ }) => {
+ const artifact = useNormalizedArtifact(item);
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ return (
+
+
+
+
+ {expanded && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ );
+ }
+);
+ArtifactEntryCollapsibleCard.displayName = 'ArtifactEntryCollapsibleCard';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_actions_flex_item.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_actions_flex_item.tsx
new file mode 100644
index 0000000000000..4758eaec4e923
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_actions_flex_item.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, { memo } from 'react';
+import { CommonProps, EuiFlexItem } from '@elastic/eui';
+import { ActionsContextMenu, ActionsContextMenuProps } from '../../actions_context_menu';
+
+export interface CardActionsFlexItemProps extends Pick {
+ /** If defined, then an overflow menu will be shown with the actions provided */
+ actions?: ActionsContextMenuProps['items'];
+}
+
+export const CardActionsFlexItem = memo(
+ ({ actions, 'data-test-subj': dataTestSubj }) => {
+ return actions && actions.length > 0 ? (
+
+
+
+ ) : null;
+ }
+);
+CardActionsFlexItem.displayName = 'CardActionsFlexItem';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx
new file mode 100644
index 0000000000000..6141437779d7d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx
@@ -0,0 +1,183 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo, ReactNode, useCallback } from 'react';
+import { CommonProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import styled from 'styled-components';
+import { CardExpandButton } from './card_expand_button';
+import { TextValueDisplay } from './text_value_display';
+import { EffectScope } from './effect_scope';
+import { CardActionsFlexItem } from './card_actions_flex_item';
+import { ArtifactInfo } from '../types';
+import { ArtifactEntryCollapsibleCardProps } from '../artifact_entry_collapsible_card';
+import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
+import { useCollapsedCssClassNames } from '../hooks/use_collapsed_css_class_names';
+import { usePolicyNavLinks } from '../hooks/use_policy_nav_links';
+import { getEmptyValue } from '../../../../common/components/empty_value';
+
+export interface CardCompressedHeaderProps
+ extends Pick,
+ Pick<
+ ArtifactEntryCollapsibleCardProps,
+ 'onExpandCollapse' | 'expanded' | 'actions' | 'policies'
+ > {
+ artifact: ArtifactInfo;
+}
+
+export const CardCompressedHeader = memo(
+ ({
+ artifact,
+ onExpandCollapse,
+ policies,
+ actions,
+ expanded = false,
+ 'data-test-subj': dataTestSubj,
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+ const cssClassNames = useCollapsedCssClassNames(expanded);
+ const policyNavLinks = usePolicyNavLinks(artifact, policies);
+
+ const handleExpandCollapseClick = useCallback(() => {
+ onExpandCollapse();
+ }, [onExpandCollapse]);
+
+ return (
+
+
+
+
+
+
+
+
+ {artifact.name}
+
+
+
+
+ {artifact.description || getEmptyValue()}
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+CardCompressedHeader.displayName = 'CardCompressedHeader';
+
+const ButtonIconPlaceHolder = styled.div`
+ display: inline-block;
+ // Sizes below should match that of the Eui's Button Icon, so that it holds the same space.
+ width: ${({ theme }) => theme.eui.euiIconSizes.large};
+ height: ${({ theme }) => theme.eui.euiIconSizes.large};
+`;
+
+const StyledEuiFlexGroup = styled(EuiFlexGroup)`
+ &.flushTop,
+ .flushTop {
+ padding-top: 0;
+ margin-top: 0;
+ }
+`;
+
+/**
+ * Layout used for the compressed card header. Used also in the ArtifactGrid for creating the grid header row
+ */
+export interface CardCompressedHeaderLayoutProps extends Pick {
+ expanded: boolean;
+ expandToggle: ReactNode;
+ name: ReactNode;
+ description: ReactNode;
+ effectScope: ReactNode;
+ /** If no menu is shown, but you want the space for it be preserved, set prop to `false` */
+ actionMenu?: ReactNode | false;
+ /**
+ * When set to `true`, all padding and margin values will be set to zero for the top of the header
+ * layout, so that all content is flushed to the top
+ */
+ flushTop?: boolean;
+}
+
+export const CardCompressedHeaderLayout = memo(
+ ({
+ expanded,
+ name,
+ expandToggle,
+ effectScope,
+ actionMenu,
+ description,
+ 'data-test-subj': dataTestSubj,
+ flushTop,
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+ const cssClassNames = useCollapsedCssClassNames(expanded);
+ const flushTopCssClassname = flushTop ? ' flushTop' : '';
+
+ return (
+
+
+ {expandToggle}
+
+
+
+
+ {name}
+
+
+ {description}
+
+
+ {effectScope}
+
+
+
+ {actionMenu === false ? (
+
+
+
+ ) : (
+ actionMenu
+ )}
+
+ );
+ }
+);
+CardCompressedHeaderLayout.displayName = 'CardCompressedHeaderLayout';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_container_panel.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_container_panel.tsx
new file mode 100644
index 0000000000000..0a64670a9de12
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_container_panel.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 styled from 'styled-components';
+import { EuiPanel } from '@elastic/eui';
+import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel';
+import React, { memo } from 'react';
+
+export const EuiPanelStyled = styled(EuiPanel)`
+ &.artifactEntryCard + &.artifactEntryCard {
+ margin-top: ${({ theme }) => theme.eui.spacerSizes.l};
+ }
+`;
+
+export type CardContainerPanelProps = Exclude;
+
+export const CardContainerPanel = memo(({ className, ...props }) => {
+ return (
+
+ );
+});
+
+CardContainerPanel.displayName = 'CardContainerPanel';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_expand_button.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_expand_button.tsx
new file mode 100644
index 0000000000000..a7c0c39321660
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_expand_button.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, { memo } from 'react';
+import { CommonProps, EuiButtonIcon, EuiButtonIconPropsForButton } from '@elastic/eui';
+import { COLLAPSE_ACTION, EXPAND_ACTION } from './translations';
+
+export interface CardExpandButtonProps extends Pick {
+ expanded: boolean;
+ onClick: EuiButtonIconPropsForButton['onClick'];
+}
+
+export const CardExpandButton = memo(
+ ({ expanded, onClick, 'data-test-subj': dataTestSubj }) => {
+ return (
+
+ );
+ }
+);
+CardExpandButton.displayName = 'CardExpandButton';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx
index ca82c97d8b820..6964f5b339312 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_header.tsx
@@ -8,15 +8,15 @@
import React, { memo } from 'react';
import { CommonProps, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { DateFieldValue } from './date_field_value';
-import { ActionsContextMenu, ActionsContextMenuProps } from '../../actions_context_menu';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
+import { CardActionsFlexItem, CardActionsFlexItemProps } from './card_actions_flex_item';
-export interface CardHeaderProps extends Pick {
+export interface CardHeaderProps
+ extends CardActionsFlexItemProps,
+ Pick {
name: string;
createdDate: string;
updatedDate: string;
- /** If defined, then an overflow menu will be shown with the actions provided */
- actions?: ActionsContextMenuProps['items'];
}
export const CardHeader = memo(
@@ -52,15 +52,7 @@ export const CardHeader = memo(
- {actions && actions.length > 0 && (
-
-
-
- )}
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx
new file mode 100644
index 0000000000000..1d694ab1771d3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_section_panel.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiPanel, EuiPanelProps } from '@elastic/eui';
+
+export type CardSectionPanelProps = Exclude<
+ EuiPanelProps,
+ 'hasBorder' | 'hasShadow' | 'paddingSize'
+>;
+
+export const CardSectionPanel = memo((props) => {
+ return ;
+});
+CardSectionPanel.displayName = 'CardSectionPanel';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx
index 4bd86b9af0650..fd787c01e50ff 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_sub_header.tsx
@@ -20,7 +20,7 @@ export const CardSubHeader = memo(
const getTestId = useTestIdGenerator(dataTestSubj);
return (
-
+
0 policies, but no menu
+// the intent in this component was to also support to be able to display only text for artifacts
+// by policy (>0), but **NOT** show the menu.
+// So something like: ` `
+// This should dispaly it as "Applied t o 3 policies", but NOT as a menu with links
+
export interface EffectScopeProps extends Pick {
/** If set (even if empty), then effect scope will be policy specific. Else, it shows as global */
policies?: ContextMenuItemNavByRouterProps[];
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx
index f1d92f4c09778..3843d7992bdf2 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/text_value_display.tsx
@@ -5,18 +5,30 @@
* 2.0.
*/
-import React, { memo, PropsWithChildren } from 'react';
+import React, { memo, PropsWithChildren, useMemo } from 'react';
import { EuiText } from '@elastic/eui';
+import classNames from 'classnames';
export type TextValueDisplayProps = PropsWithChildren<{
bold?: boolean;
+ truncate?: boolean;
}>;
/**
* Common component for displaying consistent text across the card. Changes here could impact all of
* display of values on the card
*/
-export const TextValueDisplay = memo(({ bold, children }) => {
- return {bold ? {children} : children} ;
+export const TextValueDisplay = memo(({ bold, truncate, children }) => {
+ const cssClassNames = useMemo(() => {
+ return classNames({
+ 'eui-textTruncate': truncate,
+ });
+ }, [truncate]);
+
+ return (
+
+ {bold ? {children} : children}
+
+ );
});
TextValueDisplay.displayName = 'TextValueDisplay';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts
index dce922e7e3cef..d98f4589027d4 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/translations.ts
@@ -100,3 +100,17 @@ export const OS_LINUX = i18n.translate('xpack.securitySolution.artifactCard.cond
export const OS_MAC = i18n.translate('xpack.securitySolution.artifactCard.conditions.macos', {
defaultMessage: 'Mac',
});
+
+export const EXPAND_ACTION = i18n.translate(
+ 'xpack.securitySolution.artifactExpandableCard.expand',
+ {
+ defaultMessage: 'Expand',
+ }
+);
+
+export const COLLAPSE_ACTION = i18n.translate(
+ 'xpack.securitySolution.artifactExpandableCard.collpase',
+ {
+ defaultMessage: 'Collapse',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_collapsed_css_class_names.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_collapsed_css_class_names.ts
new file mode 100644
index 0000000000000..2b9fe1c76fbac
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_collapsed_css_class_names.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { useMemo } from 'react';
+import classNames from 'classnames';
+
+/**
+ * Returns the css classnames that should be applied when the collapsible card is NOT expanded
+ * @param expanded
+ */
+export const useCollapsedCssClassNames = (expanded?: boolean): string => {
+ return useMemo(() => {
+ return classNames({
+ 'eui-textTruncate': !expanded,
+ });
+ }, [expanded]);
+};
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts
index 7b3fb07bf10ac..4ea8d4aa6ee7c 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_normalized_artifact.ts
@@ -5,53 +5,18 @@
* 2.0.
*/
-/* eslint-disable @typescript-eslint/naming-convention */
-
import { useMemo } from 'react';
-import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { AnyArtifact, ArtifactInfo } from '../types';
-import { EffectScope, TrustedApp } from '../../../../../common/endpoint/types';
-import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping';
+import { mapToArtifactInfo } from '../utils';
+import { MaybeImmutable } from '../../../../../common/endpoint/types';
/**
* Takes in any artifact and return back a new data structure used internally with by the card's components
*
* @param item
*/
-export const useNormalizedArtifact = (item: AnyArtifact): ArtifactInfo => {
+export const useNormalizedArtifact = (item: MaybeImmutable): ArtifactInfo => {
return useMemo(() => {
- const {
- name,
- created_by,
- created_at,
- updated_at,
- updated_by,
- description = '',
- entries,
- } = item;
- return {
- name,
- created_by,
- created_at,
- updated_at,
- updated_by,
- description,
- entries: entries as unknown as ArtifactInfo['entries'],
- os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item),
- effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item),
- };
+ return mapToArtifactInfo(item);
}, [item]);
};
-
-export const isTrustedApp = (item: AnyArtifact): item is TrustedApp => {
- return 'effectScope' in item;
-};
-
-const getOsFromExceptionItem = (item: ExceptionListItemSchema): string => {
- // FYI: Exceptions seem to allow for items to be assigned to more than one OS, unlike Event Filters and Trusted Apps
- return item.os_types.join(', ');
-};
-
-const getEffectScopeFromExceptionItem = (item: ExceptionListItemSchema): EffectScope => {
- return tagsToEffectScope(item.tags);
-};
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_policy_nav_links.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_policy_nav_links.ts
new file mode 100644
index 0000000000000..dd403ebaf448c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/hooks/use_policy_nav_links.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { useMemo } from 'react';
+import { EffectScopeProps } from '../components/effect_scope';
+import { ArtifactInfo, MenuItemPropsByPolicyId } from '../types';
+import { ContextMenuItemNavByRouterProps } from '../../context_menu_with_router_support/context_menu_item_nav_by_router';
+
+/**
+ * creates the policy links for each policy listed in the artifact record by grabbing the
+ * navigation data from the `policies` prop (if any)
+ */
+export const usePolicyNavLinks = (
+ artifact: ArtifactInfo,
+ policies?: MenuItemPropsByPolicyId
+): ContextMenuItemNavByRouterProps[] | undefined => {
+ return useMemo(() => {
+ return artifact.effectScope.type === 'policy'
+ ? artifact?.effectScope.policies.map((id) => {
+ return policies && policies[id]
+ ? policies[id]
+ : // else, unable to build a nav link, so just show id
+ {
+ children: id,
+ };
+ })
+ : undefined;
+ }, [artifact.effectScope, policies]);
+};
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts
index f37d5d4e650e1..71a1230889559 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/index.ts
@@ -7,3 +7,7 @@
export * from './artifact_entry_card';
export * from './artifact_entry_card_minified';
+export * from './artifact_entry_collapsible_card';
+export * from './components/card_section_panel';
+export * from './types';
+export { CardCompressedHeaderLayout } from './components/card_compressed_header';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts
index c59a2bde94589..c506c62ac4351 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/types.ts
@@ -7,6 +7,7 @@
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EffectScope, TrustedApp } from '../../../../common/endpoint/types';
+import { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support/context_menu_item_nav_by_router';
export type AnyArtifact = ExceptionListItemSchema | TrustedApp;
@@ -27,3 +28,7 @@ export interface ArtifactInfo
value: string;
}>;
}
+
+export interface MenuItemPropsByPolicyId {
+ [policyId: string]: ContextMenuItemNavByRouterProps;
+}
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/index.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/index.ts
new file mode 100644
index 0000000000000..a7bb9a7f43336
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/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 * from './is_trusted_app';
+export * from './map_to_artifact_info';
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/is_trusted_app.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/is_trusted_app.ts
new file mode 100644
index 0000000000000..a14ff293d05e8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/is_trusted_app.ts
@@ -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 { AnyArtifact } from '../types';
+import { TrustedApp } from '../../../../../common/endpoint/types';
+
+/**
+ * Type guard for `AnyArtifact` to check if it is a trusted app entry
+ * @param item
+ */
+export const isTrustedApp = (item: AnyArtifact): item is TrustedApp => {
+ return 'effectScope' in item;
+};
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts
new file mode 100644
index 0000000000000..dd9e90db327ee
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/map_to_artifact_info.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
+import { AnyArtifact, ArtifactInfo } from '../types';
+import { EffectScope, MaybeImmutable } from '../../../../../common/endpoint/types';
+import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping';
+import { isTrustedApp } from './is_trusted_app';
+
+export const mapToArtifactInfo = (_item: MaybeImmutable): ArtifactInfo => {
+ const item = _item as AnyArtifact;
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { name, created_by, created_at, updated_at, updated_by, description = '', entries } = item;
+
+ return {
+ name,
+ created_by,
+ created_at,
+ updated_at,
+ updated_by,
+ description,
+ entries: entries as unknown as ArtifactInfo['entries'],
+ os: isTrustedApp(item) ? item.os : getOsFromExceptionItem(item),
+ effectScope: isTrustedApp(item) ? item.effectScope : getEffectScopeFromExceptionItem(item),
+ };
+};
+
+const getOsFromExceptionItem = (item: ExceptionListItemSchema): string => {
+ // FYI: Exceptions seem to allow for items to be assigned to more than one OS, unlike Event Filters and Trusted Apps
+ return item.os_types.join(', ');
+};
+
+const getEffectScopeFromExceptionItem = (item: ExceptionListItemSchema): EffectScope => {
+ return tagsToEffectScope(item.tags);
+};
diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_rotuer.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx
similarity index 65%
rename from x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_rotuer.tsx
rename to x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx
index cc95235831c2e..fd087f267a9b5 100644
--- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_rotuer.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx
@@ -15,6 +15,12 @@ export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps
navigateAppId?: string;
/** Additional options for the navigation action via react-router */
navigateOptions?: NavigateToAppOptions;
+ /**
+ * if `true`, the `children` will be wrapped in a `div` that contains CSS Classname `eui-textTruncate`.
+ * **NOTE**: When this component is used in combination with `ContextMenuWithRouterSupport` and `maxWidth`
+ * is set on the menu component, this prop will be overridden
+ */
+ textTruncate?: boolean;
children: React.ReactNode;
}
@@ -23,7 +29,7 @@ export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps
* allow navigation to a URL path via React Router
*/
export const ContextMenuItemNavByRouter = memo(
- ({ navigateAppId, navigateOptions, onClick, children, ...otherMenuItemProps }) => {
+ ({ navigateAppId, navigateOptions, onClick, textTruncate, children, ...otherMenuItemProps }) => {
const handleOnClickViaNavigateToApp = useNavigateToAppEventHandler(navigateAppId ?? '', {
...navigateOptions,
onClick,
@@ -34,7 +40,19 @@ export const ContextMenuItemNavByRouter = memo(
{...otherMenuItemProps}
onClick={navigateAppId ? handleOnClickViaNavigateToApp : onClick}
>
- {children}
+ {textTruncate ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
);
}
diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx
index 8fbb7eca60a38..3f21f3995ac5b 100644
--- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { memo, useCallback, useMemo, useState } from 'react';
+import React, { CSSProperties, HTMLAttributes, memo, useCallback, useMemo, useState } from 'react';
import {
CommonProps,
EuiContextMenuPanel,
@@ -16,13 +16,19 @@ import {
import {
ContextMenuItemNavByRouter,
ContextMenuItemNavByRouterProps,
-} from './context_menu_item_nav_by_rotuer';
+} from './context_menu_item_nav_by_router';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
export interface ContextMenuWithRouterSupportProps
extends CommonProps,
Pick {
items: ContextMenuItemNavByRouterProps[];
+ /**
+ * The max width for the popup menu. Default is `32ch`.
+ * **Note** that when used (default behaviour), all menu item's `truncateText` prop will be
+ * overwritten to `true`. Setting this prop's value to `undefined` will suppress the default behaviour.
+ */
+ maxWidth?: CSSProperties['maxWidth'];
}
/**
@@ -31,7 +37,7 @@ export interface ContextMenuWithRouterSupportProps
* Menu also supports automatically closing the popup when an item is clicked.
*/
export const ContextMenuWithRouterSupport = memo(
- ({ items, button, panelPaddingSize, anchorPosition, ...commonProps }) => {
+ ({ items, button, panelPaddingSize, anchorPosition, maxWidth = '32ch', ...commonProps }) => {
const getTestId = useTestIdGenerator(commonProps['data-test-subj']);
const [isOpen, setIsOpen] = useState(false);
@@ -47,6 +53,7 @@ export const ContextMenuWithRouterSupport = memo {
handleCloseMenu();
if (itemProps.onClick) {
@@ -56,7 +63,20 @@ export const ContextMenuWithRouterSupport = memo
);
});
- }, [handleCloseMenu, items]);
+ }, [handleCloseMenu, items, maxWidth]);
+
+ type AdditionalPanelProps = Partial>;
+ const additionalContextMenuPanelProps = useMemo(() => {
+ const newAdditionalProps: AdditionalPanelProps = {
+ style: {},
+ };
+
+ if (maxWidth) {
+ newAdditionalProps.style!.maxWidth = maxWidth;
+ }
+
+ return newAdditionalProps;
+ }, [maxWidth]);
return (
-
+
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx
index 6df5413c1eb3c..d97524894d5f8 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/table_row_actions.tsx
@@ -14,7 +14,7 @@ import {
EuiPopoverProps,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { ContextMenuItemNavByRouter } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_rotuer';
+import { ContextMenuItemNavByRouter } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
import { HostMetadata } from '../../../../../../common/endpoint/types';
import { useEndpointActionItems } from '../hooks';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx
index 412db3dc2a63e..71060577e3a34 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx
@@ -10,7 +10,7 @@ import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useEndpointActionItems, useEndpointSelector } from '../../hooks';
import { detailsData } from '../../../store/selectors';
-import { ContextMenuItemNavByRouter } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_rotuer';
+import { ContextMenuItemNavByRouter } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
export const ActionsMenu = React.memo<{}>(() => {
const endpointDetails = useEndpointSelector(detailsData);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
index 8f19fea818fc6..81432edbdd5fe 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
@@ -14,7 +14,7 @@ import { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/
import { useEndpointSelector } from './hooks';
import { agentPolicies, uiQueryParams } from '../../store/selectors';
import { useAppUrl } from '../../../../../common/lib/kibana/hooks';
-import { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_rotuer';
+import { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
import { isEndpointHostIsolated } from '../../../../../common/utils/validators';
import { useLicense } from '../../../../../common/hooks/use_license';
import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts
index c7bc142eb78c5..fc32d42db647d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts
@@ -5,14 +5,17 @@
* 2.0.
*/
+import { Action } from 'redux';
import { AsyncResourceState } from '../../../../../state';
import {
PostTrustedAppCreateResponse,
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
} from '../../../../../../../common/endpoint/types';
+import { PolicyArtifactsState } from '../../../types';
+
export interface PolicyArtifactsAssignableListPageDataChanged {
type: 'policyArtifactsAssignableListPageDataChanged';
- payload: AsyncResourceState;
+ payload: AsyncResourceState;
}
export interface PolicyArtifactsUpdateTrustedApps {
@@ -37,9 +40,28 @@ export interface PolicyArtifactsAssignableListPageDataFilter {
payload: { filter: string };
}
+export interface AssignedTrustedAppsListStateChanged
+ extends Action<'assignedTrustedAppsListStateChanged'> {
+ payload: PolicyArtifactsState['assignedList'];
+}
+
+export interface PolicyDetailsListOfAllPoliciesStateChanged
+ extends Action<'policyDetailsListOfAllPoliciesStateChanged'> {
+ payload: PolicyArtifactsState['policies'];
+}
+
+export type PolicyDetailsTrustedAppsForceListDataRefresh =
+ Action<'policyDetailsTrustedAppsForceListDataRefresh'>;
+
+/**
+ * All of the possible actions for Trusted Apps under the Policy Details store
+ */
export type PolicyTrustedAppsAction =
| PolicyArtifactsAssignableListPageDataChanged
| PolicyArtifactsUpdateTrustedApps
| PolicyArtifactsUpdateTrustedAppsChanged
| PolicyArtifactsAssignableListExistDataChanged
- | PolicyArtifactsAssignableListPageDataFilter;
+ | PolicyArtifactsAssignableListPageDataFilter
+ | AssignedTrustedAppsListStateChanged
+ | PolicyDetailsListOfAllPoliciesStateChanged
+ | PolicyDetailsTrustedAppsForceListDataRefresh;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts
index 6b7e4e7d541c8..acc94f3383a84 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts
@@ -6,9 +6,10 @@
*/
import { ImmutableMiddlewareFactory } from '../../../../../../common/store';
-import { PolicyDetailsState } from '../../../types';
+import { MiddlewareRunnerContext, PolicyDetailsState } from '../../../types';
import { policyTrustedAppsMiddlewareRunner } from './policy_trusted_apps_middleware';
import { policySettingsMiddlewareRunner } from './policy_settings_middleware';
+import { TrustedAppsHttpService } from '../../../../trusted_apps/service';
export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = (
coreStart
@@ -16,7 +17,13 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory (next) => async (action) => {
next(action);
- policySettingsMiddlewareRunner(coreStart, store, action);
- policyTrustedAppsMiddlewareRunner(coreStart, store, action);
+ const trustedAppsService = new TrustedAppsHttpService(coreStart.http);
+ const middlewareContext: MiddlewareRunnerContext = {
+ coreStart,
+ trustedAppsService,
+ };
+
+ policySettingsMiddlewareRunner(middlewareContext, store, action);
+ policyTrustedAppsMiddlewareRunner(middlewareContext, store, action);
};
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
index 73b244944e502..5f612f4f4e6f6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
@@ -27,7 +27,7 @@ import { NewPolicyData, PolicyData } from '../../../../../../../common/endpoint/
import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy';
export const policySettingsMiddlewareRunner: MiddlewareRunner = async (
- coreStart,
+ { coreStart },
{ dispatch, getState },
action
) => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts
index 532e39b482401..32968e4de116f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts
@@ -7,30 +7,95 @@
import pMap from 'p-map';
import { find, isEmpty } from 'lodash/fp';
-import { PolicyDetailsState, MiddlewareRunner } from '../../../types';
+import {
+ PolicyDetailsState,
+ MiddlewareRunner,
+ GetPolicyListResponse,
+ MiddlewareRunnerContext,
+ PolicyAssignedTrustedApps,
+ PolicyDetailsStore,
+} from '../../../types';
import {
policyIdFromParams,
- isOnPolicyTrustedAppsPage,
- getCurrentArtifactsLocation,
getAssignableArtifactsList,
+ doesPolicyTrustedAppsListNeedUpdate,
+ getCurrentPolicyAssignedTrustedAppsState,
+ getLatestLoadedPolicyAssignedTrustedAppsState,
+ getTrustedAppsPolicyListState,
+ isPolicyTrustedAppListLoading,
+ getCurrentArtifactsLocation,
+ isOnPolicyTrustedAppsView,
+ getCurrentUrlLocationPaginationParams,
} from '../selectors';
import {
ImmutableArray,
ImmutableObject,
PostTrustedAppCreateRequest,
TrustedApp,
+ Immutable,
} from '../../../../../../../common/endpoint/types';
import { ImmutableMiddlewareAPI } from '../../../../../../common/store';
-import { TrustedAppsHttpService, TrustedAppsService } from '../../../../trusted_apps/service';
+import { TrustedAppsService } from '../../../../trusted_apps/service';
import {
createLoadedResourceState,
createLoadingResourceState,
createUninitialisedResourceState,
createFailedResourceState,
+ isLoadingResourceState,
+ isUninitialisedResourceState,
} from '../../../../../state';
import { parseQueryFilterToKQL } from '../../../../../common/utils';
import { SEARCHABLE_FIELDS } from '../../../../trusted_apps/constants';
import { PolicyDetailsAction } from '../action';
+import { ServerApiError } from '../../../../../../common/types';
+
+/** Runs all middleware actions associated with the Trusted Apps view in Policy Details */
+export const policyTrustedAppsMiddlewareRunner: MiddlewareRunner = async (
+ context,
+ store,
+ action
+) => {
+ const state = store.getState();
+
+ /* -----------------------------------------------------------
+ If not on the Trusted Apps Policy view, then just return
+ ----------------------------------------------------------- */
+ if (!isOnPolicyTrustedAppsView(state)) {
+ return;
+ }
+
+ const { trustedAppsService } = context;
+
+ switch (action.type) {
+ case 'userChangedUrl':
+ fetchPolicyTrustedAppsIfNeeded(context, store);
+ fetchAllPoliciesIfNeeded(context, store);
+
+ if (action.type === 'userChangedUrl' && getCurrentArtifactsLocation(state).show === 'list') {
+ await searchTrustedApps(store, trustedAppsService);
+ }
+
+ break;
+
+ case 'policyDetailsTrustedAppsForceListDataRefresh':
+ fetchPolicyTrustedAppsIfNeeded(context, store, true);
+ break;
+
+ case 'policyArtifactsUpdateTrustedApps':
+ if (getCurrentArtifactsLocation(state).show === 'list') {
+ await updateTrustedApps(store, trustedAppsService, action.payload.trustedAppIds);
+ }
+
+ break;
+
+ case 'policyArtifactsAssignableListPageDataFilter':
+ if (getCurrentArtifactsLocation(state).show === 'list') {
+ await searchTrustedApps(store, trustedAppsService, action.payload.filter);
+ }
+
+ break;
+ }
+};
const checkIfThereAreAssignableTrustedApps = async (
store: ImmutableMiddlewareAPI,
@@ -172,6 +237,8 @@ const updateTrustedApps = async (
type: 'policyArtifactsUpdateTrustedAppsChanged',
payload: createLoadedResourceState(updatedTrustedApps),
});
+
+ store.dispatch({ type: 'policyDetailsTrustedAppsForceListDataRefresh' });
} catch (err) {
store.dispatch({
type: 'policyArtifactsUpdateTrustedAppsChanged',
@@ -182,31 +249,89 @@ const updateTrustedApps = async (
}
};
-export const policyTrustedAppsMiddlewareRunner: MiddlewareRunner = async (
- coreStart,
- store,
- action
+const fetchPolicyTrustedAppsIfNeeded = async (
+ { trustedAppsService }: MiddlewareRunnerContext,
+ { getState, dispatch }: PolicyDetailsStore,
+ forceFetch: boolean = false
) => {
- const http = coreStart.http;
- const trustedAppsService = new TrustedAppsHttpService(http);
- const state = store.getState();
- if (
- action.type === 'userChangedUrl' &&
- isOnPolicyTrustedAppsPage(state) &&
- getCurrentArtifactsLocation(state).show === 'list'
- ) {
- await searchTrustedApps(store, trustedAppsService);
- } else if (
- action.type === 'policyArtifactsUpdateTrustedApps' &&
- isOnPolicyTrustedAppsPage(state) &&
- getCurrentArtifactsLocation(state).show === 'list'
- ) {
- await updateTrustedApps(store, trustedAppsService, action.payload.trustedAppIds);
- } else if (
- action.type === 'policyArtifactsAssignableListPageDataFilter' &&
- isOnPolicyTrustedAppsPage(state) &&
- getCurrentArtifactsLocation(state).show === 'list'
- ) {
- await searchTrustedApps(store, trustedAppsService, action.payload.filter);
+ const state = getState();
+
+ if (isPolicyTrustedAppListLoading(state)) {
+ return;
+ }
+
+ if (forceFetch || doesPolicyTrustedAppsListNeedUpdate(state)) {
+ dispatch({
+ type: 'assignedTrustedAppsListStateChanged',
+ // @ts-ignore will be fixed when AsyncResourceState is refactored (#830)
+ payload: createLoadingResourceState(getCurrentPolicyAssignedTrustedAppsState(state)),
+ });
+
+ try {
+ const urlLocationData = getCurrentUrlLocationPaginationParams(state);
+ const policyId = policyIdFromParams(state);
+ const fetchResponse = await trustedAppsService.getTrustedAppsList({
+ page: urlLocationData.page_index + 1,
+ per_page: urlLocationData.page_size,
+ kuery: `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))${
+ urlLocationData.filter ? ` AND (${urlLocationData.filter})` : ''
+ }`,
+ });
+
+ dispatch({
+ type: 'assignedTrustedAppsListStateChanged',
+ payload: createLoadedResourceState>({
+ location: urlLocationData,
+ artifacts: fetchResponse,
+ }),
+ });
+ } catch (error) {
+ dispatch({
+ type: 'assignedTrustedAppsListStateChanged',
+ payload: createFailedResourceState>(
+ error as ServerApiError,
+ getLatestLoadedPolicyAssignedTrustedAppsState(getState())
+ ),
+ });
+ }
+ }
+};
+
+const fetchAllPoliciesIfNeeded = async (
+ { trustedAppsService }: MiddlewareRunnerContext,
+ { getState, dispatch }: PolicyDetailsStore
+) => {
+ const state = getState();
+ const currentPoliciesState = getTrustedAppsPolicyListState(state);
+ const isLoading = isLoadingResourceState(currentPoliciesState);
+ const hasBeenLoaded = !isUninitialisedResourceState(currentPoliciesState);
+
+ if (isLoading || hasBeenLoaded) {
+ return;
+ }
+
+ dispatch({
+ type: 'policyDetailsListOfAllPoliciesStateChanged',
+ // @ts-ignore will be fixed when AsyncResourceState is refactored (#830)
+ payload: createLoadingResourceState(currentPoliciesState),
+ });
+
+ try {
+ const policyList = await trustedAppsService.getPolicyList({
+ query: {
+ page: 1,
+ perPage: 1000,
+ },
+ });
+
+ dispatch({
+ type: 'policyDetailsListOfAllPoliciesStateChanged',
+ payload: createLoadedResourceState(policyList),
+ });
+ } catch (error) {
+ dispatch({
+ type: 'policyDetailsListOfAllPoliciesStateChanged',
+ payload: createFailedResourceState(error.body || error),
+ });
}
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts
index 93108e6f376ae..3c279e696ee51 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts
@@ -37,5 +37,7 @@ export const initialPolicyDetailsState: () => Immutable = ()
assignableList: createUninitialisedResourceState(),
trustedAppsToUpdate: createUninitialisedResourceState(),
assignableListEntriesExist: createUninitialisedResourceState(),
+ assignedList: createUninitialisedResourceState(),
+ policies: createUninitialisedResourceState(),
},
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts
index 26efcaa68686d..e1d2fab6dcdb6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
import { PolicyDetailsState } from '../../../types';
-import { initialPolicyDetailsState } from '../reducer/initial_policy_details_state';
+import { initialPolicyDetailsState } from './initial_policy_details_state';
import { policyTrustedAppsReducer } from './trusted_apps_reducer';
import { ImmutableObject } from '../../../../../../../common/endpoint/types';
@@ -16,12 +16,20 @@ import {
createFailedResourceState,
} from '../../../../../state';
import { getMockListResponse, getAPIError, getMockCreateResponse } from '../../../test_utils';
+import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
describe('policy trusted apps reducer', () => {
let initialState: ImmutableObject;
beforeEach(() => {
- initialState = initialPolicyDetailsState();
+ initialState = {
+ ...initialPolicyDetailsState(),
+ location: {
+ pathname: getPolicyDetailsArtifactsListPath('abc'),
+ search: '',
+ hash: '',
+ },
+ };
});
describe('PolicyTrustedApps', () => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts
index e2843ec83ee2a..fdf15f0d67825 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts
@@ -9,11 +9,28 @@ import { ImmutableReducer } from '../../../../../../common/store';
import { PolicyDetailsState } from '../../../types';
import { AppAction } from '../../../../../../common/store/actions';
import { initialPolicyDetailsState } from './initial_policy_details_state';
+import { isUninitialisedResourceState } from '../../../../../state';
+import { getCurrentPolicyAssignedTrustedAppsState, isOnPolicyTrustedAppsView } from '../selectors';
export const policyTrustedAppsReducer: ImmutableReducer = (
state = initialPolicyDetailsState(),
action
) => {
+ /* ----------------------------------------------------------
+ If not on the Trusted Apps Policy view, then just return
+ ---------------------------------------------------------- */
+ if (!isOnPolicyTrustedAppsView(state)) {
+ // If the artifacts state namespace needs resetting, then do it now
+ if (!isUninitialisedResourceState(getCurrentPolicyAssignedTrustedAppsState(state))) {
+ return {
+ ...state,
+ artifacts: initialPolicyDetailsState().artifacts,
+ };
+ }
+
+ return state;
+ }
+
if (action.type === 'policyArtifactsAssignableListPageDataChanged') {
return {
...state,
@@ -44,5 +61,25 @@ export const policyTrustedAppsReducer: ImmutableReducer = (state) => state.artifacts.location;
+
+export const getUrlLocationPathname: PolicyDetailsSelector = (state) =>
+ state.location?.pathname;
+
+/** Returns a boolean of whether the user is on the policy form page or not */
+export const isOnPolicyFormView: PolicyDetailsSelector = createSelector(
+ getUrlLocationPathname,
+ (pathname) => {
+ return (
+ matchPath(pathname ?? '', {
+ path: MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
+ exact: true,
+ }) !== null
+ );
+ }
+);
+
+/** Returns a boolean of whether the user is on the policy details page or not */
+export const isOnPolicyTrustedAppsView: PolicyDetailsSelector = createSelector(
+ getUrlLocationPathname,
+ (pathname) => {
+ return (
+ matchPath(pathname ?? '', {
+ path: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
+ exact: true,
+ }) !== null
+ );
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
index 84049c98eaa11..40d77f5869b67 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
@@ -23,8 +23,8 @@ import {
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
} from '../../../../../common/constants';
import { ManagementRoutePolicyDetailsParams } from '../../../../../types';
-import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy/get_policy_data_for_update';
-import { isOnPolicyTrustedAppsPage } from './trusted_apps_selectors';
+import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy';
+import { isOnPolicyTrustedAppsView, isOnPolicyFormView } from './policy_common_selectors';
/** Returns the policy details */
export const policyDetails = (state: Immutable) => state.policyItem;
@@ -81,19 +81,9 @@ export const needsToRefresh = (state: Immutable): boolean =>
return !state.policyItem && !state.apiError;
};
-/** Returns a boolean of whether the user is on the policy form page or not */
-export const isOnPolicyFormPage = (state: Immutable) => {
- return (
- matchPath(state.location?.pathname ?? '', {
- path: MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
- exact: true,
- }) !== null
- );
-};
-
/** Returns a boolean of whether the user is on some of the policy details page or not */
export const isOnPolicyDetailsPage = (state: Immutable) =>
- isOnPolicyFormPage(state) || isOnPolicyTrustedAppsPage(state);
+ isOnPolicyFormView(state) || isOnPolicyTrustedAppsView(state);
/** Returns the license info fetched from the license service */
export const license = (state: Immutable) => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts
index 6d32988464957..6839edb965332 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts
@@ -6,9 +6,8 @@
*/
import { PolicyDetailsState } from '../../../types';
-import { initialPolicyDetailsState } from '../reducer/initial_policy_details_state';
+import { initialPolicyDetailsState } from '../reducer';
import {
- getCurrentArtifactsLocation,
getAssignableArtifactsList,
getAssignableArtifactsListIsLoading,
getUpdateArtifactsIsLoading,
@@ -17,8 +16,8 @@ import {
getAssignableArtifactsListExist,
getAssignableArtifactsListExistIsLoading,
getUpdateArtifacts,
- isOnPolicyTrustedAppsPage,
} from './trusted_apps_selectors';
+import { getCurrentArtifactsLocation, isOnPolicyTrustedAppsView } from './policy_common_selectors';
import { ImmutableObject } from '../../../../../../../common/endpoint/types';
import {
@@ -39,7 +38,7 @@ describe('policy trusted apps selectors', () => {
describe('isOnPolicyTrustedAppsPage()', () => {
it('when location is on policy trusted apps page', () => {
- const isOnPage = isOnPolicyTrustedAppsPage({
+ const isOnPage = isOnPolicyTrustedAppsView({
...initialState,
location: {
pathname: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
@@ -50,7 +49,7 @@ describe('policy trusted apps selectors', () => {
expect(isOnPage).toBeFalsy();
});
it('when location is not on policy trusted apps page', () => {
- const isOnPage = isOnPolicyTrustedAppsPage({
+ const isOnPage = isOnPolicyTrustedAppsView({
...initialState,
location: { pathname: '', search: '', hash: '' },
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts
index 65d24ac58cab4..b168ec18107c2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts
@@ -5,35 +5,48 @@
* 2.0.
*/
-import { matchPath } from 'react-router-dom';
-import { PolicyDetailsArtifactsPageLocation, PolicyDetailsState } from '../../../types';
+import { createSelector } from 'reselect';
+import { Pagination } from '@elastic/eui';
+import {
+ PolicyArtifactsState,
+ PolicyAssignedTrustedApps,
+ PolicyDetailsArtifactsPageListLocationParams,
+ PolicyDetailsSelector,
+ PolicyDetailsState,
+} from '../../../types';
import {
Immutable,
ImmutableArray,
PostTrustedAppCreateResponse,
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
+ PolicyData,
} from '../../../../../../../common/endpoint/types';
-import { MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH } from '../../../../../common/constants';
+import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../../../common/constants';
import {
getLastLoadedResourceState,
isFailedResourceState,
isLoadedResourceState,
isLoadingResourceState,
+ LoadedResourceState,
} from '../../../../../state';
+import { getCurrentArtifactsLocation } from './policy_common_selectors';
-/**
- * Returns current artifacts location
- */
-export const getCurrentArtifactsLocation = (
- state: Immutable
-): Immutable => state.artifacts.location;
+export const doesPolicyHaveTrustedApps = (
+ state: PolicyDetailsState
+): { loading: boolean; hasTrustedApps: boolean } => {
+ // TODO: implement empty state (task #1645)
+ return {
+ loading: false,
+ hasTrustedApps: true,
+ };
+};
/**
* Returns current assignable artifacts list
*/
export const getAssignableArtifactsList = (
state: Immutable
-): Immutable | undefined =>
+): Immutable | undefined =>
getLastLoadedResourceState(state.artifacts.assignableList)?.data;
/**
@@ -92,12 +105,79 @@ export const getUpdateArtifacts = (
: undefined;
};
-/** Returns a boolean of whether the user is on the policy details page or not */
-export const isOnPolicyTrustedAppsPage = (state: Immutable) => {
- return (
- matchPath(state.location?.pathname ?? '', {
- path: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
- exact: true,
- }) !== null
- );
+export const getCurrentPolicyAssignedTrustedAppsState: PolicyDetailsSelector<
+ PolicyArtifactsState['assignedList']
+> = (state) => {
+ return state.artifacts.assignedList;
};
+
+export const getLatestLoadedPolicyAssignedTrustedAppsState: PolicyDetailsSelector<
+ undefined | LoadedResourceState
+> = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => {
+ return getLastLoadedResourceState(currentAssignedTrustedAppsState);
+});
+
+export const getCurrentUrlLocationPaginationParams: PolicyDetailsSelector =
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ createSelector(getCurrentArtifactsLocation, ({ filter, page_index, page_size }) => {
+ return { filter, page_index, page_size };
+ });
+
+export const doesPolicyTrustedAppsListNeedUpdate: PolicyDetailsSelector = createSelector(
+ getCurrentPolicyAssignedTrustedAppsState,
+ getCurrentUrlLocationPaginationParams,
+ (assignedListState, locationData) => {
+ return (
+ !isLoadedResourceState(assignedListState) ||
+ (isLoadedResourceState(assignedListState) &&
+ (
+ Object.keys(locationData) as Array
+ ).some((key) => assignedListState.data.location[key] !== locationData[key]))
+ );
+ }
+);
+
+export const isPolicyTrustedAppListLoading: PolicyDetailsSelector = createSelector(
+ getCurrentPolicyAssignedTrustedAppsState,
+ (assignedState) => isLoadingResourceState(assignedState)
+);
+
+export const getPolicyTrustedAppList: PolicyDetailsSelector =
+ createSelector(getLatestLoadedPolicyAssignedTrustedAppsState, (assignedState) => {
+ return assignedState?.data.artifacts.data ?? [];
+ });
+
+export const getPolicyTrustedAppsListPagination: PolicyDetailsSelector = createSelector(
+ getLatestLoadedPolicyAssignedTrustedAppsState,
+ (currentAssignedTrustedAppsState) => {
+ const trustedAppsApiResponse = currentAssignedTrustedAppsState?.data.artifacts;
+
+ return {
+ // Trusted apps api is `1` based for page - need to subtract here for `Pagination` component
+ pageIndex: trustedAppsApiResponse?.page ? trustedAppsApiResponse.page - 1 : 0,
+ pageSize: trustedAppsApiResponse?.per_page ?? MANAGEMENT_PAGE_SIZE_OPTIONS[0],
+ totalItemCount: trustedAppsApiResponse?.total || 0,
+ pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
+ };
+ }
+);
+
+export const getTrustedAppsPolicyListState: PolicyDetailsSelector<
+ PolicyDetailsState['artifacts']['policies']
+> = (state) => state.artifacts.policies;
+
+export const getTrustedAppsListOfAllPolicies: PolicyDetailsSelector = createSelector(
+ getTrustedAppsPolicyListState,
+ (policyListState) => {
+ return getLastLoadedResourceState(policyListState)?.data.items ?? [];
+ }
+);
+
+export const getTrustedAppsAllPoliciesById: PolicyDetailsSelector<
+ Record>
+> = createSelector(getTrustedAppsListOfAllPolicies, (allPolicies) => {
+ return allPolicies.reduce>>((mapById, policy) => {
+ mapById[policy.id] = policy;
+ return mapById;
+ }, {}) as Immutable>>;
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts
index 383b7e277babd..d92c41f5a1cc6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts
@@ -6,13 +6,13 @@
*/
import {
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
PostTrustedAppCreateResponse,
} from '../../../../../common/endpoint/types';
import { createSampleTrustedApps, createSampleTrustedApp } from '../../trusted_apps/test_utils';
-export const getMockListResponse: () => GetTrustedListAppsResponse = () => ({
+export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({
data: createSampleTrustedApps({}),
per_page: 100,
page: 1,
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
index 8e4e31a3c70e9..87f3243211c89 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
@@ -14,58 +14,41 @@ import {
PolicyData,
UIPolicyConfig,
PostTrustedAppCreateResponse,
- GetTrustedListAppsResponse,
MaybeImmutable,
+ GetTrustedAppsListResponse,
} from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
import {
GetAgentStatusResponse,
GetOnePackagePolicyResponse,
GetPackagePoliciesResponse,
- GetPackagesResponse,
UpdatePackagePolicyResponse,
} from '../../../../../fleet/common';
import { AsyncResourceState } from '../../state';
import { ImmutableMiddlewareAPI } from '../../../common/store';
import { AppAction } from '../../../common/store/actions';
+import { TrustedAppsService } from '../trusted_apps/service';
+
+export type PolicyDetailsStore = ImmutableMiddlewareAPI;
/**
* Function that runs Policy Details middleware
*/
export type MiddlewareRunner = (
- coreStart: CoreStart,
- store: ImmutableMiddlewareAPI,
+ context: MiddlewareRunnerContext,
+ store: PolicyDetailsStore,
action: MaybeImmutable
) => Promise;
-/**
- * Policy list store state
- */
-export interface PolicyListState {
- /** Array of policy items */
- policyItems: PolicyData[];
- /** Information about the latest endpoint package */
- endpointPackageInfo?: GetPackagesResponse['response'][0];
- /** API error if loading data failed */
- apiError?: ServerApiError;
- /** total number of policies */
- total: number;
- /** Number of policies per page */
- pageSize: number;
- /** page number (zero based) */
- pageIndex: number;
- /** data is being retrieved from server */
- isLoading: boolean;
- /** current location information */
- location?: Immutable;
- /** policy is being deleted */
- isDeleting: boolean;
- /** Deletion status */
- deleteStatus?: boolean;
- /** A summary of stats for the agents associated with a given Fleet Agent Policy */
- agentStatusSummary?: GetAgentStatusResponse['results'];
+export interface MiddlewareRunnerContext {
+ coreStart: CoreStart;
+ trustedAppsService: TrustedAppsService;
}
+export type PolicyDetailsSelector = (
+ state: Immutable
+) => Immutable;
+
/**
* Policy details store state
*/
@@ -90,6 +73,11 @@ export interface PolicyDetailsState {
license?: ILicense;
}
+export interface PolicyAssignedTrustedApps {
+ location: PolicyDetailsArtifactsPageListLocationParams;
+ artifacts: GetTrustedAppsListResponse;
+}
+
/**
* Policy artifacts store state
*/
@@ -97,11 +85,15 @@ export interface PolicyArtifactsState {
/** artifacts location params */
location: PolicyDetailsArtifactsPageLocation;
/** A list of artifacts can be linked to the policy */
- assignableList: AsyncResourceState;
- /** Represents if avaialble trusted apps entries exist, regardless of whether the list is showing results */
+ assignableList: AsyncResourceState;
+ /** Represents if available trusted apps entries exist, regardless of whether the list is showing results */
assignableListEntriesExist: AsyncResourceState;
/** A list of trusted apps going to be updated */
trustedAppsToUpdate: AsyncResourceState;
+ /** List of artifacts currently assigned to the policy (body specific and global) */
+ assignedList: AsyncResourceState;
+ /** A list of all available polices */
+ policies: AsyncResourceState;
}
export enum OS {
@@ -110,13 +102,17 @@ export enum OS {
linux = 'linux',
}
-export interface PolicyDetailsArtifactsPageLocation {
+export interface PolicyDetailsArtifactsPageListLocationParams {
page_index: number;
page_size: number;
- show?: 'list';
filter: string;
}
+export interface PolicyDetailsArtifactsPageLocation
+ extends PolicyDetailsArtifactsPageListLocationParams {
+ show?: 'list';
+}
+
/**
* Returns the keys of an object whose values meet a criteria.
* Ex) interface largeNestedObject = {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx
index 7046b289063f6..32f6b43a7ac98 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx
@@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import {
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
Immutable,
TrustedApp,
} from '../../../../../../../common/endpoint/types';
@@ -16,7 +16,7 @@ import { Loader } from '../../../../../../common/components/loader';
import { ArtifactEntryCardMinified } from '../../../../../components/artifact_entry_card';
export interface PolicyArtifactsAssignableListProps {
- artifacts: Immutable; // Or other artifacts type like Event Filters or Endpoint Exceptions
+ artifacts: Immutable; // Or other artifacts type like Event Filters or Endpoint Exceptions
selectedArtifactIds: string[];
selectedArtifactsUpdated: (id: string, selected: boolean) => void;
isListLoading: boolean;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx
index 80ee88e826852..da37590dcb9aa 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx
@@ -12,8 +12,8 @@ import { EuiTabbedContent, EuiSpacer, EuiTabbedContentTab } from '@elastic/eui';
import { usePolicyDetailsSelector } from '../policy_hooks';
import {
- isOnPolicyFormPage,
- isOnPolicyTrustedAppsPage,
+ isOnPolicyFormView,
+ isOnPolicyTrustedAppsView,
policyIdFromParams,
} from '../../store/policy_details/selectors';
@@ -23,8 +23,8 @@ import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../commo
export const PolicyTabs = React.memo(() => {
const history = useHistory();
- const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormPage);
- const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsPage);
+ const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormView);
+ const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsView);
const policyId = usePolicyDetailsSelector(policyIdFromParams);
const tabs = useMemo(
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/index.ts
new file mode 100644
index 0000000000000..1360b7ba60e37
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/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 { PolicyTrustedAppsLayout } from './layout';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
index f29b6a9feae3b..d2a0c0867c717 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
@@ -17,6 +17,7 @@ import {
import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors';
import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks';
import { PolicyTrustedAppsFlyout } from '../flyout';
+import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list';
export const PolicyTrustedAppsLayout = React.memo(() => {
const location = usePolicyDetailsSelector(getCurrentArtifactsLocation);
@@ -67,8 +68,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
color="transparent"
borderRadius="none"
>
- {/* TODO: To be implemented */}
- {'Policy trusted apps layout content'}
+
{showListFlyout ? : null}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
new file mode 100644
index 0000000000000..4463a395043a4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import { EuiLoadingSpinner, EuiSpacer, EuiText, Pagination } from '@elastic/eui';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import {
+ ArtifactCardGrid,
+ ArtifactCardGridCardComponentProps,
+ ArtifactCardGridProps,
+} from '../../../../../components/artifact_card_grid';
+import { usePolicyDetailsSelector } from '../../policy_hooks';
+import {
+ doesPolicyHaveTrustedApps,
+ getCurrentArtifactsLocation,
+ getPolicyTrustedAppList,
+ getPolicyTrustedAppsListPagination,
+ getTrustedAppsAllPoliciesById,
+ isPolicyTrustedAppListLoading,
+ policyIdFromParams,
+} from '../../../store/policy_details/selectors';
+import {
+ getPolicyDetailPath,
+ getPolicyDetailsArtifactsListPath,
+ getTrustedAppsListPath,
+} from '../../../../../common/routing';
+import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types';
+import { useAppUrl } from '../../../../../../common/lib/kibana';
+import { APP_ID } from '../../../../../../../common/constants';
+import { ContextMenuItemNavByRouterProps } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
+import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card';
+
+export const PolicyTrustedAppsList = memo(() => {
+ const history = useHistory();
+ const { getAppUrl } = useAppUrl();
+ const policyId = usePolicyDetailsSelector(policyIdFromParams);
+ const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps);
+ const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading);
+ const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList);
+ const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination);
+ const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation);
+ const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById);
+
+ const [isCardExpanded, setCardExpanded] = useState>({});
+
+ // TODO:PT show load errors if any
+
+ const handlePageChange = useCallback(
+ ({ pageIndex, pageSize }) => {
+ history.push(
+ getPolicyDetailsArtifactsListPath(policyId, {
+ ...urlParams,
+ // If user changed page size, then reset page index back to the first page
+ page_index: pageSize !== pagination.pageSize ? 0 : pageIndex,
+ page_size: pageSize,
+ })
+ );
+ },
+ [history, pagination.pageSize, policyId, urlParams]
+ );
+
+ const handleExpandCollapse = useCallback(
+ ({ expanded, collapsed }) => {
+ const newCardExpandedSettings: Record = {};
+
+ for (const trustedApp of expanded) {
+ newCardExpandedSettings[trustedApp.id] = true;
+ }
+
+ for (const trustedApp of collapsed) {
+ newCardExpandedSettings[trustedApp.id] = false;
+ }
+
+ setCardExpanded(newCardExpandedSettings);
+ },
+ []
+ );
+
+ const totalItemsCountLabel = useMemo(() => {
+ return i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalCount', {
+ defaultMessage:
+ 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}',
+ values: { totalItemsCount: pagination.totalItemCount },
+ });
+ }, [pagination.totalItemCount]);
+
+ const cardProps = useMemo, ArtifactCardGridCardComponentProps>>(() => {
+ const newCardProps = new Map();
+
+ for (const trustedApp of trustedAppItems) {
+ const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' });
+ const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] =
+ trustedApp.effectScope.type === 'global'
+ ? undefined
+ : trustedApp.effectScope.policies.reduce<
+ Required['policies']
+ >((byIdPolicies, trustedAppAssignedPolicyId) => {
+ if (!allPoliciesById[trustedAppAssignedPolicyId]) {
+ byIdPolicies[trustedAppAssignedPolicyId] = { children: trustedAppAssignedPolicyId };
+ return byIdPolicies;
+ }
+
+ const policyDetailsPath = getPolicyDetailPath(trustedAppAssignedPolicyId);
+
+ const thisPolicyMenuProps: ContextMenuItemNavByRouterProps = {
+ navigateAppId: APP_ID,
+ navigateOptions: {
+ path: policyDetailsPath,
+ },
+ href: getAppUrl({ path: policyDetailsPath }),
+ children: allPoliciesById[trustedAppAssignedPolicyId].name,
+ };
+
+ byIdPolicies[trustedAppAssignedPolicyId] = thisPolicyMenuProps;
+
+ return byIdPolicies;
+ }, {});
+
+ const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = {
+ expanded: Boolean(isCardExpanded[trustedApp.id]),
+ actions: [
+ {
+ icon: 'controlsHorizontal',
+ children: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction',
+ { defaultMessage: 'View full details' }
+ ),
+ href: getAppUrl({ appId: APP_ID, path: viewUrlPath }),
+ navigateAppId: APP_ID,
+ navigateOptions: { path: viewUrlPath },
+ },
+ ],
+ policies: assignedPoliciesMenuItems,
+ };
+
+ newCardProps.set(trustedApp, thisTrustedAppCardProps);
+ }
+
+ return newCardProps;
+ }, [allPoliciesById, getAppUrl, isCardExpanded, trustedAppItems]);
+
+ const provideCardProps = useCallback['cardComponentProps']>(
+ (item) => {
+ return cardProps.get(item as Immutable)!;
+ },
+ [cardProps]
+ );
+
+ // Anytime a new set of data (trusted apps) is retrieved, reset the card expand state
+ useEffect(() => {
+ setCardExpanded({});
+ }, [trustedAppItems]);
+
+ if (hasTrustedApps.loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!hasTrustedApps.hasTrustedApps) {
+ // TODO: implement empty state (task #1645)
+ return {'No trusted application'}
;
+ }
+
+ return (
+ <>
+
+ {totalItemsCountLabel}
+
+
+
+
+
+ >
+ );
+});
+PolicyTrustedAppsList.displayName = 'PolicyTrustedAppsList';
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts
index c643094e61126..09aa80ffae495 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts
@@ -18,7 +18,7 @@ import {
import {
DeleteTrustedAppsRequestParams,
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
GetTrustedAppsListRequest,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
@@ -36,7 +36,7 @@ import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/servi
export interface TrustedAppsService {
getTrustedApp(params: GetOneTrustedAppRequestParams): Promise;
- getTrustedAppsList(request: GetTrustedAppsListRequest): Promise;
+ getTrustedAppsList(request: GetTrustedAppsListRequest): Promise;
deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise;
createTrustedApp(request: PostTrustedAppCreateRequest): Promise;
updateTrustedApp(
@@ -58,7 +58,7 @@ export class TrustedAppsHttpService implements TrustedAppsService {
}
async getTrustedAppsList(request: GetTrustedAppsListRequest) {
- return this.http.get(TRUSTED_APPS_LIST_API, {
+ return this.http.get(TRUSTED_APPS_LIST_API, {
query: request,
});
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
index 236a93d63bcee..e2b5ad43e40f2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
@@ -407,7 +407,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
class="body-content undefined"
>
{
page: number = 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
per_page: number = 20
- ): GetTrustedListAppsResponse => {
+ ): GetTrustedAppsListResponse => {
return {
data: [getFakeTrustedApp()],
total: 50, // << Should be a value large enough to fulfill two pages
@@ -683,7 +683,7 @@ describe('When on the Trusted Apps Page', () => {
});
describe('and there are no trusted apps', () => {
- const releaseExistsResponse: jest.MockedFunction<() => Promise
> =
+ const releaseExistsResponse: jest.MockedFunction<() => Promise> =
jest.fn(async () => {
return {
data: [],
@@ -692,7 +692,7 @@ describe('When on the Trusted Apps Page', () => {
per_page: 1,
};
});
- const releaseListResponse: jest.MockedFunction<() => Promise> =
+ const releaseListResponse: jest.MockedFunction<() => Promise> =
jest.fn(async () => {
return {
data: [],
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts
index 3fb05c8bf1048..0fcb05827358e 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts
@@ -10,11 +10,17 @@ import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient } from '@kbn/test';
import bluebird from 'bluebird';
import { basename } from 'path';
+import { AxiosResponse } from 'axios';
import { TRUSTED_APPS_CREATE_API, TRUSTED_APPS_LIST_API } from '../../../common/endpoint/constants';
import { TrustedApp } from '../../../common/endpoint/types';
import { TrustedAppGenerator } from '../../../common/endpoint/data_generators/trusted_app_generator';
import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { setupFleetForEndpoint } from '../../../common/endpoint/data_loaders/setup_fleet_for_endpoint';
+import { GetPolicyListResponse } from '../../../public/management/pages/policy/types';
+import {
+ PACKAGE_POLICY_API_ROUTES,
+ PACKAGE_POLICY_SAVED_OBJECT_TYPE,
+} from '../../../../fleet/common';
const defaultLogger = new ToolingLog({ level: 'info', writeTo: process.stdout });
const separator = '----------------------------------------';
@@ -83,21 +89,25 @@ export const run: (options?: RunOptions) => Promise = async ({
}),
]);
- // Setup a list of read endpoint policies and return a method to randomly select one
+ // Setup a list of real endpoint policies and return a method to randomly select one
const randomPolicyId: () => string = await (async () => {
const randomN = (max: number): number => Math.floor(Math.random() * max);
- const policyIds: string[] = [];
-
- for (let i = 0, t = 5; i < t; i++) {
- policyIds.push(
- (
- await indexFleetEndpointPolicy(
- kbnClient,
- `Policy for Trusted App assignment ${i + 1}`,
- installedEndpointPackage.version
- )
- ).integrationPolicies[0].id
- );
+ const policyIds: string[] =
+ (await fetchEndpointPolicies(kbnClient)).data.items.map((policy) => policy.id) || [];
+
+ // If the number of existing policies is less than 5, then create some more policies
+ if (policyIds.length < 5) {
+ for (let i = 0, t = 5 - policyIds.length; i < t; i++) {
+ policyIds.push(
+ (
+ await indexFleetEndpointPolicy(
+ kbnClient,
+ `Policy for Trusted App assignment ${i + 1}`,
+ installedEndpointPackage.version
+ )
+ ).integrationPolicies[0].id
+ );
+ }
}
return () => policyIds[randomN(policyIds.length)];
@@ -153,3 +163,16 @@ const createRunLogger = () => {
},
});
};
+
+const fetchEndpointPolicies = (
+ kbnClient: KbnClient
+): Promise> => {
+ return kbnClient.request({
+ method: 'GET',
+ path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
+ query: {
+ perPage: 100,
+ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
+ },
+ });
+};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
index 7cbdbceaf24cc..9cefc55eddec4 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
@@ -16,7 +16,7 @@ import {
GetOneTrustedAppResponse,
GetTrustedAppsListRequest,
GetTrustedAppsSummaryResponse,
- GetTrustedListAppsResponse,
+ GetTrustedAppsListResponse,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
PutTrustedAppUpdateRequest,
@@ -124,7 +124,7 @@ export const getTrustedApp = async (
export const getTrustedAppsList = async (
exceptionsListClient: ExceptionListClient,
{ page, per_page: perPage, kuery }: GetTrustedAppsListRequest
-): Promise => {
+): Promise => {
// Ensure list is created if it does not exist
await exceptionsListClient.createTrustedAppsList();
From c1f2b1983de66c796dd0ac69d76eab9048616a1a Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 4 Oct 2021 12:49:27 +0100
Subject: [PATCH 29/98] skip flaky suite (#106650)
---
x-pack/test/functional/apps/infra/home_page.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts
index 78fc57e50bf31..c0bcee5f78966 100644
--- a/x-pack/test/functional/apps/infra/home_page.ts
+++ b/x-pack/test/functional/apps/infra/home_page.ts
@@ -86,7 +86,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
- describe('Saved Views', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/106650
+ describe.skip('Saved Views', () => {
before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'));
after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'));
it('should have save and load controls', async () => {
From a99bdcacab9fabb99d50c226876774ea5bf73303 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 4 Oct 2021 13:02:23 +0100
Subject: [PATCH 30/98] skip failing es promotion suites (#113744)
---
.../input_control_vis/input_control_range.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
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']);
From fefc34e28f72264997cbd5b190908eeaa9887097 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 4 Oct 2021 13:08:02 +0100
Subject: [PATCH 31/98] skip failing es promotion suites (#113745)
---
test/functional/apps/management/_scripted_fields.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
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 () {
From edf16e60126427f5101e75b72f046fee7c491065 Mon Sep 17 00:00:00 2001
From: Aleh Zasypkin
Date: Mon, 4 Oct 2021 15:33:21 +0200
Subject: [PATCH 32/98] Remove `jsonwebtoken` and `base64url` dependencies.
(#113723)
---
package.json | 3 --
renovate.json5 | 7 ++--
.../fixtures/oidc/oidc_tools.ts | 41 ++++++++++++-------
yarn.lock | 11 +----
4 files changed, 31 insertions(+), 31 deletions(-)
diff --git a/package.json b/package.json
index ac30b5de6f486..f436a13a057e9 100644
--- a/package.json
+++ b/package.json
@@ -272,7 +272,6 @@
"json-stable-stringify": "^1.0.1",
"json-stringify-pretty-compact": "1.2.0",
"json-stringify-safe": "5.0.1",
- "jsonwebtoken": "^8.5.1",
"jsts": "^1.6.2",
"kea": "^2.4.2",
"load-json-file": "^6.2.0",
@@ -554,7 +553,6 @@
"@types/jsdom": "^16.2.3",
"@types/json-stable-stringify": "^1.0.32",
"@types/json5": "^0.0.30",
- "@types/jsonwebtoken": "^8.5.5",
"@types/license-checker": "15.0.0",
"@types/listr": "^0.14.0",
"@types/loader-utils": "^1.1.3",
@@ -662,7 +660,6 @@
"babel-plugin-styled-components": "^1.13.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"backport": "^5.6.6",
- "base64url": "^3.0.1",
"callsites": "^3.1.0",
"chai": "3.5.0",
"chance": "1.0.18",
diff --git a/renovate.json5 b/renovate.json5
index 12a30876291da..dea7d311bae16 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -86,10 +86,9 @@
{
groupName: 'platform security modules',
packageNames: [
- 'broadcast-channel',
- 'jsonwebtoken', '@types/jsonwebtoken',
- 'node-forge', '@types/node-forge',
- 'require-in-the-middle',
+ 'broadcast-channel',
+ 'node-forge', '@types/node-forge',
+ 'require-in-the-middle',
'tough-cookie', '@types/tough-cookie',
'xml-crypto', '@types/xml-crypto'
],
diff --git a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
index 8d078994eb0e9..3db2e2ebdce0f 100644
--- a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
+++ b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
@@ -5,10 +5,8 @@
* 2.0.
*/
-import base64url from 'base64url';
-import { createHash } from 'crypto';
+import { createHash, createSign } from 'crypto';
import fs from 'fs';
-import jwt from 'jsonwebtoken';
import url from 'url';
export function getStateAndNonce(urlWithStateAndNonce: string) {
@@ -16,16 +14,20 @@ export function getStateAndNonce(urlWithStateAndNonce: string) {
return { state: parsedQuery.state as string, nonce: parsedQuery.nonce as string };
}
+function fromBase64(base64: string) {
+ return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
+}
+
export function createTokens(userId: string, nonce: string) {
- const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem'));
- const iat = Math.floor(Date.now() / 1000);
+ const idTokenHeader = fromBase64(
+ Buffer.from(JSON.stringify({ alg: 'RS256' })).toString('base64')
+ );
+ const iat = Math.floor(Date.now() / 1000);
const accessToken = `valid-access-token${userId}`;
const accessTokenHashBuffer = createHash('sha256').update(accessToken).digest();
-
- return {
- accessToken,
- idToken: jwt.sign(
+ const idTokenBody = fromBase64(
+ Buffer.from(
JSON.stringify({
iss: 'https://test-op.elastic.co',
sub: `user${userId}`,
@@ -34,10 +36,19 @@ export function createTokens(userId: string, nonce: string) {
exp: iat + 3600,
iat,
// See more details on `at_hash` at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
- at_hash: base64url(accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2)),
- }),
- signingKey,
- { algorithm: 'RS256' }
- ),
- };
+ at_hash: fromBase64(
+ accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2).toString('base64')
+ ),
+ })
+ ).toString('base64')
+ );
+
+ const idToken = `${idTokenHeader}.${idTokenBody}`;
+
+ const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem'));
+ const idTokenSignature = fromBase64(
+ createSign('RSA-SHA256').update(idToken).sign(signingKey, 'base64')
+ );
+
+ return { accessToken, idToken: `${idToken}.${idTokenSignature}` };
}
diff --git a/yarn.lock b/yarn.lock
index 14cf34cae847b..83c0691646817 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6394,13 +6394,6 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818"
integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==
-"@types/jsonwebtoken@^8.5.5":
- version "8.5.5"
- resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz#da5f2f4baee88f052ef3e4db4c1a0afb46cff22c"
- integrity sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw==
- dependencies:
- "@types/node" "*"
-
"@types/keyv@*":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
@@ -9235,7 +9228,7 @@ base64-js@^1.0.2, base64-js@^1.1.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-j
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
-base64url@^3.0.0, base64url@^3.0.1:
+base64url@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
@@ -19171,7 +19164,7 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
-jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1:
+jsonwebtoken@^8.3.0:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
From 163133827b894468cb9dce4ed0a4aad493aff1f2 Mon Sep 17 00:00:00 2001
From: Kevin Lacabane
Date: Mon, 4 Oct 2021 15:57:47 +0200
Subject: [PATCH 33/98] [Stack Monitoring] React migration kibana overview
(#113604)
* Create react Kibana template
* React Kibana overview
* Add breadcrumb to kibana overview
* fix linting errors
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/public/application/index.tsx | 11 +-
.../pages/kibana/kibana_template.tsx | 30 +++++
.../application/pages/kibana/overview.tsx | 119 ++++++++++++++++++
3 files changed, 159 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index dea8d18bb65b1..acdf3b0986a64 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -23,7 +23,8 @@ import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview';
import { BeatsOverviewPage } from './pages/beats/overview';
import { BeatsInstancesPage } from './pages/beats/instances';
import { BeatsInstancePage } from './pages/beats/instance';
-import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants';
+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 { ElasticsearchNodePage } from './pages/elasticsearch/node_page';
@@ -133,6 +134,14 @@ const MonitoringApp: React.FC<{
fetchAllClusters={false}
/>
+ {/* Kibana Views */}
+
+
= ({ ...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 (
+
+
+
+ );
+};
From c868cd5c812a82f72fa21b3a4cd261160c910d39 Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Mon, 4 Oct 2021 16:00:08 +0200
Subject: [PATCH 34/98] [Discover] Extract fetch observable initialization to
separate function (#108831)
* Don't trigger autorefresh when there's no time picker
- because there's no UI for that
* Refactor and add test
* Add doc and test
* Refactor
* Remove index pattern without timefield filtering
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../apps/main/services/use_saved_search.ts | 43 ++++-----
.../apps/main/utils/get_fetch_observable.ts | 61 ++++++++++++
.../main/utils/get_fetch_observeable.test.ts | 95 +++++++++++++++++++
3 files changed, 172 insertions(+), 27 deletions(-)
create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
index 164dff8627790..26f95afba5a93 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
@@ -6,15 +6,14 @@
* Side Public License, v 1.
*/
import { useCallback, useEffect, useMemo, useRef } from 'react';
-import { BehaviorSubject, merge, Subject } from 'rxjs';
-import { debounceTime, filter, tap } from 'rxjs/operators';
+import { BehaviorSubject, Subject } from 'rxjs';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverSearchSessionManager } from './discover_search_session';
import { SearchSource } from '../../../../../../data/common';
import { GetStateReturn } from './discover_state';
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
import { RequestAdapter } from '../../../../../../inspector/public';
-import { AutoRefreshDoneFn } from '../../../../../../data/public';
+import type { AutoRefreshDoneFn } from '../../../../../../data/public';
import { validateTimeRange } from '../utils/validate_time_range';
import { Chart } from '../components/chart/point_series';
import { useSingleton } from '../utils/use_singleton';
@@ -23,6 +22,7 @@ import { FetchStatus } from '../../../types';
import { fetchAll } from '../utils/fetch_all';
import { useBehaviorSubject } from '../utils/use_behavior_subject';
import { sendResetMsg } from './use_saved_search_messages';
+import { getFetch$ } from '../utils/get_fetch_observable';
export interface SavedSearchData {
main$: DataMain$;
@@ -134,6 +134,7 @@ export const useSavedSearch = ({
*/
const refs = useRef<{
abortController?: AbortController;
+ autoRefreshDone?: AutoRefreshDoneFn;
}>({});
/**
@@ -145,29 +146,17 @@ export const useSavedSearch = ({
* handler emitted by `timefilter.getAutoRefreshFetch$()`
* to notify when data completed loading and to start a new autorefresh loop
*/
- let autoRefreshDoneCb: AutoRefreshDoneFn | undefined;
- const fetch$ = merge(
+ const setAutoRefreshDone = (fn: AutoRefreshDoneFn | undefined) => {
+ refs.current.autoRefreshDone = fn;
+ };
+ const fetch$ = getFetch$({
+ setAutoRefreshDone,
+ data,
+ main$,
refetch$,
- filterManager.getFetches$(),
- timefilter.getFetch$(),
- timefilter.getAutoRefreshFetch$().pipe(
- tap((done) => {
- autoRefreshDoneCb = done;
- }),
- filter(() => {
- /**
- * filter to prevent auto-refresh triggered fetch when
- * loading is still ongoing
- */
- const currentFetchStatus = main$.getValue().fetchStatus;
- return (
- currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL
- );
- })
- ),
- data.query.queryString.getUpdates$(),
- searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
- ).pipe(debounceTime(100));
+ searchSessionManager,
+ searchSource,
+ });
const subscription = fetch$.subscribe((val) => {
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
@@ -190,8 +179,8 @@ export const useSavedSearch = ({
}).subscribe({
complete: () => {
// if this function was set and is executed, another refresh fetch can be triggered
- autoRefreshDoneCb?.();
- autoRefreshDoneCb = undefined;
+ refs.current.autoRefreshDone?.();
+ refs.current.autoRefreshDone = undefined;
},
});
} catch (error) {
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
new file mode 100644
index 0000000000000..aac6196e64f6f
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { merge } from 'rxjs';
+import { debounceTime, filter, tap } from 'rxjs/operators';
+
+import { FetchStatus } from '../../../types';
+import type {
+ AutoRefreshDoneFn,
+ DataPublicPluginStart,
+ SearchSource,
+} from '../../../../../../data/public';
+import { DataMain$, DataRefetch$ } from '../services/use_saved_search';
+import { DiscoverSearchSessionManager } from '../services/discover_search_session';
+
+/**
+ * This function returns an observable that's used to trigger data fetching
+ */
+export function getFetch$({
+ setAutoRefreshDone,
+ data,
+ main$,
+ refetch$,
+ searchSessionManager,
+}: {
+ setAutoRefreshDone: (val: AutoRefreshDoneFn | undefined) => void;
+ data: DataPublicPluginStart;
+ main$: DataMain$;
+ refetch$: DataRefetch$;
+ searchSessionManager: DiscoverSearchSessionManager;
+ searchSource: SearchSource;
+}) {
+ const { timefilter } = data.query.timefilter;
+ const { filterManager } = data.query;
+ return merge(
+ refetch$,
+ filterManager.getFetches$(),
+ timefilter.getFetch$(),
+ timefilter.getAutoRefreshFetch$().pipe(
+ tap((done) => {
+ setAutoRefreshDone(done);
+ }),
+ filter(() => {
+ const currentFetchStatus = main$.getValue().fetchStatus;
+ return (
+ /**
+ * filter to prevent auto-refresh triggered fetch when
+ * loading is still ongoing
+ */
+ currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL
+ );
+ })
+ ),
+ data.query.queryString.getUpdates$(),
+ searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
+ ).pipe(debounceTime(100));
+}
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
new file mode 100644
index 0000000000000..5f728b115b2e9
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
@@ -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 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 { getFetch$ } from './get_fetch_observable';
+import { FetchStatus } from '../../../types';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { DataPublicPluginStart } from '../../../../../../data/public';
+import { createSearchSessionMock } from '../../../../__mocks__/search_session';
+import { DataRefetch$ } from '../services/use_saved_search';
+import { savedSearchMock, savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search';
+
+function createDataMock(
+ queryString$: Subject,
+ filterManager$: Subject,
+ timefilterFetch$: Subject,
+ autoRefreshFetch$: Subject
+) {
+ return {
+ query: {
+ queryString: {
+ getUpdates$: () => {
+ return queryString$;
+ },
+ },
+ filterManager: {
+ getFetches$: () => {
+ return filterManager$;
+ },
+ },
+ timefilter: {
+ timefilter: {
+ getFetch$: () => {
+ return timefilterFetch$;
+ },
+ getAutoRefreshFetch$: () => {
+ return autoRefreshFetch$;
+ },
+ },
+ },
+ },
+ } as unknown as DataPublicPluginStart;
+}
+
+describe('getFetchObservable', () => {
+ test('refetch$.next should trigger fetch$.next', async (done) => {
+ const searchSessionManagerMock = createSearchSessionMock();
+
+ const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED });
+ const refetch$: DataRefetch$ = new Subject();
+ const fetch$ = getFetch$({
+ setAutoRefreshDone: jest.fn(),
+ main$,
+ refetch$,
+ data: createDataMock(new Subject(), new Subject(), new Subject(), new Subject()),
+ searchSessionManager: searchSessionManagerMock.searchSessionManager,
+ searchSource: savedSearchMock.searchSource,
+ });
+
+ fetch$.subscribe(() => {
+ done();
+ });
+ refetch$.next();
+ });
+ test('getAutoRefreshFetch$ should trigger fetch$.next', async () => {
+ jest.useFakeTimers();
+ const searchSessionManagerMock = createSearchSessionMock();
+ const autoRefreshFetch$ = new Subject();
+
+ const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED });
+ const refetch$: DataRefetch$ = new Subject();
+ const dataMock = createDataMock(new Subject(), new Subject(), new Subject(), autoRefreshFetch$);
+ const setAutoRefreshDone = jest.fn();
+ const fetch$ = getFetch$({
+ setAutoRefreshDone,
+ main$,
+ refetch$,
+ data: dataMock,
+ searchSessionManager: searchSessionManagerMock.searchSessionManager,
+ searchSource: savedSearchMockWithTimeField.searchSource,
+ });
+
+ const fetchfnMock = jest.fn();
+ fetch$.subscribe(() => {
+ fetchfnMock();
+ });
+ autoRefreshFetch$.next(jest.fn());
+ jest.runAllTimers();
+ expect(fetchfnMock).toHaveBeenCalledTimes(1);
+ expect(setAutoRefreshDone).toHaveBeenCalled();
+ });
+});
From 1ff02e1da684a5bbd3d1179db9567b5dd2fe4a97 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Mon, 4 Oct 2021 10:05:01 -0400
Subject: [PATCH 35/98] [Observability] [Exploratory View] Add exploratory view
multi series (#113464)
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* [Observability] [Exploratory View] Create multi series feature branch (#108079)
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* update types
* update tests
* [Observability] exploratory view design issues (#111028)
* remove custom y axis labels for better clarity
* move add series button to the bottom
* disable auto apply
* fix missing test
* When series count changes, collapse other series. (#110894)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* Feature/observability exploratory view multi series panels (#111555)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* [Exploratory View] Fix date range picker on secondary series (#111700)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* [Exploratory View] Collapse series only on add, not delete (#111790)
* [Exploratory view] Remove preview panel (#111884)
* [Exploratory view] implement popovers for data type and metric type (#112370)
* implement popovers for data type and metric type
* adjust types
* add IncompleteBadge
* make report metric dismissable
* show date-picker even if metric is undefined
* adjust styles of expanded series row
* add truncation to series name
* move incomplete badge and add edit pencil
* add tooltip to data type badge
* adjust content
* lint
* delete extra file
* move filters row
* adjust name editing behavior
* adjust filter styles
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* move cases button to top
* fix types
* more types :(
Co-authored-by: Justin Kambic
Co-authored-by: shahzad31
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Shahzad
---
packages/kbn-test/src/jest/utils/get_url.ts | 9 +-
test/functional/page_objects/common_page.ts | 5 +-
.../app/RumDashboard/ActionMenu/index.tsx | 22 +-
.../PageLoadDistribution/index.tsx | 19 +-
.../app/RumDashboard/PageViewsTrend/index.tsx | 19 +-
.../analyze_data_button.test.tsx | 8 +-
.../analyze_data_button.tsx | 37 +-
.../apm/server/lib/rum_client/has_rum_data.ts | 5 +-
.../add_data_buttons/mobile_add_data.tsx | 32 ++
.../add_data_buttons/synthetics_add_data.tsx | 32 ++
.../shared/add_data_buttons/ux_add_data.tsx | 32 ++
.../action_menu/action_menu.test.tsx | 61 ++++
.../components/action_menu/action_menu.tsx | 98 ++++++
.../components/action_menu/index.tsx | 26 ++
.../date_range_picker.tsx | 63 ++--
.../components/empty_view.tsx | 17 +-
.../components/filter_label.test.tsx | 14 +-
.../components/filter_label.tsx | 11 +-
.../components/series_color_picker.tsx | 65 ++++
.../series_date_picker/index.tsx | 35 +-
.../series_date_picker.test.tsx | 59 ++--
.../configurations/constants/constants.ts | 13 +
.../configurations/constants/url_constants.ts | 4 +-
.../configurations/default_configs.ts | 17 +-
.../configurations/lens_attributes.test.ts | 23 +-
.../configurations/lens_attributes.ts | 45 +--
.../mobile/device_distribution_config.ts | 8 +-
.../mobile/distribution_config.ts | 4 +-
.../mobile/kpi_over_time_config.ts | 10 +-
.../rum/core_web_vitals_config.test.ts | 9 +-
.../rum/core_web_vitals_config.ts | 4 +-
.../rum/data_distribution_config.ts | 9 +-
.../rum/kpi_over_time_config.ts | 10 +-
.../synthetics/data_distribution_config.ts | 9 +-
.../synthetics/kpi_over_time_config.ts | 4 +-
.../test_data/sample_attribute.ts | 89 +++--
.../test_data/sample_attribute_cwv.ts | 4 +-
.../test_data/sample_attribute_kpi.ts | 75 ++--
.../exploratory_view/configurations/utils.ts | 27 +-
.../exploratory_view.test.tsx | 34 +-
.../exploratory_view/exploratory_view.tsx | 174 +++++++---
.../header/add_to_case_action.test.tsx | 7 +-
.../header/add_to_case_action.tsx | 3 +-
.../exploratory_view/header/header.test.tsx | 47 +--
.../shared/exploratory_view/header/header.tsx | 87 ++---
.../last_updated.tsx | 21 +-
.../exploratory_view/hooks/use_add_to_case.ts | 2 +-
.../hooks/use_app_index_pattern.tsx | 2 +-
.../hooks/use_discover_link.tsx | 92 +++++
.../hooks/use_lens_attributes.ts | 57 ++-
.../hooks/use_series_filters.ts | 43 ++-
.../hooks/use_series_storage.test.tsx | 91 ++---
.../hooks/use_series_storage.tsx | 123 ++++---
.../shared/exploratory_view/index.tsx | 4 +-
.../exploratory_view/lens_embeddable.tsx | 77 +++-
.../shared/exploratory_view/rtl_helpers.tsx | 61 ++--
.../columns/data_types_col.test.tsx | 62 ----
.../series_builder/columns/data_types_col.tsx | 74 ----
.../columns/date_picker_col.tsx | 39 ---
.../columns/report_breakdowns.test.tsx | 74 ----
.../columns/report_breakdowns.tsx | 26 --
.../columns/report_definition_col.tsx | 101 ------
.../columns/report_filters.test.tsx | 28 --
.../series_builder/columns/report_filters.tsx | 29 --
.../columns/report_types_col.test.tsx | 79 -----
.../columns/report_types_col.tsx | 108 ------
.../series_builder/report_metric_options.tsx | 46 ---
.../series_builder/series_builder.tsx | 303 ----------------
.../series_editor/chart_edit_options.tsx | 30 --
.../series_editor/columns/breakdowns.test.tsx | 22 +-
.../series_editor/columns/breakdowns.tsx | 49 +--
.../series_editor/columns/chart_options.tsx | 35 --
.../columns/chart_type_select.tsx | 73 ++++
.../columns/chart_types.test.tsx | 6 +-
.../columns/chart_types.tsx | 52 ++-
.../columns/data_type_select.test.tsx | 45 +++
.../columns/data_type_select.tsx | 144 ++++++++
.../series_editor/columns/date_picker_col.tsx | 78 ++++-
.../columns/filter_expanded.test.tsx | 48 +--
.../series_editor/columns/filter_expanded.tsx | 139 ++++----
.../columns/filter_value_btn.test.tsx | 145 ++++----
.../columns/filter_value_btn.tsx | 16 +-
.../columns/incomplete_badge.tsx | 63 ++++
.../columns/operation_type_select.test.tsx | 36 +-
.../columns/operation_type_select.tsx | 13 +-
.../series_editor/columns/remove_series.tsx | 41 ++-
.../columns/report_definition_col.test.tsx | 46 +--
.../columns/report_definition_col.tsx | 59 ++++
.../columns/report_definition_field.tsx | 50 ++-
.../columns/report_type_select.tsx | 63 ++++
.../{ => columns}/selected_filters.test.tsx | 22 +-
.../columns/selected_filters.tsx | 101 ++++++
.../series_editor/columns/series_actions.tsx | 139 ++++----
.../series_editor/columns/series_filter.tsx | 145 ++------
.../series_editor/columns/series_info.tsx | 37 ++
.../columns/series_name.test.tsx | 47 +++
.../series_editor/columns/series_name.tsx | 105 ++++++
.../series_editor/expanded_series_row.tsx | 95 +++++
.../series_editor/report_metric_options.tsx | 139 ++++++++
.../series_editor/selected_filters.tsx | 101 ------
.../exploratory_view/series_editor/series.tsx | 93 +++++
.../series_editor/series_editor.tsx | 328 +++++++++++-------
.../shared/exploratory_view/types.ts | 17 +-
.../views/add_series_button.test.tsx | 106 ++++++
.../views/add_series_button.tsx | 80 +++++
.../exploratory_view/views/series_views.tsx | 26 ++
.../exploratory_view/views/view_actions.tsx | 30 ++
.../field_value_combobox.tsx | 61 ++--
.../field_value_selection.tsx | 4 +-
.../field_value_suggestions/index.test.tsx | 2 +
.../shared/field_value_suggestions/index.tsx | 8 +-
.../shared/field_value_suggestions/types.ts | 3 +-
.../filter_value_label/filter_value_label.tsx | 18 +-
.../public/components/shared/index.tsx | 3 +-
.../public/hooks/use_quick_time_ranges.tsx | 2 +-
x-pack/plugins/observability/public/plugin.ts | 2 +
.../observability/public/routes/index.tsx | 16 +-
.../translations/translations/ja-JP.json | 20 --
.../translations/translations/zh-CN.json | 20 --
.../common/charts/ping_histogram.tsx | 25 +-
.../common/header/action_menu_content.tsx | 29 +-
.../monitor_duration_container.tsx | 21 +-
.../apps/observability/exploratory_view.ts | 82 +++++
.../apps/observability/index.ts | 3 +-
124 files changed, 3602 insertions(+), 2508 deletions(-)
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_date_picker => components}/date_range_picker.tsx (58%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_color_picker.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{ => components}/series_date_picker/index.tsx (54%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{ => components}/series_date_picker/series_date_picker.test.tsx (51%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => header}/last_updated.tsx (55%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/chart_types.test.tsx (85%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/chart_types.tsx (77%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/operation_type_select.test.tsx (69%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/operation_type_select.tsx (91%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/report_definition_col.test.tsx (65%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/report_definition_field.tsx (69%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/{ => columns}/selected_filters.test.tsx (59%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
create mode 100644 x-pack/test/observability_functional/apps/observability/exploratory_view.ts
diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts
index 734e26c5199d7..e08695b334e1b 100644
--- a/packages/kbn-test/src/jest/utils/get_url.ts
+++ b/packages/kbn-test/src/jest/utils/get_url.ts
@@ -22,11 +22,6 @@ interface UrlParam {
username?: string;
}
-interface App {
- pathname?: string;
- hash?: string;
-}
-
/**
* Converts a config and a pathname to a url
* @param {object} config A url config
@@ -46,11 +41,11 @@ interface App {
* @return {string}
*/
-function getUrl(config: UrlParam, app: App) {
+function getUrl(config: UrlParam, app: UrlParam) {
return url.format(_.assign({}, config, app));
}
-getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) {
+getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) {
config = _.pickBy(config, function (val, param) {
return param !== 'auth';
});
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/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}}>
{
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/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/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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index c2d46fa5762d2..5da17d8a746a0 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -18947,36 +18947,19 @@
"xpack.observability.expView.operationType.95thPercentile": "95パーセンタイル",
"xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル",
"xpack.observability.expView.operationType.average": "平均",
- "xpack.observability.expView.operationType.label": "計算",
"xpack.observability.expView.operationType.median": "中央",
"xpack.observability.expView.operationType.sum": "合計",
- "xpack.observability.expView.reportType.noDataType": "データ型が選択されていません。",
"xpack.observability.expView.reportType.selectDataType": "ビジュアライゼーションを作成するデータ型を選択します。",
- "xpack.observability.expView.seriesBuilder.actions": "アクション",
"xpack.observability.expView.seriesBuilder.addSeries": "数列を追加",
"xpack.observability.expView.seriesBuilder.apply": "変更を適用",
- "xpack.observability.expView.seriesBuilder.autoApply": "自動適用",
- "xpack.observability.expView.seriesBuilder.breakdown": "内訳",
- "xpack.observability.expView.seriesBuilder.dataType": "データ型",
- "xpack.observability.expView.seriesBuilder.definition": "定義",
"xpack.observability.expView.seriesBuilder.emptyReportDefinition": "ビジュアライゼーションを作成するレポート定義を選択します。",
"xpack.observability.expView.seriesBuilder.emptyview": "表示する情報がありません。",
- "xpack.observability.expView.seriesBuilder.filters": "フィルター",
"xpack.observability.expView.seriesBuilder.loadingView": "ビューを読み込んでいます...",
- "xpack.observability.expView.seriesBuilder.report": "レポート",
- "xpack.observability.expView.seriesBuilder.selectDataType": "データ型が選択されていません",
"xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプが選択されていません",
"xpack.observability.expView.seriesBuilder.selectReportType.empty": "レポートタイプを選択すると、ビジュアライゼーションを作成します。",
- "xpack.observability.expView.seriesEditor.actions": "アクション",
- "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します",
- "xpack.observability.expView.seriesEditor.breakdowns": "内訳",
"xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去",
- "xpack.observability.expView.seriesEditor.filters": "フィルター",
- "xpack.observability.expView.seriesEditor.name": "名前",
"xpack.observability.expView.seriesEditor.notFound": "系列が見つかりません。系列を追加してください。",
"xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します",
- "xpack.observability.expView.seriesEditor.seriesNotFound": "系列が見つかりません。系列を追加してください。",
- "xpack.observability.expView.seriesEditor.time": "時間",
"xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。",
"xpack.observability.featureCatalogueTitle": "オブザーバビリティ",
"xpack.observability.featureRegistry.linkObservabilityTitle": "ケース",
@@ -19038,7 +19021,6 @@
"xpack.observability.overview.ux.title": "ユーザーエクスペリエンス",
"xpack.observability.overviewLinkTitle": "概要",
"xpack.observability.pageLayout.sideNavTitle": "オブザーバビリティ",
- "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません",
"xpack.observability.resources.documentation": "ドキュメント",
"xpack.observability.resources.forum": "ディスカッションフォーラム",
"xpack.observability.resources.quick_start": "クイックスタートビデオ",
@@ -19054,8 +19036,6 @@
"xpack.observability.section.apps.uptime.title": "アップタイム",
"xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください",
"xpack.observability.seriesEditor.clone": "系列をコピー",
- "xpack.observability.seriesEditor.edit": "系列を編集",
- "xpack.observability.seriesEditor.save": "系列を保存",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.average": "平均",
"xpack.observability.ux.coreVitals.averageMessage": " {bad}未満",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index e3f53a34449ef..6d5b94f49cb6f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -19220,36 +19220,19 @@
"xpack.observability.expView.operationType.95thPercentile": "第 95 个百分位",
"xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位",
"xpack.observability.expView.operationType.average": "平均值",
- "xpack.observability.expView.operationType.label": "计算",
"xpack.observability.expView.operationType.median": "中值",
"xpack.observability.expView.operationType.sum": "求和",
- "xpack.observability.expView.reportType.noDataType": "未选择任何数据类型。",
"xpack.observability.expView.reportType.selectDataType": "选择数据类型以创建可视化。",
- "xpack.observability.expView.seriesBuilder.actions": "操作",
"xpack.observability.expView.seriesBuilder.addSeries": "添加序列",
"xpack.observability.expView.seriesBuilder.apply": "应用更改",
- "xpack.observability.expView.seriesBuilder.autoApply": "自动应用",
- "xpack.observability.expView.seriesBuilder.breakdown": "分解",
- "xpack.observability.expView.seriesBuilder.dataType": "数据类型",
- "xpack.observability.expView.seriesBuilder.definition": "定义",
"xpack.observability.expView.seriesBuilder.emptyReportDefinition": "选择报告定义以创建可视化。",
"xpack.observability.expView.seriesBuilder.emptyview": "没有可显示的内容。",
- "xpack.observability.expView.seriesBuilder.filters": "筛选",
"xpack.observability.expView.seriesBuilder.loadingView": "正在加载视图......",
- "xpack.observability.expView.seriesBuilder.report": "报告",
- "xpack.observability.expView.seriesBuilder.selectDataType": "未选择任何数据类型",
"xpack.observability.expView.seriesBuilder.selectReportType": "未选择任何报告类型",
"xpack.observability.expView.seriesBuilder.selectReportType.empty": "选择报告类型以创建可视化。",
- "xpack.observability.expView.seriesEditor.actions": "操作",
- "xpack.observability.expView.seriesEditor.addFilter": "添加筛选",
- "xpack.observability.expView.seriesEditor.breakdowns": "分解",
"xpack.observability.expView.seriesEditor.clearFilter": "清除筛选",
- "xpack.observability.expView.seriesEditor.filters": "筛选",
- "xpack.observability.expView.seriesEditor.name": "名称",
"xpack.observability.expView.seriesEditor.notFound": "未找到任何序列。请添加序列。",
"xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列",
- "xpack.observability.expView.seriesEditor.seriesNotFound": "未找到任何序列。请添加序列。",
- "xpack.observability.expView.seriesEditor.time": "时间",
"xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。",
"xpack.observability.featureCatalogueTitle": "可观测性",
"xpack.observability.featureRegistry.linkObservabilityTitle": "案例",
@@ -19311,7 +19294,6 @@
"xpack.observability.overview.ux.title": "用户体验",
"xpack.observability.overviewLinkTitle": "概览",
"xpack.observability.pageLayout.sideNavTitle": "可观测性",
- "xpack.observability.reportTypeCol.nodata": "没有可用数据",
"xpack.observability.resources.documentation": "文档",
"xpack.observability.resources.forum": "讨论论坛",
"xpack.observability.resources.quick_start": "快速入门视频",
@@ -19327,8 +19309,6 @@
"xpack.observability.section.apps.uptime.title": "运行时间",
"xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试",
"xpack.observability.seriesEditor.clone": "复制序列",
- "xpack.observability.seriesEditor.edit": "编辑序列",
- "xpack.observability.seriesEditor.save": "保存序列",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.average": "平均值",
"xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}",
diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
index 1a53a2c9b64a0..aa981071b7ee2 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
+++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
@@ -22,6 +22,7 @@ import React, { useContext } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import numeral from '@elastic/numeral';
import moment from 'moment';
+import { useSelector } from 'react-redux';
import { getChartDateLabel } from '../../../lib/helper';
import { ChartWrapper } from './chart_wrapper';
import { UptimeThemeContext } from '../../../contexts';
@@ -32,6 +33,7 @@ import { getDateRangeFromChartElement } from './utils';
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations';
import { createExploratoryViewUrl } from '../../../../../observability/public';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
+import { monitorStatusSelector } from '../../../state/selectors';
export interface PingHistogramComponentProps {
/**
@@ -73,6 +75,8 @@ export const PingHistogramComponent: React.FC = ({
const monitorId = useMonitorId();
+ const selectedMonitor = useSelector(monitorStatusSelector);
+
const { basePath } = useUptimeSettingsContext();
const [getUrlParams, updateUrlParams] = useUrlParams();
@@ -189,12 +193,21 @@ export const PingHistogramComponent: React.FC = ({
const pingHistogramExploratoryViewLink = createExploratoryViewUrl(
{
- 'pings-over-time': {
- dataType: 'synthetics',
- reportType: 'kpi-over-time',
- time: { from: dateRangeStart, to: dateRangeEnd },
- ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}),
- },
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${monitorId}-pings`,
+ dataType: 'synthetics',
+ selectedMetricField: 'summary.up',
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ reportDefinitions: {
+ 'monitor.name':
+ monitorId && selectedMonitor?.monitor?.name
+ ? [selectedMonitor.monitor.name]
+ : ['ALL_VALUES'],
+ },
+ },
+ ],
},
basePath
);
diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
index ef5e10394739a..c459fe46da975 100644
--- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
+++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
@@ -10,13 +10,15 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useHistory } from 'react-router-dom';
-import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public';
+import { useSelector } from 'react-redux';
+import { createExploratoryViewUrl } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
import { useGetUrlParams } from '../../../hooks';
import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers';
import { SETTINGS_ROUTE } from '../../../../common/constants';
import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params';
+import { monitorStatusSelector } from '../../../state/selectors';
const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', {
defaultMessage: 'Add data',
@@ -38,13 +40,28 @@ export function ActionMenuContent(): React.ReactElement {
const { dateRangeStart, dateRangeEnd } = params;
const history = useHistory();
+ const selectedMonitor = useSelector(monitorStatusSelector);
+
+ const monitorId = selectedMonitor?.monitor?.id;
+
const syntheticExploratoryViewLink = createExploratoryViewUrl(
{
- 'synthetics-series': {
- dataType: 'synthetics',
- isNew: true,
- time: { from: dateRangeStart, to: dateRangeEnd },
- } as unknown as SeriesUrl,
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ dataType: 'synthetics',
+ seriesType: 'area_stacked',
+ selectedMetricField: 'monitor.duration.us',
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ breakdown: monitorId ? 'observer.geo.name' : 'monitor.type',
+ reportDefinitions: {
+ 'monitor.name': selectedMonitor?.monitor?.name
+ ? [selectedMonitor?.monitor?.name]
+ : ['ALL_VALUES'],
+ },
+ name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration',
+ },
+ ],
},
basePath
);
diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
index cbfba4ffcb239..35eab80c15967 100644
--- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
@@ -51,16 +51,19 @@ export const MonitorDuration: React.FC = ({ monitorId }) => {
const exploratoryViewLink = createExploratoryViewUrl(
{
- [`monitor-duration`]: {
- reportType: 'kpi-over-time',
- time: { from: dateRangeStart, to: dateRangeEnd },
- reportDefinitions: {
- 'monitor.id': [monitorId] as string[],
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${monitorId}-response-duration`,
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ reportDefinitions: {
+ 'monitor.id': [monitorId] as string[],
+ },
+ breakdown: 'observer.geo.name',
+ operationType: 'average',
+ dataType: 'synthetics',
},
- breakdown: 'observer.geo.name',
- operationType: 'average',
- dataType: 'synthetics',
- },
+ ],
},
basePath
);
diff --git a/x-pack/test/observability_functional/apps/observability/exploratory_view.ts b/x-pack/test/observability_functional/apps/observability/exploratory_view.ts
new file mode 100644
index 0000000000000..8f27f20ce30e6
--- /dev/null
+++ b/x-pack/test/observability_functional/apps/observability/exploratory_view.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Path from 'path';
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['observability', 'common', 'header']);
+ const esArchiver = getService('esArchiver');
+ const find = getService('find');
+
+ const testSubjects = getService('testSubjects');
+
+ const rangeFrom = '2021-01-17T16%3A46%3A15.338Z';
+ const rangeTo = '2021-01-19T17%3A01%3A32.309Z';
+
+ // Failing: See https://github.com/elastic/kibana/issues/106934
+ describe.skip('ExploratoryView', () => {
+ before(async () => {
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0')
+ );
+
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0')
+ );
+
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data')
+ );
+
+ await PageObjects.common.navigateToApp('ux', {
+ search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`,
+ });
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ after(async () => {
+ await esArchiver.unload(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0')
+ );
+
+ await esArchiver.unload(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0')
+ );
+ });
+
+ it('should able to open exploratory view from ux app', async () => {
+ await testSubjects.exists('uxAnalyzeBtn');
+ await testSubjects.click('uxAnalyzeBtn');
+ expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true);
+ });
+
+ it('renders lens visualization', async () => {
+ expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true);
+
+ expect(
+ await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]')
+ ).to.eql(true);
+
+ expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true);
+ });
+
+ it('can do a breakdown per series', async () => {
+ await testSubjects.click('seriesBreakdown');
+
+ expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true);
+
+ await find.clickByCssSelector('[id="user_agent.name"]');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true);
+ expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true);
+ });
+ });
+}
diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts
index 019fb0994715e..b163d4d6bb8d5 100644
--- a/x-pack/test/observability_functional/apps/observability/index.ts
+++ b/x-pack/test/observability_functional/apps/observability/index.ts
@@ -8,9 +8,10 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
- describe('Observability specs', function () {
+ describe('ObservabilityApp', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./feature_controls'));
+ loadTestFile(require.resolve('./exploratory_view'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./alerts/workflow_status'));
loadTestFile(require.resolve('./alerts/pagination'));
From 8c89daedba4a9ca4b361bc72f9d00a6094d3c4bf Mon Sep 17 00:00:00 2001
From: ymao1
Date: Mon, 4 Oct 2021 10:06:07 -0400
Subject: [PATCH 36/98] Adding range filter to ownerId aggregation (#113557)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/workload_statistics.test.ts | 27 +++++++++++++++----
.../server/monitoring/workload_statistics.ts | 21 ++++++++++++---
2 files changed, 39 insertions(+), 9 deletions(-)
diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
index 9c697be985155..9628e2807627a 100644
--- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
+++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
@@ -65,7 +65,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 1,
+ ownerIds: {
+ value: 1,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
@@ -127,8 +129,19 @@ describe('Workload Statistics Aggregator', () => {
missing: { field: 'task.schedule' },
},
ownerIds: {
- cardinality: {
- field: 'task.ownerId',
+ filter: {
+ range: {
+ 'task.startedAt': {
+ gte: 'now-1w/w',
+ },
+ },
+ },
+ aggs: {
+ ownerIds: {
+ cardinality: {
+ field: 'task.ownerId',
+ },
+ },
},
},
idleTasks: {
@@ -264,7 +277,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 1,
+ ownerIds: {
+ value: 1,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
@@ -605,7 +620,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 3,
+ ownerIds: {
+ value: 3,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
index b833e4ed57530..9ac528cfd1ced 100644
--- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
+++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
@@ -147,8 +147,19 @@ export function createWorkloadAggregator(
missing: { field: 'task.schedule' },
},
ownerIds: {
- cardinality: {
- field: 'task.ownerId',
+ filter: {
+ range: {
+ 'task.startedAt': {
+ gte: 'now-1w/w',
+ },
+ },
+ },
+ aggs: {
+ ownerIds: {
+ cardinality: {
+ field: 'task.ownerId',
+ },
+ },
},
},
idleTasks: {
@@ -213,7 +224,7 @@ export function createWorkloadAggregator(
const taskTypes = aggregations.taskType.buckets;
const nonRecurring = aggregations.nonRecurringTasks.doc_count;
- const ownerIds = aggregations.ownerIds.value;
+ const ownerIds = aggregations.ownerIds.ownerIds.value;
const {
overdue: {
@@ -448,7 +459,9 @@ export interface WorkloadAggregationResponse {
doc_count: number;
};
ownerIds: {
- value: number;
+ ownerIds: {
+ value: number;
+ };
};
[otherAggs: string]: estypes.AggregationsAggregate;
}
From 69bee186c27aca043ab7393d91e4ad608ae6b35c Mon Sep 17 00:00:00 2001
From: "Joey F. Poon"
Date: Mon, 4 Oct 2021 09:48:01 -0500
Subject: [PATCH 37/98] [Security Solution] create task for auto restarting
failed OLM transforms (#113686)
---
.../security_solution/common/constants.ts | 21 +-
.../common/endpoint/constants.ts | 7 +-
.../management/pages/endpoint_hosts/mocks.ts | 8 +-
.../pages/endpoint_hosts/store/middleware.ts | 6 +-
.../store/mock_endpoint_result_list.ts | 4 +-
.../management/pages/endpoint_hosts/types.ts | 20 +-
.../pages/endpoint_hosts/view/index.test.tsx | 9 +-
.../pages/endpoint_hosts/view/index.tsx | 2 +-
.../check_metadata_transforms_task.test.ts | 250 ++++++++++++++++++
.../check_metadata_transforms_task.ts | 214 +++++++++++++++
.../server/endpoint/lib/metadata/index.ts | 8 +
.../security_solution/server/plugin.ts | 12 +
12 files changed, 526 insertions(+), 35 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 4316b1c033ec6..e91f74320c026 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -7,7 +7,7 @@
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';
@@ -331,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/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
index cf3f53b5b2ea9..010fe48f29418 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
@@ -37,8 +37,8 @@ import {
PendingActionsHttpMockInterface,
pendingActionsHttpMock,
} from '../../../common/lib/endpoint_pending_actions/mocks';
-import { TRANSFORM_STATS_URL } from '../../../../common/constants';
-import { TransformStatsResponse, TRANSFORM_STATE } from './types';
+import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants';
+import { TransformStatsResponse } from './types';
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
metadataList: () => HostResultList;
@@ -238,14 +238,14 @@ export const failedTransformStateMock = {
count: 1,
transforms: [
{
- state: TRANSFORM_STATE.FAILED,
+ state: TRANSFORM_STATES.FAILED,
},
],
};
export const transformsHttpMocks = httpHandlerMockFactory([
{
id: 'metadataTransformStats',
- path: TRANSFORM_STATS_URL,
+ path: METADATA_TRANSFORM_STATS_URL,
method: 'get',
handler: () => failedTransformStateMock,
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index 84cf3513d5d3a..7a45ff06c496b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -78,7 +78,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari
import { EndpointPackageInfoStateChanged } from './action';
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
import { getIsInvalidDateRange } from '../utils';
-import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
+import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
type EndpointPageStore = ImmutableMiddlewareAPI;
@@ -785,7 +785,9 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E
});
try {
- const transformStatsResponse: TransformStatsResponse = await http.get(TRANSFORM_STATS_URL);
+ const transformStatsResponse: TransformStatsResponse = await http.get(
+ METADATA_TRANSFORM_STATS_URL
+ );
dispatch({
type: 'metadataTransformStatsChanged',
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
index 8e8e5a61221a9..2e3de427e6960 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
@@ -30,7 +30,7 @@ import {
import { GetPolicyListResponse } from '../../policy/types';
import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks';
import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants';
-import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
+import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
import { TransformStats, TransformStatsResponse } from '../types';
const generator = new EndpointDocGenerator('seed');
@@ -163,7 +163,7 @@ const endpointListApiPathHandlerMocks = ({
return pendingActionsResponseMock();
},
- [TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
+ [METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
count: transforms.length,
transforms,
}),
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
index dd0bc79f1ba52..0fa96fe00fd2c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
@@ -22,6 +22,7 @@ import { ServerApiError } from '../../../common/types';
import { GetPackagesResponse } from '../../../../../fleet/common';
import { IIndexPattern } from '../../../../../../../src/plugins/data/public';
import { AsyncResourceState } from '../../state';
+import { TRANSFORM_STATES } from '../../../../common/constants';
export interface EndpointState {
/** list of host **/
@@ -143,24 +144,7 @@ export interface EndpointIndexUIQueryParams {
admin_query?: string;
}
-export const TRANSFORM_STATE = {
- ABORTING: 'aborting',
- FAILED: 'failed',
- INDEXING: 'indexing',
- STARTED: 'started',
- STOPPED: 'stopped',
- STOPPING: 'stopping',
- WAITING: 'waiting',
-};
-
-export const WARNING_TRANSFORM_STATES = new Set([
- TRANSFORM_STATE.ABORTING,
- TRANSFORM_STATE.FAILED,
- TRANSFORM_STATE.STOPPED,
- TRANSFORM_STATE.STOPPING,
-]);
-
-const transformStates = Object.values(TRANSFORM_STATE);
+const transformStates = Object.values(TRANSFORM_STATES);
export type TransformState = typeof transformStates[number];
export interface TransformStats {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 33c45e6e2f548..b2c438659b771 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -46,8 +46,9 @@ import {
APP_PATH,
MANAGEMENT_PATH,
DEFAULT_TIMEPICKER_QUICK_RANGES,
+ TRANSFORM_STATES,
} from '../../../../../common/constants';
-import { TransformStats, TRANSFORM_STATE } from '../types';
+import { TransformStats } from '../types';
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
@@ -1403,7 +1404,7 @@ describe('when on the endpoint list page', () => {
const transforms: TransformStats[] = [
{
id: `${metadataTransformPrefix}-0.20.0`,
- state: TRANSFORM_STATE.STARTED,
+ state: TRANSFORM_STATES.STARTED,
} as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
@@ -1414,7 +1415,7 @@ describe('when on the endpoint list page', () => {
it('is not displayed when non-relevant transform is failing', () => {
const transforms: TransformStats[] = [
- { id: 'not-metadata', state: TRANSFORM_STATE.FAILED } as TransformStats,
+ { id: 'not-metadata', state: TRANSFORM_STATES.FAILED } as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
render();
@@ -1426,7 +1427,7 @@ describe('when on the endpoint list page', () => {
const transforms: TransformStats[] = [
{
id: `${metadataTransformPrefix}-0.20.0`,
- state: TRANSFORM_STATE.FAILED,
+ state: TRANSFORM_STATES.FAILED,
} as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index e71474321c868..7845409353898 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -58,8 +58,8 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
import { TableRowActions } from './components/table_row_actions';
import { EndpointAgentStatus } from './components/endpoint_agent_status';
import { CallOut } from '../../../../common/components/callouts';
-import { WARNING_TRANSFORM_STATES } from '../types';
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
+import { WARNING_TRANSFORM_STATES } from '../../../../../common/constants';
const MAX_PAGINATED_ITEM = 9999;
const TRANSFORM_URL = '/data/transform';
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
new file mode 100644
index 0000000000000..0510743fdf05b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
@@ -0,0 +1,250 @@
+/*
+ * 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 { ApiResponse } from '@elastic/elasticsearch';
+import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/api/types';
+import {
+ CheckMetadataTransformsTask,
+ TYPE,
+ VERSION,
+ BASE_NEXT_ATTEMPT_DELAY,
+} from './check_metadata_transforms_task';
+import { createMockEndpointAppContext } from '../../mocks';
+import { coreMock } from '../../../../../../../src/core/server/mocks';
+import { taskManagerMock } from '../../../../../task_manager/server/mocks';
+import { TaskManagerSetupContract, TaskStatus } from '../../../../../task_manager/server';
+import { CoreSetup } from '../../../../../../../src/core/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
+import { TRANSFORM_STATES } from '../../../../common/constants';
+import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
+import { RunResult } from '../../../../../task_manager/server/task';
+
+const MOCK_TASK_INSTANCE = {
+ id: `${TYPE}:${VERSION}`,
+ runAt: new Date(),
+ attempts: 0,
+ ownerId: '',
+ status: TaskStatus.Running,
+ startedAt: new Date(),
+ scheduledAt: new Date(),
+ retryAt: new Date(),
+ params: {},
+ state: {},
+ taskType: TYPE,
+};
+const failedTransformId = 'failing-transform';
+const goodTransformId = 'good-transform';
+
+describe('check metadata transforms task', () => {
+ const { createSetup: coreSetupMock } = coreMock;
+ const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock;
+
+ let mockTask: CheckMetadataTransformsTask;
+ let mockCore: CoreSetup;
+ let mockTaskManagerSetup: jest.Mocked;
+ beforeAll(() => {
+ mockCore = coreSetupMock();
+ mockTaskManagerSetup = tmSetupMock();
+ mockTask = new CheckMetadataTransformsTask({
+ endpointAppContext: createMockEndpointAppContext(),
+ core: mockCore,
+ taskManager: mockTaskManagerSetup,
+ });
+ });
+
+ describe('task lifecycle', () => {
+ it('should create task', () => {
+ expect(mockTask).toBeInstanceOf(CheckMetadataTransformsTask);
+ });
+
+ it('should register task', () => {
+ expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled();
+ });
+
+ it('should schedule task', async () => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
+ });
+ });
+
+ describe('task logic', () => {
+ let esClient: ElasticsearchClientMock;
+ beforeEach(async () => {
+ const [{ elasticsearch }] = await mockCore.getStartServices();
+ esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock;
+ });
+
+ const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ const createTaskRunner =
+ mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner;
+ const taskRunner = createTaskRunner({ taskInstance });
+ return taskRunner.run();
+ };
+
+ const buildFailedStatsResponse = () =>
+ ({
+ body: {
+ transforms: [
+ {
+ id: goodTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ {
+ id: failedTransformId,
+ state: TRANSFORM_STATES.FAILED,
+ },
+ ],
+ },
+ } as unknown as ApiResponse);
+
+ it('should stop task if transform stats response fails', async () => {
+ esClient.transform.getTransformStats.mockRejectedValue({});
+ await runTask();
+ expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ expect(esClient.transform.stopTransform).not.toHaveBeenCalled();
+ expect(esClient.transform.startTransform).not.toHaveBeenCalled();
+ });
+
+ it('should attempt transform restart if failing state', async () => {
+ const transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ const taskResponse = (await runTask()) as RunResult;
+
+ expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ expect(esClient.transform.stopTransform).toHaveBeenCalledWith({
+ transform_id: failedTransformId,
+ allow_no_match: true,
+ wait_for_completion: true,
+ force: true,
+ });
+ expect(esClient.transform.startTransform).toHaveBeenCalledWith({
+ transform_id: failedTransformId,
+ });
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 0,
+ });
+ });
+
+ it('should correctly track transform restart attempts', async () => {
+ const transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ let taskResponse = (await runTask()) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 1,
+ });
+
+ esClient.transform.startTransform.mockRejectedValueOnce({});
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 2,
+ });
+
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 0,
+ });
+ });
+
+ it('should correctly back off subsequent restart attempts', async () => {
+ let transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ let taskStartedAt = new Date();
+ let taskResponse = (await runTask()) as RunResult;
+ let delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
+ let expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ let expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ esClient.transform.startTransform.mockRejectedValueOnce({});
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // should be exponential on second+ attempt
+ delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // should be exponential on second+ attempt
+ delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // back to base delay after success
+ delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ transformStatsResponseMock = {
+ body: {
+ transforms: [
+ {
+ id: goodTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ {
+ id: failedTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ ],
+ },
+ } as unknown as ApiResponse;
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // no more explicit runAt after subsequent success
+ expect(taskResponse?.runAt).toBeUndefined();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
new file mode 100644
index 0000000000000..68f149bcc64c4
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
@@ -0,0 +1,214 @@
+/*
+ * 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 { ApiResponse } from '@elastic/elasticsearch';
+import {
+ TransformGetTransformStatsResponse,
+ TransformGetTransformStatsTransformStats,
+} from '@elastic/elasticsearch/api/types';
+import { CoreSetup, ElasticsearchClient, Logger } from 'src/core/server';
+import {
+ ConcreteTaskInstance,
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+ throwUnrecoverableError,
+} from '../../../../../task_manager/server';
+import { EndpointAppContext } from '../../types';
+import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
+import { WARNING_TRANSFORM_STATES } from '../../../../common/constants';
+import { wrapErrorIfNeeded } from '../../utils';
+
+const SCOPE = ['securitySolution'];
+const INTERVAL = '2h';
+const TIMEOUT = '4m';
+export const TYPE = 'endpoint:metadata-check-transforms-task';
+export const VERSION = '0.0.1';
+const MAX_ATTEMPTS = 5;
+export const BASE_NEXT_ATTEMPT_DELAY = 5; // minutes
+
+export interface CheckMetadataTransformsTaskSetupContract {
+ endpointAppContext: EndpointAppContext;
+ core: CoreSetup;
+ taskManager: TaskManagerSetupContract;
+}
+
+export interface CheckMetadataTransformsTaskStartContract {
+ taskManager: TaskManagerStartContract;
+}
+
+export class CheckMetadataTransformsTask {
+ private logger: Logger;
+ private wasStarted: boolean = false;
+
+ constructor(setupContract: CheckMetadataTransformsTaskSetupContract) {
+ const { endpointAppContext, core, taskManager } = setupContract;
+ this.logger = endpointAppContext.logFactory.get(this.getTaskId());
+ taskManager.registerTaskDefinitions({
+ [TYPE]: {
+ title: 'Security Solution Endpoint Metadata Periodic Tasks',
+ timeout: TIMEOUT,
+ createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
+ return {
+ run: async () => {
+ return this.runTask(taskInstance, core);
+ },
+ cancel: async () => {},
+ };
+ },
+ },
+ });
+ }
+
+ public start = async ({ taskManager }: CheckMetadataTransformsTaskStartContract) => {
+ if (!taskManager) {
+ this.logger.error('missing required service during start');
+ return;
+ }
+
+ this.wasStarted = true;
+
+ try {
+ await taskManager.ensureScheduled({
+ id: this.getTaskId(),
+ taskType: TYPE,
+ scope: SCOPE,
+ schedule: {
+ interval: INTERVAL,
+ },
+ state: {
+ attempts: {},
+ },
+ params: { version: VERSION },
+ });
+ } catch (e) {
+ this.logger.debug(`Error scheduling task, received ${e.message}`);
+ }
+ };
+
+ private runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => {
+ // if task was not `.start()`'d yet, then exit
+ if (!this.wasStarted) {
+ this.logger.debug('[runTask()] Aborted. MetadataTask not started yet');
+ return;
+ }
+
+ // Check that this task is current
+ if (taskInstance.id !== this.getTaskId()) {
+ // old task, die
+ throwUnrecoverableError(new Error('Outdated task version'));
+ }
+
+ const [{ elasticsearch }] = await core.getStartServices();
+ const esClient = elasticsearch.client.asInternalUser;
+
+ let transformStatsResponse: ApiResponse;
+ try {
+ transformStatsResponse = await esClient?.transform.getTransformStats({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ } catch (e) {
+ const err = wrapErrorIfNeeded(e);
+ const errMessage = `failed to get transform stats with error: ${err}`;
+ this.logger.error(errMessage);
+
+ return;
+ }
+
+ const { transforms } = transformStatsResponse.body;
+ if (!transforms.length) {
+ this.logger.info('no OLM metadata transforms found');
+ return;
+ }
+
+ let didAttemptRestart: boolean = false;
+ let highestAttempt: number = 0;
+ const attempts = { ...taskInstance.state.attempts };
+
+ for (const transform of transforms) {
+ const restartedTransform = await this.restartTransform(
+ esClient,
+ transform,
+ attempts[transform.id]
+ );
+ if (restartedTransform.didAttemptRestart) {
+ didAttemptRestart = true;
+ }
+ attempts[transform.id] = restartedTransform.attempts;
+ highestAttempt = Math.max(attempts[transform.id], highestAttempt);
+ }
+
+ // after a restart attempt run next check sooner with exponential backoff
+ let runAt: Date | undefined;
+ if (didAttemptRestart) {
+ const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(highestAttempt, 1) * 60000;
+ runAt = new Date(new Date().getTime() + delay);
+ }
+
+ const nextState = { attempts };
+ const nextTask = runAt ? { state: nextState, runAt } : { state: nextState };
+ return nextTask;
+ };
+
+ private restartTransform = async (
+ esClient: ElasticsearchClient,
+ transform: TransformGetTransformStatsTransformStats,
+ currentAttempts: number = 0
+ ) => {
+ let attempts = currentAttempts;
+ let didAttemptRestart = false;
+
+ if (!WARNING_TRANSFORM_STATES.has(transform.state)) {
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ }
+
+ if (attempts > MAX_ATTEMPTS) {
+ this.logger.warn(
+ `transform ${transform.id} has failed to restart ${attempts} times. stopping auto restart attempts.`
+ );
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ }
+
+ try {
+ this.logger.info(`failed transform detected with id: ${transform.id}. attempting restart.`);
+ await esClient.transform.stopTransform({
+ transform_id: transform.id,
+ allow_no_match: true,
+ wait_for_completion: true,
+ force: true,
+ });
+ await esClient.transform.startTransform({
+ transform_id: transform.id,
+ });
+
+ // restart succeeded, reset attempt count
+ attempts = 0;
+ } catch (e) {
+ const err = wrapErrorIfNeeded(e);
+ const errMessage = `failed to restart transform ${transform.id} with error: ${err}`;
+ this.logger.error(errMessage);
+
+ // restart failed, increment attempt count
+ attempts = attempts + 1;
+ } finally {
+ didAttemptRestart = true;
+ }
+
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ };
+
+ private getTaskId = (): string => {
+ return `${TYPE}:${VERSION}`;
+ };
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts
new file mode 100644
index 0000000000000..6f5d6f5a4121b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/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 * from './check_metadata_transforms_task';
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 391beb3c40121..f69565cacceb5 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -59,6 +59,7 @@ import { initRoutes } from './routes';
import { isAlertExecutor } from './lib/detection_engine/signals/types';
import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type';
import { ManifestTask } from './endpoint/lib/artifacts';
+import { CheckMetadataTransformsTask } from './endpoint/lib/metadata';
import { initSavedObjects } from './saved_objects';
import { AppClientFactory } from './client';
import { createConfig, ConfigType } from './config';
@@ -157,6 +158,7 @@ export class Plugin implements IPlugin;
private telemetryUsageCounter?: UsageCounter;
@@ -363,6 +365,12 @@ export class Plugin implements IPlugin
Date: Mon, 4 Oct 2021 10:52:05 -0400
Subject: [PATCH 38/98] [Fleet] Fix how we get the default output in the Fleet
UI (#113620)
---
.../fleet_server_on_prem_instructions.tsx | 6 ++----
.../public/components/settings_flyout/index.tsx | 5 ++---
.../fleet/public/hooks/use_request/outputs.ts | 17 +++++++++++++++++
3 files changed, 21 insertions(+), 7 deletions(-)
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/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_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',
From c558f26dd13e7949edf19071d9eaa15d5dd05bad Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 4 Oct 2021 17:55:49 +0300
Subject: [PATCH 39/98] [TSVB] Update the series and metrics Ids that are
numbers to strings (#113619)
* [TSVB] Update the series and metrics Ids that are numbers to strings
* Minor changes
* Adds a unit test to TSVB plugin to test this case
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../timeseries/public/vis_state.test.ts | 126 ++++++++++++++++++
.../public/legacy/vis_update_state.js | 28 ++++
.../public/legacy/vis_update_state.test.js | 83 ++++++++++++
3 files changed, 237 insertions(+)
create mode 100644 src/plugins/vis_types/timeseries/public/vis_state.test.ts
diff --git a/src/plugins/vis_types/timeseries/public/vis_state.test.ts b/src/plugins/vis_types/timeseries/public/vis_state.test.ts
new file mode 100644
index 0000000000000..82e52a0493391
--- /dev/null
+++ b/src/plugins/vis_types/timeseries/public/vis_state.test.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { updateOldState } from '../../../visualizations/public';
+
+/**
+ * The reason we add this test is to ensure that `convertNumIdsToStringsForTSVB` of the updateOldState runs correctly
+ * for the TSVB vis state. As the `updateOldState` runs on the visualizations plugin. a change to our objects structure can
+ * result to forget this case.
+ * Just for reference the `convertNumIdsToStringsForTSVB` finds and converts the series and metrics ids that have only digits to strings
+ * by adding an x prefix. Number ids are never been generated from the editor, only programmatically.
+ * See https://github.com/elastic/kibana/issues/113601.
+ */
+describe('TimeseriesVisState', () => {
+ test('should format the TSVB visState correctly', () => {
+ const visState = {
+ title: 'test',
+ type: 'metrics',
+ aggs: [],
+ params: {
+ time_range_mode: 'entire_time_range',
+ id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497',
+ type: 'timeseries',
+ series: [
+ {
+ id: '1',
+ color: '#68BC00',
+ split_mode: 'terms',
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ metrics: [
+ {
+ id: '10',
+ type: 'count',
+ },
+ ],
+ separate_axis: 0,
+ axis_position: 'right',
+ formatter: 'default',
+ chart_type: 'line',
+ line_width: 1,
+ point_size: 1,
+ fill: 0.5,
+ stacked: 'none',
+ terms_field: 'Cancelled',
+ },
+ ],
+ time_field: '',
+ use_kibana_indexes: true,
+ interval: '',
+ axis_position: 'left',
+ axis_formatter: 'number',
+ axis_scale: 'normal',
+ show_legend: 1,
+ truncate_legend: 1,
+ max_lines_legend: 1,
+ show_grid: 1,
+ tooltip_mode: 'show_all',
+ drop_last_bucket: 0,
+ isModelInvalid: false,
+ index_pattern: {
+ id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b',
+ },
+ },
+ };
+ const newVisState = updateOldState(visState);
+ expect(newVisState).toEqual({
+ aggs: [],
+ params: {
+ axis_formatter: 'number',
+ axis_position: 'left',
+ axis_scale: 'normal',
+ drop_last_bucket: 0,
+ id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497',
+ index_pattern: {
+ id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b',
+ },
+ interval: '',
+ isModelInvalid: false,
+ max_lines_legend: 1,
+ series: [
+ {
+ axis_position: 'right',
+ chart_type: 'line',
+ color: '#68BC00',
+ fill: 0.5,
+ formatter: 'default',
+ id: 'x1',
+ line_width: 1,
+ metrics: [
+ {
+ id: 'x10',
+ type: 'count',
+ },
+ ],
+ palette: {
+ name: 'default',
+ type: 'palette',
+ },
+ point_size: 1,
+ separate_axis: 0,
+ split_mode: 'terms',
+ stacked: 'none',
+ terms_field: 'Cancelled',
+ },
+ ],
+ show_grid: 1,
+ show_legend: 1,
+ time_field: '',
+ time_range_mode: 'entire_time_range',
+ tooltip_mode: 'show_all',
+ truncate_legend: 1,
+ type: 'timeseries',
+ use_kibana_indexes: true,
+ },
+ title: 'test',
+ type: 'metrics',
+ });
+ });
+});
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',
+ },
+ ],
+ },
+ ]);
+ });
+ });
});
From 252278a433850ed7775debcd1c74b8e63ef286ba Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Mon, 4 Oct 2021 10:57:32 -0400
Subject: [PATCH 40/98] [buildkite] Fix packer cache issues (#113769)
---
.buildkite/scripts/common/env.sh | 4 ++--
.buildkite/scripts/packer_cache.sh | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh
index 89121581c75d1..ac80a66d33fa0 100755
--- a/.buildkite/scripts/common/env.sh
+++ b/.buildkite/scripts/common/env.sh
@@ -56,8 +56,8 @@ else
fi
# These are for backwards-compatibility
-export GIT_COMMIT="$BUILDKITE_COMMIT"
-export GIT_BRANCH="$BUILDKITE_BRANCH"
+export GIT_COMMIT="${BUILDKITE_COMMIT:-}"
+export GIT_BRANCH="${BUILDKITE_BRANCH:-}"
export FLEET_PACKAGE_REGISTRY_PORT=6104
export TEST_CORS_SERVER_PORT=6105
diff --git a/.buildkite/scripts/packer_cache.sh b/.buildkite/scripts/packer_cache.sh
index 45d3dc439ff4d..617ea79c827b0 100755
--- a/.buildkite/scripts/packer_cache.sh
+++ b/.buildkite/scripts/packer_cache.sh
@@ -2,6 +2,7 @@
set -euo pipefail
+source .buildkite/scripts/common/util.sh
source .buildkite/scripts/common/env.sh
source .buildkite/scripts/common/setup_node.sh
From 3d7e04b799b11407e2689cde532a7f82297b4874 Mon Sep 17 00:00:00 2001
From: Domenico Andreoli
Date: Mon, 4 Oct 2021 17:04:24 +0200
Subject: [PATCH 41/98] [Security] Add EQL rule test in CCS config (#112852)
---
.../event_correlation_rule.spec.ts | 55 ++
.../security_solution/cypress/objects/rule.ts | 23 +
.../cypress/tasks/api_calls/rules.ts | 31 +-
.../es_archives/linux_process/data.json | 135 +++
.../es_archives/linux_process/mappings.json | 935 ++++++++++++++++++
5 files changed, 1178 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts
create mode 100644 x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
create mode 100644 x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
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/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/test/security_solution_cypress/es_archives/linux_process/data.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
new file mode 100644
index 0000000000000..ed29f3fe3e4e1
--- /dev/null
+++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
@@ -0,0 +1,135 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "qxnqn3sBBf0WZxoXk7tg",
+ "index": "run-parts",
+ "source": {
+ "@timestamp": "2021-09-01T05:52:29.9451497Z",
+ "agent": {
+ "id": "cda623db-f791-4869-a63d-5b8352dfaa56",
+ "type": "endpoint",
+ "version": "7.14.0"
+ },
+ "data_stream": {
+ "dataset": "endpoint.events.process",
+ "namespace": "default",
+ "type": "logs"
+ },
+ "ecs": {
+ "version": "1.6.0"
+ },
+ "elastic": {
+ "agent": {
+ "id": "cda623db-f791-4869-a63d-5b8352dfaa56"
+ }
+ },
+ "event": {
+ "action": "exec",
+ "agent_id_status": "verified",
+ "category": [
+ "process"
+ ],
+ "created": "2021-09-01T05:52:29.9451497Z",
+ "dataset": "endpoint.events.process",
+ "id": "MGwI0NpfzFKkX6gW+++++CVd",
+ "ingested": "2021-09-01T05:52:35.677424686Z",
+ "kind": "event",
+ "module": "endpoint",
+ "sequence": 3523,
+ "type": [
+ "start"
+ ]
+ },
+ "group": {
+ "Ext": {
+ "real": {
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "id": 0,
+ "name": "root"
+ },
+ "host": {
+ "architecture": "x86_64",
+ "hostname": "localhost",
+ "id": "f5c59e5f0c963f828782bc413653d324",
+ "ip": [
+ "127.0.0.1",
+ "::1"
+ ],
+ "mac": [
+ "00:16:3e:10:96:79"
+ ],
+ "name": "localhost",
+ "os": {
+ "Ext": {
+ "variant": "Debian"
+ },
+ "family": "debian",
+ "full": "Debian 10",
+ "kernel": "4.19.0-17-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18)",
+ "name": "Linux",
+ "platform": "debian",
+ "version": "10"
+ }
+ },
+ "message": "Endpoint process event",
+ "process": {
+ "Ext": {
+ "ancestry": [
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTMwNzYyMTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI4OTI0ODAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI3NDgwMzAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTI3ODA5NTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTIzNzEzOTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNzgyMjQwMDA=",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNTQ1MTUzMDA=",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEtMTMyNzQ5NDg4NjkuMA=="
+ ]
+ },
+ "args": [
+ "run-parts",
+ "--lsbsysinit",
+ "/etc/update-motd.d"
+ ],
+ "args_count": 3,
+ "command_line": "run-parts --lsbsysinit /etc/update-motd.d",
+ "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTQ1MTQ5NzAw",
+ "executable": "/usr/bin/run-parts",
+ "hash": {
+ "md5": "c83b0578484bf5267893d795b55928bd",
+ "sha1": "46b6e74e28e5daf69c1dd0f18a8e911ae2922dda",
+ "sha256": "3346b4d47c637a8c02cb6865eee42d2a5aa9c4e46c6371a9143621348d27420f"
+ },
+ "name": "run-parts",
+ "parent": {
+ "args": [
+ "sh",
+ "-c",
+ "/usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new"
+ ],
+ "args_count": 0,
+ "command_line": "sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new",
+ "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw",
+ "executable": "/",
+ "name": "",
+ "pid": 1349
+ },
+ "pid": 1350
+ },
+ "user": {
+ "Ext": {
+ "real": {
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "type": "_doc"
+ }
+}
diff --git a/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
new file mode 100644
index 0000000000000..d244defbdab0b
--- /dev/null
+++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
@@ -0,0 +1,935 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ },
+ "index": "run-parts",
+ "mappings": {
+ "_data_stream_timestamp": {
+ "enabled": true
+ },
+ "_meta": {
+ "managed": true,
+ "managed_by": "ingest-manager",
+ "package": {
+ "name": "endpoint"
+ }
+ },
+ "date_detection": false,
+ "dynamic": "false",
+ "dynamic_templates": [
+ {
+ "strings_as_keyword": {
+ "mapping": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "match_mapping_type": "string"
+ }
+ }
+ ],
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "agent": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "data_stream": {
+ "properties": {
+ "dataset": {
+ "type": "constant_keyword",
+ "value": "endpoint.events.process"
+ },
+ "namespace": {
+ "type": "constant_keyword",
+ "value": "default"
+ },
+ "type": {
+ "type": "constant_keyword",
+ "value": "logs"
+ }
+ }
+ },
+ "destination": {
+ "properties": {
+ "geo": {
+ "properties": {
+ "city_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "continent_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "location": {
+ "type": "geo_point"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "ecs": {
+ "properties": {
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "event": {
+ "properties": {
+ "action": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "agent_id_status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "category": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "created": {
+ "type": "date"
+ },
+ "dataset": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "hash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "ingested": {
+ "type": "date"
+ },
+ "kind": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "module": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "outcome": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "provider": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sequence": {
+ "type": "long"
+ },
+ "severity": {
+ "type": "long"
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "group": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "host": {
+ "properties": {
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "hostname": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "ip": {
+ "type": "ip"
+ },
+ "mac": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "os": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "variant": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "family": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "full": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "kernel": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "platform": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ }
+ }
+ },
+ "message": {
+ "type": "text"
+ },
+ "package": {
+ "properties": {
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "process": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "ancestry": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "authentication_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ },
+ "type": "nested"
+ },
+ "defense_evasions": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "dll": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "mapped_address": {
+ "type": "unsigned_long"
+ },
+ "mapped_size": {
+ "type": "unsigned_long"
+ }
+ }
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "path": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "protection": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "session": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "token": {
+ "properties": {
+ "elevation": {
+ "type": "boolean"
+ },
+ "elevation_level": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "elevation_type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "integrity_level_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "args": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "args_count": {
+ "type": "long"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ }
+ },
+ "command_line": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "entity_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "executable": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "exit_code": {
+ "type": "long"
+ },
+ "hash": {
+ "properties": {
+ "md5": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha1": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha256": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha512": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "parent": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ },
+ "type": "nested"
+ },
+ "protection": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "real": {
+ "properties": {
+ "pid": {
+ "type": "long"
+ }
+ }
+ },
+ "user": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "args": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "args_count": {
+ "type": "long"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ }
+ },
+ "command_line": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "entity_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "executable": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "exit_code": {
+ "type": "long"
+ },
+ "hash": {
+ "properties": {
+ "md5": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha1": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha256": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha512": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "pe": {
+ "properties": {
+ "company": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "description": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "file_version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "imphash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "original_file_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "product": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pgid": {
+ "type": "long"
+ },
+ "pid": {
+ "type": "long"
+ },
+ "ppid": {
+ "type": "long"
+ },
+ "thread": {
+ "properties": {
+ "id": {
+ "type": "long"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "title": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ },
+ "working_directory": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pe": {
+ "properties": {
+ "company": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "description": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "file_version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "imphash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "original_file_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "product": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pgid": {
+ "type": "long"
+ },
+ "pid": {
+ "type": "long"
+ },
+ "ppid": {
+ "type": "long"
+ },
+ "thread": {
+ "properties": {
+ "id": {
+ "type": "long"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "title": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ },
+ "working_directory": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "source": {
+ "properties": {
+ "geo": {
+ "properties": {
+ "city_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "continent_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "location": {
+ "type": "geo_point"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "user": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "email": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "full_name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "group": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "hash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_replicas": "0",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
From 10ca6f42d6910352bb592ccc1d6ee8faf714488d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 4 Oct 2021 11:11:08 -0400
Subject: [PATCH 42/98] [APM] Show APM Server stand-alone mode in Kibana
Upgrade Assistant (cloud-only) (#113567)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../server/deprecations/deprecations.test.ts | 70 ++++++++++++++++++
.../plugins/apm/server/deprecations/index.ts | 74 +++++++++++++++++++
x-pack/plugins/apm/server/plugin.ts | 7 ++
3 files changed, 151 insertions(+)
create mode 100644 x-pack/plugins/apm/server/deprecations/deprecations.test.ts
create mode 100644 x-pack/plugins/apm/server/deprecations/index.ts
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/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$,
From 4693c3812e5decfefcb6bc8835702e141a5f3ed6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Mon, 4 Oct 2021 16:19:03 +0100
Subject: [PATCH 43/98] =?UTF-8?q?[console]=C2=A0Deprecate=20"proxyFilter"?=
=?UTF-8?q?=20and=20"proxyConfig"=20on=208.x=20(#113555)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/plugins/console/common/constants/index.ts | 9 ++
.../console/common/constants/plugin.ts | 9 ++
src/plugins/console/kibana.json | 4 +-
src/plugins/console/server/config.ts | 87 ++++++++++-----
src/plugins/console/server/index.ts | 9 +-
src/plugins/console/server/plugin.ts | 22 +++-
.../api/console/proxy/create_handler.ts | 44 +++++---
.../server/routes/api/console/proxy/mocks.ts | 32 ++++--
.../routes/api/console/proxy/params.test.ts | 104 ++++++++++--------
src/plugins/console/server/routes/index.ts | 6 +-
10 files changed, 214 insertions(+), 112 deletions(-)
create mode 100644 src/plugins/console/common/constants/index.ts
create mode 100644 src/plugins/console/common/constants/plugin.ts
diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts
new file mode 100644
index 0000000000000..0a8dac9b7fff3
--- /dev/null
+++ b/src/plugins/console/common/constants/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { MAJOR_VERSION } from './plugin';
diff --git a/src/plugins/console/common/constants/plugin.ts b/src/plugins/console/common/constants/plugin.ts
new file mode 100644
index 0000000000000..cd301ec296395
--- /dev/null
+++ b/src/plugins/console/common/constants/plugin.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const MAJOR_VERSION = '8.0.0';
diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json
index 69c7176ff6a47..e2345514d76b9 100644
--- a/src/plugins/console/kibana.json
+++ b/src/plugins/console/kibana.json
@@ -1,12 +1,14 @@
{
"id": "console",
- "version": "kibana",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
"server": true,
"ui": true,
"owner": {
"name": "Stack Management",
"githubTeam": "kibana-stack-management"
},
+ "configPath": ["console"],
"requiredPlugins": ["devTools", "share"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"]
diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts
index 4e42e3c21d2ad..6d667fed081e8 100644
--- a/src/plugins/console/server/config.ts
+++ b/src/plugins/console/server/config.ts
@@ -6,37 +6,70 @@
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
import { schema, TypeOf } from '@kbn/config-schema';
+import { PluginConfigDescriptor } from 'kibana/server';
-export type ConfigType = TypeOf;
+import { MAJOR_VERSION } from '../common/constants';
-export const config = schema.object(
- {
- enabled: schema.boolean({ defaultValue: true }),
- proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
- ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
- proxyConfig: schema.arrayOf(
- schema.object({
- match: schema.object({
- protocol: schema.string({ defaultValue: '*' }),
- host: schema.string({ defaultValue: '*' }),
- port: schema.string({ defaultValue: '*' }),
- path: schema.string({ defaultValue: '*' }),
- }),
-
- timeout: schema.number(),
- ssl: schema.object(
- {
- verify: schema.boolean(),
- ca: schema.arrayOf(schema.string()),
- cert: schema.string(),
- key: schema.string(),
- },
- { defaultValue: undefined }
- ),
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
+const baseSettings = {
+ enabled: schema.boolean({ defaultValue: true }),
+ ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
+};
+
+// Settings only available in 7.x
+const deprecatedSettings = {
+ proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
+ proxyConfig: schema.arrayOf(
+ schema.object({
+ match: schema.object({
+ protocol: schema.string({ defaultValue: '*' }),
+ host: schema.string({ defaultValue: '*' }),
+ port: schema.string({ defaultValue: '*' }),
+ path: schema.string({ defaultValue: '*' }),
}),
- { defaultValue: [] }
- ),
+
+ timeout: schema.number(),
+ ssl: schema.object(
+ {
+ verify: schema.boolean(),
+ ca: schema.arrayOf(schema.string()),
+ cert: schema.string(),
+ key: schema.string(),
+ },
+ { defaultValue: undefined }
+ ),
+ }),
+ { defaultValue: [] }
+ ),
+};
+
+const configSchema = schema.object(
+ {
+ ...baseSettings,
},
{ defaultValue: undefined }
);
+
+const configSchema7x = schema.object(
+ {
+ ...baseSettings,
+ ...deprecatedSettings,
+ },
+ { defaultValue: undefined }
+);
+
+export type ConfigType = TypeOf;
+export type ConfigType7x = TypeOf;
+
+export const config: PluginConfigDescriptor = {
+ schema: kibanaVersion.major < 8 ? configSchema7x : configSchema,
+ deprecations: ({ deprecate, unused }) => [
+ deprecate('enabled', '8.0.0'),
+ deprecate('proxyFilter', '8.0.0'),
+ deprecate('proxyConfig', '8.0.0'),
+ unused('ssl'),
+ ],
+};
diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts
index cd05652c62838..6ae518f5dc796 100644
--- a/src/plugins/console/server/index.ts
+++ b/src/plugins/console/server/index.ts
@@ -6,16 +6,11 @@
* Side Public License, v 1.
*/
-import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
+import { PluginInitializerContext } from 'kibana/server';
-import { ConfigType, config as configSchema } from './config';
import { ConsoleServerPlugin } from './plugin';
export { ConsoleSetup, ConsoleStart } from './types';
+export { config } from './config';
export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx);
-
-export const config: PluginConfigDescriptor = {
- deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')],
- schema: configSchema,
-};
diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts
index a5f1ca6107600..613337b286fbf 100644
--- a/src/plugins/console/server/plugin.ts
+++ b/src/plugins/console/server/plugin.ts
@@ -7,10 +7,11 @@
*/
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
+import { SemVer } from 'semver';
import { ProxyConfigCollection } from './lib';
import { SpecDefinitionsService, EsLegacyConfigService } from './services';
-import { ConfigType } from './config';
+import { ConfigType, ConfigType7x } from './config';
import { registerRoutes } from './routes';
@@ -23,7 +24,7 @@ export class ConsoleServerPlugin implements Plugin {
esLegacyConfigService = new EsLegacyConfigService();
- constructor(private readonly ctx: PluginInitializerContext) {
+ constructor(private readonly ctx: PluginInitializerContext) {
this.log = this.ctx.logger.get();
}
@@ -34,10 +35,17 @@ export class ConsoleServerPlugin implements Plugin {
save: true,
},
}));
-
+ const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version);
const config = this.ctx.config.get();
const globalConfig = this.ctx.config.legacy.get();
- const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str));
+
+ let pathFilters: RegExp[] | undefined;
+ let proxyConfigCollection: ProxyConfigCollection | undefined;
+ if (kibanaVersion.major < 8) {
+ // "pathFilters" and "proxyConfig" are only used in 7.x
+ pathFilters = (config as ConfigType7x).proxyFilter.map((str: string) => new RegExp(str));
+ proxyConfigCollection = new ProxyConfigCollection((config as ConfigType7x).proxyConfig);
+ }
this.esLegacyConfigService.setup(elasticsearch.legacy.config$);
@@ -51,7 +59,6 @@ export class ConsoleServerPlugin implements Plugin {
specDefinitionService: this.specDefinitionsService,
},
proxy: {
- proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig),
readLegacyESConfig: async (): Promise => {
const legacyConfig = await this.esLegacyConfigService.readConfig();
return {
@@ -59,8 +66,11 @@ export class ConsoleServerPlugin implements Plugin {
...legacyConfig,
};
},
- pathFilters: proxyPathFilters,
+ // Deprecated settings (only used in 7.x):
+ proxyConfigCollection,
+ pathFilters,
},
+ kibanaVersion,
});
return {
diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index 8ca5720d559ce..9ece066246e4a 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -9,6 +9,7 @@
import { Agent, IncomingMessage } from 'http';
import * as url from 'url';
import { pick, trimStart, trimEnd } from 'lodash';
+import { SemVer } from 'semver';
import { KibanaRequest, RequestHandler } from 'kibana/server';
@@ -58,17 +59,22 @@ function filterHeaders(originalHeaders: object, headersToKeep: string[]): object
function getRequestConfig(
headers: object,
esConfig: ESConfigForProxy,
- proxyConfigCollection: ProxyConfigCollection,
- uri: string
+ uri: string,
+ kibanaVersion: SemVer,
+ proxyConfigCollection?: ProxyConfigCollection
): { agent: Agent; timeout: number; headers: object; rejectUnauthorized?: boolean } {
const filteredHeaders = filterHeaders(headers, esConfig.requestHeadersWhitelist);
const newHeaders = setHeaders(filteredHeaders, esConfig.customHeaders);
- if (proxyConfigCollection.hasConfig()) {
- return {
- ...proxyConfigCollection.configForUri(uri),
- headers: newHeaders,
- };
+ if (kibanaVersion.major < 8) {
+ // In 7.x we still support the proxyConfig setting defined in kibana.yml
+ // From 8.x we don't support it anymore so we don't try to read it here.
+ if (proxyConfigCollection!.hasConfig()) {
+ return {
+ ...proxyConfigCollection!.configForUri(uri),
+ headers: newHeaders,
+ };
+ }
}
return {
@@ -106,18 +112,23 @@ export const createHandler =
({
log,
proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection },
+ kibanaVersion,
}: RouteDependencies): RequestHandler =>
async (ctx, request, response) => {
const { body, query } = request;
const { path, method } = query;
- if (!pathFilters.some((re) => re.test(path))) {
- return response.forbidden({
- body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
- headers: {
- 'Content-Type': 'text/plain',
- },
- });
+ if (kibanaVersion.major < 8) {
+ // The "console.proxyFilter" setting in kibana.yaml has been deprecated in 8.x
+ // We only read it on the 7.x branch
+ if (!pathFilters!.some((re) => re.test(path))) {
+ return response.forbidden({
+ body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ });
+ }
}
const legacyConfig = await readLegacyESConfig();
@@ -134,8 +145,9 @@ export const createHandler =
const { timeout, agent, headers, rejectUnauthorized } = getRequestConfig(
request.headers,
legacyConfig,
- proxyConfigCollection,
- uri.toString()
+ uri.toString(),
+ kibanaVersion,
+ proxyConfigCollection
);
const requestHeaders = {
diff --git a/src/plugins/console/server/routes/api/console/proxy/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/mocks.ts
index 010e35ab505af..d06ca90adf556 100644
--- a/src/plugins/console/server/routes/api/console/proxy/mocks.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/mocks.ts
@@ -5,28 +5,41 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
jest.mock('../../../../lib/proxy_request', () => ({
proxyRequest: jest.fn(),
}));
import { duration } from 'moment';
+import { MAJOR_VERSION } from '../../../../../common/constants';
import { ProxyConfigCollection } from '../../../../lib';
import { RouteDependencies, ProxyDependencies } from '../../../../routes';
import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services';
import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks';
-const defaultProxyValue = Object.freeze({
- readLegacyESConfig: async () => ({
- requestTimeout: duration(30000),
- customHeaders: {},
- requestHeadersWhitelist: [],
- hosts: ['http://localhost:9200'],
- }),
- pathFilters: [/.*/],
- proxyConfigCollection: new ProxyConfigCollection([]),
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
+const readLegacyESConfig = async () => ({
+ requestTimeout: duration(30000),
+ customHeaders: {},
+ requestHeadersWhitelist: [],
+ hosts: ['http://localhost:9200'],
+});
+
+let defaultProxyValue = Object.freeze({
+ readLegacyESConfig,
});
+if (kibanaVersion.major < 8) {
+ // In 7.x we still support the "pathFilter" and "proxyConfig" kibana.yml settings
+ defaultProxyValue = Object.freeze({
+ readLegacyESConfig,
+ pathFilters: [/.*/],
+ proxyConfigCollection: new ProxyConfigCollection([]),
+ });
+}
+
interface MockDepsArgument extends Partial> {
proxy?: Partial;
}
@@ -51,5 +64,6 @@ export const getProxyRouteHandlerDeps = ({
}
: defaultProxyValue,
log,
+ kibanaVersion,
};
};
diff --git a/src/plugins/console/server/routes/api/console/proxy/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/params.test.ts
index e08d2f8adecbf..edefb2f11f1f1 100644
--- a/src/plugins/console/server/routes/api/console/proxy/params.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/params.test.ts
@@ -5,14 +5,17 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
import { kibanaResponseFactory } from '../../../../../../../core/server';
-import { getProxyRouteHandlerDeps } from './mocks';
-import { createResponseStub } from './stubs';
+import { getProxyRouteHandlerDeps } from './mocks'; // import need to come first
+import { createResponseStub } from './stubs'; // import needs to come first
+import { MAJOR_VERSION } from '../../../../../common/constants';
import * as requestModule from '../../../../lib/proxy_request';
-
import { createHandler } from './create_handler';
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
describe('Console Proxy Route', () => {
let handler: ReturnType;
@@ -21,58 +24,71 @@ describe('Console Proxy Route', () => {
});
describe('params', () => {
- describe('pathFilters', () => {
- describe('no matches', () => {
- it('rejects with 403', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
- );
+ if (kibanaVersion.major < 8) {
+ describe('pathFilters', () => {
+ describe('no matches', () => {
+ it('rejects with 403', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({
+ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
+ })
+ );
- const { status } = await handler(
- {} as any,
- { query: { method: 'POST', path: '/baz/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { query: { method: 'POST', path: '/baz/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(403);
+ expect(status).toBe(403);
+ });
});
- });
- describe('one match', () => {
- it('allows the request', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
- );
- (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+ describe('one match', () => {
+ it('allows the request', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({
+ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
+ })
+ );
+
+ (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
- const { status } = await handler(
- {} as any,
- { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(200);
- expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ expect(status).toBe(200);
+ expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ });
});
- });
- describe('all match', () => {
- it('allows the request', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
- );
- (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+ describe('all match', () => {
+ it('allows the request', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
+ );
+
+ (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
- const { status } = await handler(
- {} as any,
- { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(200);
- expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ expect(status).toBe(200);
+ expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ });
});
});
- });
+ } else {
+ // jest requires to have at least one test in the file
+ test('dummy required test', () => {
+ expect(true).toBe(true);
+ });
+ }
});
});
diff --git a/src/plugins/console/server/routes/index.ts b/src/plugins/console/server/routes/index.ts
index 2c46547f92f1b..3911e8cfabc60 100644
--- a/src/plugins/console/server/routes/index.ts
+++ b/src/plugins/console/server/routes/index.ts
@@ -7,6 +7,7 @@
*/
import { IRouter, Logger } from 'kibana/server';
+import { SemVer } from 'semver';
import { EsLegacyConfigService, SpecDefinitionsService } from '../services';
import { ESConfigForProxy } from '../types';
@@ -18,8 +19,8 @@ import { registerSpecDefinitionsRoute } from './api/console/spec_definitions';
export interface ProxyDependencies {
readLegacyESConfig: () => Promise;
- pathFilters: RegExp[];
- proxyConfigCollection: ProxyConfigCollection;
+ pathFilters?: RegExp[]; // Only present in 7.x
+ proxyConfigCollection?: ProxyConfigCollection; // Only present in 7.x
}
export interface RouteDependencies {
@@ -30,6 +31,7 @@ export interface RouteDependencies {
esLegacyConfigService: EsLegacyConfigService;
specDefinitionService: SpecDefinitionsService;
};
+ kibanaVersion: SemVer;
}
export const registerRoutes = (dependencies: RouteDependencies) => {
From 3d0da7f0f69aa37db96db7489566c8bd2198be58 Mon Sep 17 00:00:00 2001
From: Sandra G
Date: Mon, 4 Oct 2021 11:37:19 -0400
Subject: [PATCH 44/98] [Stack Monitoring] Migrate Index Views to React
(#113660)
* index views
* fix type
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/public/application/index.tsx | 17 +++
.../elasticsearch/index_advanced_page.tsx | 77 +++++++++++++
.../pages/elasticsearch/index_page.tsx | 106 ++++++++++++++++++
.../pages/elasticsearch/item_template.tsx | 36 ++++++
.../pages/elasticsearch/node_page.tsx | 56 +++++----
.../elasticsearch/index/index_react.js | 70 ++++++++++++
.../components/cluster_view_react.js | 5 +-
7 files changed, 335 insertions(+), 32 deletions(-)
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index acdf3b0986a64..a958e6061215d 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -27,6 +27,8 @@ 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';
@@ -84,6 +86,21 @@ const MonitoringApp: React.FC<{
/>
{/* ElasticSearch Views */}
+
+
+
+
+
= ({ 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/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/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}
/>
From 59b15df115f5f0ed8f6d6a54c862594534dcd13a Mon Sep 17 00:00:00 2001
From: Sergi Massaneda
Date: Mon, 4 Oct 2021 17:49:14 +0200
Subject: [PATCH 45/98] fix priority reset bug (#113626)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../jira/jira_params.test.tsx | 18 ++++++++++++++++++
.../builtin_action_types/jira/jira_params.tsx | 4 ++--
.../jira/use_get_fields_by_issue_type.tsx | 2 +-
3 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
index a05db00f141ab..812c234e80d9e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
@@ -95,6 +95,10 @@ describe('JiraParamsFields renders', () => {
description: { allowedValues: [], defaultValue: {} },
},
};
+ const useGetFieldsByIssueTypeResponseLoading = {
+ isLoading: true,
+ fields: {},
+ };
beforeEach(() => {
jest.clearAllMocks();
@@ -421,5 +425,19 @@ describe('JiraParamsFields renders', () => {
expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium');
expect(editAction.mock.calls[1][1].incident.priority).toEqual(null);
});
+
+ test('Preserve priority when the issue type fields are loading and hasPriority becomes stale', () => {
+ useGetFieldsByIssueTypeMock
+ .mockReturnValueOnce(useGetFieldsByIssueTypeResponseLoading)
+ .mockReturnValue(useGetFieldsByIssueTypeResponse);
+ const wrapper = mount( );
+
+ expect(editAction).not.toBeCalled();
+
+ wrapper.setProps({ ...defaultProps }); // just to force component call useGetFieldsByIssueType again
+
+ expect(editAction).toBeCalledTimes(1);
+ expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium');
+ });
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
index 834892f2bf374..32390c163cf2a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
@@ -147,11 +147,11 @@ const JiraParamsFields: React.FunctionComponent {
- if (!hasPriority && incident.priority != null) {
+ if (!isLoadingFields && !hasPriority && incident.priority != null) {
editSubActionProperty('priority', null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasPriority]);
+ }, [hasPriority, isLoadingFields]);
const labelOptions = useMemo(
() => (incident.labels ? incident.labels.map((label: string) => ({ label })) : []),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
index 38be618119c4a..61db73c129db6 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
@@ -62,8 +62,8 @@ export const useGetFieldsByIssueType = ({
});
if (!didCancel) {
- setIsLoading(false);
setFields(res.data ?? {});
+ setIsLoading(false);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.FIELDS_API_ERROR,
From a9c100768e7c5407739da69d64e5b8250fe52f87 Mon Sep 17 00:00:00 2001
From: Justin Kambic
Date: Mon, 4 Oct 2021 11:56:00 -0400
Subject: [PATCH 46/98] Increase timeout for long-running unit test assertions.
(#113122)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../uptime/public/hooks/use_composite_image.test.tsx | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx
index 79e0cde1eaab8..9e2cb1e498b73 100644
--- a/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx
+++ b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx
@@ -191,10 +191,13 @@ describe('use composite image', () => {
expect(composeSpy.mock.calls[0][1]).toBe(canvasMock);
expect(composeSpy.mock.calls[0][2]).toBe(blocks);
- await waitFor(() => {
- expect(onComposeImageSuccess).toHaveBeenCalledTimes(1);
- expect(onComposeImageSuccess).toHaveBeenCalledWith('compose success');
- });
+ await waitFor(
+ () => {
+ expect(onComposeImageSuccess).toHaveBeenCalledTimes(1);
+ expect(onComposeImageSuccess).toHaveBeenCalledWith('compose success');
+ },
+ { timeout: 10000 }
+ );
});
});
});
From 9df505181739b27265d6e44de0c404449a187cc1 Mon Sep 17 00:00:00 2001
From: spalger
Date: Mon, 4 Oct 2021 15:52:42 +0000
Subject: [PATCH 47/98] Revert "Revert "chore: add modifications to staging
automatically after eslint fix (#113443)""
This reverts commit cc73577f84fc3d1c93d30832423f5cfd8b410609.
---
src/dev/run_precommit_hook.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js
index e1eafaf28d95d..0a594a232cc22 100644
--- a/src/dev/run_precommit_hook.js
+++ b/src/dev/run_precommit_hook.js
@@ -6,6 +6,9 @@
* Side Public License, v 1.
*/
+import SimpleGit from 'simple-git/promise';
+
+import { REPO_ROOT } from '@kbn/utils';
import { run, combineErrors, createFlagError, createFailError } from '@kbn/dev-utils';
import * as Eslint from './eslint';
import * as Stylelint from './stylelint';
@@ -48,6 +51,11 @@ run(
await Linter.lintFiles(log, filesToLint, {
fix: flags.fix,
});
+
+ if (flags.fix) {
+ const simpleGit = new SimpleGit(REPO_ROOT);
+ await simpleGit.add(filesToLint);
+ }
} catch (error) {
errors.push(error);
}
From 2b401d06df0e523804a1bafa33a5acfdc41043ec Mon Sep 17 00:00:00 2001
From: spalger
Date: Mon, 4 Oct 2021 15:59:25 +0000
Subject: [PATCH 48/98] Revert "Lint git index content on commit (#113300)"
This reverts commit 92fe7f8ab36be27a42bb96073996e6101331ae58.
---
src/dev/eslint/lint_files.ts | 42 +---------
src/dev/file.ts | 18 -----
.../precommit_hook/get_files_for_commit.js | 81 ++++---------------
src/dev/run_precommit_hook.js | 14 +---
src/dev/stylelint/lint_files.js | 56 +++----------
5 files changed, 31 insertions(+), 180 deletions(-)
diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts
index 77fe941fb7aeb..5c6118edeb2ec 100644
--- a/src/dev/eslint/lint_files.ts
+++ b/src/dev/eslint/lint_files.ts
@@ -12,41 +12,6 @@ import { REPO_ROOT } from '@kbn/utils';
import { createFailError, ToolingLog } from '@kbn/dev-utils';
import { File } from '../file';
-// For files living on the filesystem
-function lintFilesOnFS(cli: CLIEngine, files: File[]) {
- const paths = files.map((file) => file.getRelativePath());
- return cli.executeOnFiles(paths);
-}
-
-// For files living somewhere else (ie. git object)
-async function lintFilesOnContent(cli: CLIEngine, files: File[]) {
- const report: CLIEngine.LintReport = {
- results: [],
- errorCount: 0,
- warningCount: 0,
- fixableErrorCount: 0,
- fixableWarningCount: 0,
- usedDeprecatedRules: [],
- };
-
- for (let i = 0; i < files.length; i++) {
- const r = cli.executeOnText(await files[i].getContent(), files[i].getRelativePath());
- // Despite a relative path was given, the result would contain an absolute one. Work around it.
- r.results[0].filePath = r.results[0].filePath.replace(
- files[i].getAbsolutePath(),
- files[i].getRelativePath()
- );
- report.results.push(...r.results);
- report.errorCount += r.errorCount;
- report.warningCount += r.warningCount;
- report.fixableErrorCount += r.fixableErrorCount;
- report.fixableWarningCount += r.fixableWarningCount;
- report.usedDeprecatedRules.push(...r.usedDeprecatedRules);
- }
-
- return report;
-}
-
/**
* Lints a list of files with eslint. eslint reports are written to the log
* and a FailError is thrown when linting errors occur.
@@ -55,16 +20,15 @@ async function lintFilesOnContent(cli: CLIEngine, files: File[]) {
* @param {Array} files
* @return {undefined}
*/
-export async function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) {
+export function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) {
const cli = new CLIEngine({
cache: true,
cwd: REPO_ROOT,
fix,
});
- const virtualFilesCount = files.filter((file) => file.isVirtual()).length;
- const report =
- virtualFilesCount && !fix ? await lintFilesOnContent(cli, files) : lintFilesOnFS(cli, files);
+ const paths = files.map((file) => file.getRelativePath());
+ const report = cli.executeOnFiles(paths);
if (fix) {
CLIEngine.outputFixes(report);
diff --git a/src/dev/file.ts b/src/dev/file.ts
index 01005b257a403..b532a7bb70602 100644
--- a/src/dev/file.ts
+++ b/src/dev/file.ts
@@ -7,13 +7,11 @@
*/
import { dirname, extname, join, relative, resolve, sep, basename } from 'path';
-import { createFailError } from '@kbn/dev-utils';
export class File {
private path: string;
private relativePath: string;
private ext: string;
- private fileReader: undefined | (() => Promise);
constructor(path: string) {
this.path = resolve(path);
@@ -57,11 +55,6 @@ export class File {
);
}
- // Virtual files cannot be read as usual, an helper is needed
- public isVirtual() {
- return this.fileReader !== undefined;
- }
-
public getRelativeParentDirs() {
const parents: string[] = [];
@@ -88,15 +81,4 @@ export class File {
public toJSON() {
return this.relativePath;
}
-
- public setFileReader(fileReader: () => Promise) {
- this.fileReader = fileReader;
- }
-
- public getContent() {
- if (this.fileReader) {
- return this.fileReader();
- }
- throw createFailError('getContent() was invoked on a non-virtual File');
- }
}
diff --git a/src/dev/precommit_hook/get_files_for_commit.js b/src/dev/precommit_hook/get_files_for_commit.js
index 52dfab49c5c64..44c8c9d5e6bc0 100644
--- a/src/dev/precommit_hook/get_files_for_commit.js
+++ b/src/dev/precommit_hook/get_files_for_commit.js
@@ -6,65 +6,12 @@
* Side Public License, v 1.
*/
-import { format } from 'util';
import SimpleGit from 'simple-git';
import { fromNode as fcb } from 'bluebird';
import { REPO_ROOT } from '@kbn/utils';
import { File } from '../file';
-/**
- * Return the `git diff` argument used for building the list of files
- *
- * @param {String} gitRef
- * @return {String}
- *
- * gitRef return
- * '' '--cached'
- * '[' ']