From 5a9fc214150e74c4d7a599c416d35e59dc8c247f Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Mon, 12 Apr 2021 11:22:07 +0200
Subject: [PATCH 01/79] [Discover] Unskip histogram hiding test (#95759)
- improves the test to be no longer flaky
---
test/functional/apps/discover/_discover_histogram.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts
index 72deb74459ab9..e41422555f81d 100644
--- a/test/functional/apps/discover/_discover_histogram.ts
+++ b/test/functional/apps/discover/_discover_histogram.ts
@@ -21,9 +21,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
const testSubjects = getService('testSubjects');
const browser = getService('browser');
+ const retry = getService('retry');
- // FLAKY: https://github.com/elastic/kibana/issues/94532
- describe.skip('discover histogram', function describeIndexTests() {
+ describe('discover histogram', function describeIndexTests() {
before(async () => {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.load('long_window_logstash');
@@ -107,8 +107,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
await testSubjects.click('discoverChartToggle');
- canvasExists = await elasticChart.canvasExists();
- expect(canvasExists).to.be(true);
+ await retry.waitFor(`Discover histogram to be displayed`, async () => {
+ canvasExists = await elasticChart.canvasExists();
+ return canvasExists;
+ });
+
await PageObjects.discover.saveSearch('persisted hidden histogram');
await PageObjects.header.waitUntilLoadingHasFinished();
From 9946125ab496f2843d84d1adfbc0c274128e9f55 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 12 Apr 2021 12:30:11 +0200
Subject: [PATCH 02/79] [Lens] Hide "Show more errors" once expanded (#96605)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../editor_frame/workspace_panel/workspace_panel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 8a0b9922c736b..f9058b48dd1a8 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
@@ -570,7 +570,7 @@ export const InnerVisualizationWrapper = ({
{
setLocalState((prevState: WorkspaceState) => ({
From a05a66ccce5de2cd65ef28412080f93c56359cae Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 12 Apr 2021 12:49:47 +0100
Subject: [PATCH 03/79] skip flaky suite (#96691)
---
.../components/flyout/add_timeline_button/index.test.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx
index f8913148c625b..84406aed3619f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx
@@ -35,7 +35,8 @@ jest.mock('../../../../common/components/inspect', () => ({
InspectButtonContainer: jest.fn(({ children }) => {children}
),
}));
-describe('AddTimelineButton', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/96691
+describe.skip('AddTimelineButton', () => {
let wrapper: ReactWrapper;
const props = {
timelineId: TimelineId.active,
From d2012c0ce3f55acabdf1d0f9f59ab22657d33d27 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 12 Apr 2021 14:25:15 +0200
Subject: [PATCH 04/79] [Lens] Make table and metric show on top Chart switcher
(#96601)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../datatable_visualization/visualization.tsx | 1 +
.../workspace_panel/chart_switch.tsx | 29 ++++++++++++-------
.../metric_visualization/visualization.tsx | 1 +
x-pack/plugins/lens/public/types.ts | 5 ++++
4 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 4094ecee74e1c..f8b56f4ff2f81 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -60,6 +60,7 @@ export const datatableVisualization: Visualization
groupLabel: i18n.translate('xpack.lens.datatable.groupLabel', {
defaultMessage: 'Tabular and single value',
}),
+ sortPriority: 1,
},
],
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 ef8c0798bb91e..5538dd26d0323 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
@@ -219,12 +219,15 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
// reorganize visualizations in groups
const grouped: Record<
string,
- Array<
- VisualizationType & {
- visualizationId: string;
- selection: VisualizationSelection;
- }
- >
+ {
+ priority: number;
+ visualizations: Array<
+ VisualizationType & {
+ visualizationId: string;
+ selection: VisualizationSelection;
+ }
+ >;
+ }
> = {};
// Will need it later on to quickly pick up the metadata from it
const lookup: Record<
@@ -240,13 +243,17 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
visualizationType.label.toLowerCase().includes(lowercasedSearchTerm) ||
visualizationType.fullLabel?.toLowerCase().includes(lowercasedSearchTerm);
if (isSearchMatch) {
- grouped[visualizationType.groupLabel] = grouped[visualizationType.groupLabel] || [];
+ grouped[visualizationType.groupLabel] = grouped[visualizationType.groupLabel] || {
+ priority: 0,
+ visualizations: [],
+ };
const visualizationEntry = {
...visualizationType,
visualizationId,
selection: getSelection(visualizationId, visualizationType.id),
};
- grouped[visualizationType.groupLabel].push(visualizationEntry);
+ grouped[visualizationType.groupLabel].priority += visualizationType.sortPriority || 0;
+ grouped[visualizationType.groupLabel].visualizations.push(visualizationEntry);
lookup[`${visualizationId}:${visualizationType.id}`] = visualizationEntry;
}
}
@@ -254,9 +261,11 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
return {
visualizationTypes: Object.keys(grouped)
- .sort()
+ .sort((groupA, groupB) => {
+ return grouped[groupB].priority - grouped[groupA].priority;
+ })
.flatMap((group): SelectableEntry[] => {
- const visualizations = grouped[group];
+ const { visualizations } = grouped[group];
if (visualizations.length === 0) {
return [];
}
diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
index 34b9e4d2b2526..e0977be7535af 100644
--- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
@@ -55,6 +55,7 @@ export const metricVisualization: Visualization = {
groupLabel: i18n.translate('xpack.lens.metric.groupLabel', {
defaultMessage: 'Tabular and single value',
}),
+ sortPriority: 1,
},
],
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 3d34d22c5048a..94b4433a82551 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -550,6 +550,11 @@ export interface VisualizationType {
* The group the visualization belongs to
*/
groupLabel: string;
+ /**
+ * The priority of the visualization in the list (global priority)
+ * Higher number means higher priority. When omitted defaults to 0
+ */
+ sortPriority?: number;
}
export interface Visualization {
From 1de77ccb4e9c8be1e539da2d26edfa71747bcce3 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Mon, 12 Apr 2021 08:27:54 -0400
Subject: [PATCH 05/79] [Fleet] Create enrollment API keys as current user
(#96464)
---
.../routes/enrollment_api_key/handler.ts | 3 +-
.../server/services/agent_policy_update.ts | 2 +-
.../services/api_keys/enrollment_api_key.ts | 54 +++++++++++--------
.../apis/enrollment_api_keys/crud.ts | 49 ++++++++---------
4 files changed, 57 insertions(+), 51 deletions(-)
diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts
index c85dc06c38286..0959a9a88704a 100644
--- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts
+++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts
@@ -67,10 +67,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler<
export const deleteEnrollmentApiKeyHandler: RequestHandler<
TypeOf
> = async (context, request, response) => {
- const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
try {
- await APIKeyService.deleteEnrollmentApiKey(soClient, esClient, request.params.keyId);
+ await APIKeyService.deleteEnrollmentApiKey(esClient, request.params.keyId);
const body: DeleteEnrollmentAPIKeyResponse = { action: 'deleted' };
diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts
index dc566b2c435a6..3f5f717c94597 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts
@@ -56,6 +56,6 @@ export async function agentPolicyUpdateEventHandler(
if (action === 'deleted') {
await unenrollForAgentPolicyId(soClient, esClient, agentPolicyId);
- await deleteEnrollmentApiKeyForAgentPolicyId(soClient, esClient, agentPolicyId);
+ await deleteEnrollmentApiKeyForAgentPolicyId(esClient, agentPolicyId);
}
}
diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
index 7059cc96159b9..b8a24a006a674 100644
--- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
+++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
@@ -17,7 +17,7 @@ import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
import { agentPolicyService } from '../agent_policy';
import { escapeSearchQueryPhrase } from '../saved_object';
-import { createAPIKey, invalidateAPIKeys } from './security';
+import { invalidateAPIKeys } from './security';
const uuidRegex = /^\([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\)$/;
@@ -77,14 +77,9 @@ export async function getEnrollmentAPIKey(
/**
* Invalidate an api key and mark it as inactive
- * @param soClient
* @param id
*/
-export async function deleteEnrollmentApiKey(
- soClient: SavedObjectsClientContract,
- esClient: ElasticsearchClient,
- id: string
-) {
+export async function deleteEnrollmentApiKey(esClient: ElasticsearchClient, id: string) {
const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id);
await invalidateAPIKeys([enrollmentApiKey.api_key_id]);
@@ -102,7 +97,6 @@ export async function deleteEnrollmentApiKey(
}
export async function deleteEnrollmentApiKeyForAgentPolicyId(
- soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentPolicyId: string
) {
@@ -120,7 +114,7 @@ export async function deleteEnrollmentApiKeyForAgentPolicyId(
}
for (const apiKey of items) {
- await deleteEnrollmentApiKey(soClient, esClient, apiKey.id);
+ await deleteEnrollmentApiKey(esClient, apiKey.id);
}
}
}
@@ -182,19 +176,37 @@ export async function generateEnrollmentAPIKey(
}
const name = providedKeyName ? `${providedKeyName} (${id})` : id;
- const key = await createAPIKey(soClient, name, {
- // Useless role to avoid to have the privilege of the user that created the key
- 'fleet-apikey-enroll': {
- cluster: [],
- applications: [
- {
- application: '.fleet',
- privileges: ['no-privileges'],
- resources: ['*'],
+
+ const { body: key } = await esClient.security
+ .createApiKey({
+ body: {
+ name,
+ // @ts-expect-error Metadata in api keys
+ metadata: {
+ managed_by: 'fleet',
+ managed: true,
+ type: 'enroll',
+ policy_id: data.agentPolicyId,
},
- ],
- },
- });
+ role_descriptors: {
+ // Useless role to avoid to have the privilege of the user that created the key
+ 'fleet-apikey-enroll': {
+ cluster: [],
+ index: [],
+ applications: [
+ {
+ application: '.fleet',
+ privileges: ['no-privileges'],
+ resources: ['*'],
+ },
+ ],
+ },
+ },
+ },
+ })
+ .catch((err) => {
+ throw new Error(`Impossible to create an api key: ${err.message}`);
+ });
if (!key) {
throw new Error(
diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts
index 2569d9aef4b5b..d9946bb174f5d 100644
--- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts
+++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts
@@ -115,6 +115,28 @@ export default function (providerContext: FtrProviderContext) {
expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id');
});
+ it('should create an ES ApiKey with metadata', async () => {
+ const { body: apiResponse } = await supertest
+ .post(`/api/fleet/enrollment-api-keys`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ policy_id: 'policy1',
+ })
+ .expect(200);
+
+ const { body: apiKeyRes } = await es.security.getApiKey({
+ id: apiResponse.item.api_key_id,
+ });
+
+ // @ts-expect-error Metadata not yet in the client type
+ expect(apiKeyRes.api_keys[0].metadata).eql({
+ policy_id: 'policy1',
+ managed_by: 'fleet',
+ managed: true,
+ type: 'enroll',
+ });
+ });
+
it('should create an ES ApiKey with limited privileges', async () => {
const { body: apiResponse } = await supertest
.post(`/api/fleet/enrollment-api-keys`)
@@ -162,33 +184,6 @@ export default function (providerContext: FtrProviderContext) {
},
});
});
-
- describe('It should handle error when the Fleet user is invalid', () => {
- before(async () => {});
- after(async () => {
- await getService('supertest')
- .post(`/api/fleet/agents/setup`)
- .set('kbn-xsrf', 'xxx')
- .send({ forceRecreate: true });
- });
-
- it('should not allow to create an enrollment api key if the Fleet admin user is invalid', async () => {
- await es.security.changePassword({
- username: 'fleet_enroll',
- body: {
- password: Buffer.from((Math.random() * 10000000).toString()).toString('base64'),
- },
- });
- const res = await supertest
- .post(`/api/fleet/enrollment-api-keys`)
- .set('kbn-xsrf', 'xxx')
- .send({
- policy_id: 'policy1',
- })
- .expect(400);
- expect(res.body.message).match(/Fleet Admin user is invalid/);
- });
- });
});
});
}
From 886d7e0140bfeb539aaa040056e31e2f218c4f06 Mon Sep 17 00:00:00 2001
From: Uladzislau Lasitsa
Date: Mon, 12 Apr 2021 16:16:47 +0300
Subject: [PATCH 06/79] Stacked line charts incorrectly shows one term as 100%
(#96203)
* set "stacked" mode metric if the referenced axis is "percentage"
* Fixed CI
* Move logic inside chart_option component
* Fixed CI
* Update utils.ts
* Update index.tsx
* Update index.tsx
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../__snapshots__/chart_options.test.tsx.snap | 1 +
.../metrics_axes/chart_options.test.tsx | 14 +++++++++++--
.../options/metrics_axes/chart_options.tsx | 20 +++++++++++++++++--
3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap
index 56f35ae021173..59a7cf966df91 100644
--- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap
+++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap
@@ -54,6 +54,7 @@ exports[`ChartOptions component should init with the default set of props 1`] =
{
expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Normal);
});
+
+ it('should set "stacked" mode and disabled control if the referenced axis is "percentage"', () => {
+ defaultProps.valueAxes[0].scale.mode = AxisMode.Percentage;
+ defaultProps.chart.mode = ChartMode.Normal;
+ const paramName = 'mode';
+ const comp = mount();
+
+ expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Stacked);
+ expect(comp.find({ paramName }).prop('disabled')).toBeTruthy();
+ });
});
diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx
index 6f0b4fc5c9d22..23452a87aae60 100644
--- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx
+++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx
@@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { SelectOption } from '../../../../../../vis_default_editor/public';
-import { SeriesParam, ValueAxis } from '../../../../types';
+import { SeriesParam, ValueAxis, ChartMode, AxisMode } from '../../../../types';
import { LineOptions } from './line_options';
import { SetParamByIndex, ChangeValueAxis } from '.';
import { ChartType } from '../../../../../common';
@@ -38,6 +38,7 @@ function ChartOptions({
changeValueAxis,
setParamByIndex,
}: ChartOptionsParams) {
+ const [disabledMode, setDisabledMode] = useState(false);
const setChart: SetChart = useCallback(
(paramName, value) => {
setParamByIndex('seriesParams', index, paramName, value);
@@ -68,6 +69,20 @@ function ChartOptions({
[valueAxes]
);
+ useEffect(() => {
+ const valueAxisToMetric = valueAxes.find((valueAxis) => valueAxis.id === chart.valueAxis);
+ if (valueAxisToMetric) {
+ if (valueAxisToMetric.scale.mode === AxisMode.Percentage) {
+ setDisabledMode(true);
+ if (chart.mode !== ChartMode.Stacked) {
+ setChart('mode', ChartMode.Stacked);
+ }
+ } else if (disabledMode) {
+ setDisabledMode(false);
+ }
+ }
+ }, [valueAxes, chart, disabledMode, setChart, setDisabledMode]);
+
return (
<>
From c40121151fdf9ed17582e53902d214e6bed49ba6 Mon Sep 17 00:00:00 2001
From: John Schulz
Date: Mon, 12 Apr 2021 09:43:06 -0400
Subject: [PATCH 07/79] [Fleet] UI changes on hosted policy detail view
(#96337)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Fixes several items from https://github.com/elastic/observability-design/issues/32
- Agent policy detail page
- [x] Integrations tab: 1a) Show a lock icon with hover tooltip next to host policy name
- [x] Integrations tab: 7a) hide the "Add integration" button
- [x] Integrations tab: 7b) hide the "delete integration" action which appears in the [...] actions menu
- [x] Settings tab: 5a) Do not show the “Delete policy” section for Hosted agent policies
- [x] Settings tab: 5b) Disable the "name" and "description" inputs
- Agents detail page
- [x] 2b) remove the "actions" button in the page header (top right)
## Screenshots
Agent policy detail page - Integrations tab
- 1a) Show a lock icon with hover tooltip next to host policy name
- 7a) hide the "Add integration" button
- 7b) hide the "delete integration" action which appears in the [...] actions menu
Non-hosted policy
Hosted policy
Agent policy detail page - Settings tab
- 5a) Do not show the “Delete policy” section for Hosted agent policies
- 5b) Disable the "name" and "description" inputs
non-hosted policy: items available
Hosted policy: items hidden / disabled
Agents detail page: 2b) remove the "actions" button in the page header (top right)
shown on non-hosted policy
hidden on hosted policy
### Checklist
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/agent_policy_form.tsx | 3 +-
.../package_policies_table.tsx | 112 +++++++++---------
.../agent_policy/details_page/index.tsx | 51 +++++---
.../agents/agent_details_page/index.tsx | 23 ++--
.../sections/agents/agent_list_page/index.tsx | 5 +-
5 files changed, 112 insertions(+), 82 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index 238cba217da8e..a1ac30995f722 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -144,6 +144,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
isInvalid={Boolean(touchedFields[name] && validation[name])}
>
updateAgentPolicy({ [name]: e.target.value })}
@@ -283,7 +284,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
}}
/>
- {isEditing && 'id' in agentPolicy ? (
+ {isEditing && 'id' in agentPolicy && agentPolicy.is_managed !== true ? (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
index db88de0ba720b..9e23fc775a213 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
@@ -167,42 +167,45 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
}),
actions: [
{
- render: (packagePolicy: InMemoryPackagePolicy) => (
- {}}
- // key="packagePolicyView"
- // >
- //
- // ,
-
-
- ,
- // FIXME: implement Copy package policy action
- // {}} key="packagePolicyCopy">
- //
- // ,
+ render: (packagePolicy: InMemoryPackagePolicy) => {
+ const menuItems = [
+ // FIXME: implement View package policy action
+ // {}}
+ // key="packagePolicyView"
+ // >
+ //
+ // ,
+
+
+ ,
+ // FIXME: implement Copy package policy action
+ // {}} key="packagePolicyCopy">
+ //
+ // ,
+ ];
+
+ if (!agentPolicy.is_managed) {
+ menuItems.push(
{(deletePackagePoliciesPrompt) => {
return (
@@ -220,10 +223,11 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
);
}}
- ,
- ]}
- />
- ),
+
+ );
+ }
+ return ;
+ },
},
],
},
@@ -244,19 +248,21 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
}}
{...rest}
search={{
- toolsRight: [
-
-
- ,
- ],
+ toolsRight: agentPolicy.is_managed
+ ? []
+ : [
+
+
+ ,
+ ],
box: {
incremental: true,
schema: true,
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx
index 350d6439c9d3d..3e6ca5944c380 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx
@@ -12,6 +12,8 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
+ EuiIconTip,
+ EuiTitle,
EuiText,
EuiSpacer,
EuiButtonEmpty,
@@ -84,23 +86,42 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => {
-
-
- {isLoading ? (
-
- ) : (
- (agentPolicy && agentPolicy.name) || (
-
+ ) : (
+
+
+
+
+ {(agentPolicy && agentPolicy.name) || (
+
+ )}
+
+
+
+ {agentPolicy?.is_managed && (
+
+
- )
+
)}
-
-
+
+ )}
{agentPolicy && agentPolicy.description ? (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx
index adeb56f489ea3..56b99f645f97c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx
@@ -194,17 +194,18 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
),
},
{
- content: (
-
- ),
+ content:
+ isAgentPolicyLoading || agentPolicyData?.item?.is_managed ? undefined : (
+
+ ),
},
].map((item, index) => (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx
index 8e9c549fe5609..d01d290e129b8 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx
@@ -341,9 +341,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;
+ if (!agent.policy_id) return true;
- const agentPolicy = agentPolicies.find((p) => p.id === agent.policy_id);
- const isManaged = agent.policy_id && agentPolicy?.is_managed === true;
+ const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
+ const isManaged = agentPolicy?.is_managed === true;
return !isManaged;
};
From a2c47ef5f5890856c63e3ddfa769f859467c45d5 Mon Sep 17 00:00:00 2001
From: Shahzad
Date: Mon, 12 Apr 2021 15:53:53 +0200
Subject: [PATCH 08/79] [Exploratory View]Additional metrics for kpi over time
(#96532)
---
x-pack/plugins/lens/public/index.ts | 1 +
.../public/indexpattern_datasource/types.ts | 1 +
.../apm/service_latency_config.ts | 7 +-
.../apm/service_throughput_config.ts | 9 +-
.../configurations/constants/constants.ts | 2 +
.../configurations/constants/url_constants.ts | 2 +-
.../configurations/lens_attributes.test.ts | 85 +++++++---
.../configurations/lens_attributes.ts | 136 ++++++++++++----
.../logs/logs_frequency_config.ts | 2 +-
.../metrics/cpu_usage_config.ts | 5 +-
.../metrics/memory_usage_config.ts | 5 +-
.../metrics/network_activity_config.ts | 5 +-
.../configurations/rum/kpi_trends_config.ts | 33 ++--
.../rum/performance_dist_config.ts | 10 +-
.../synthetics/field_formats.ts | 1 +
.../synthetics/monitor_duration_config.ts | 7 +-
.../synthetics/monitor_pings_config.ts | 2 +-
.../exploratory_view/configurations/utils.ts | 11 +-
.../exploratory_view/exploratory_view.tsx | 19 +--
.../hooks/use_default_index_pattern.tsx | 1 +
.../hooks/use_init_exploratory_view.ts | 14 +-
.../hooks/use_lens_attributes.ts | 13 +-
.../hooks/use_url_storage.tsx | 6 +-
.../columns/chart_types.test.tsx | 12 +-
.../series_builder/columns/chart_types.tsx | 104 ++++++++++++
.../columns/data_types_col.test.tsx | 4 +-
.../series_builder/columns/data_types_col.tsx | 12 +-
.../columns/operation_type_select.test.tsx | 64 ++++++++
.../columns/operation_type_select.tsx | 82 ++++++++++
.../columns/report_definition_col.tsx | 22 ++-
.../columns/report_types_col.test.tsx | 6 +-
.../columns/report_types_col.tsx | 7 +
.../series_builder/series_builder.tsx | 15 +-
.../series_date_picker/index.tsx | 3 +-
.../series_date_picker.test.tsx | 3 +-
.../series_editor/columns/actions_col.tsx | 12 +-
.../series_editor/columns/chart_types.tsx | 149 ------------------
.../columns/metric_selection.test.tsx | 112 -------------
.../columns/metric_selection.tsx | 86 ----------
.../shared/exploratory_view/types.ts | 15 +-
.../utils/observability_index_patterns.ts | 12 +-
41 files changed, 585 insertions(+), 512 deletions(-)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/chart_types.test.tsx (74%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index cedb648215c0e..fcfed9b9f1fc5 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -33,6 +33,7 @@ export type {
IndexPatternPersistedState,
PersistedIndexPatternLayer,
IndexPatternColumn,
+ FieldBasedIndexPatternColumn,
OperationType,
IncompleteColumn,
FiltersIndexPatternColumn,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
index 79155184a5f6d..18f653c588ee8 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
@@ -11,6 +11,7 @@ import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/pub
import { DragDropIdentifier } from '../drag_drop/providers';
export {
+ FieldBasedIndexPatternColumn,
IndexPatternColumn,
OperationType,
IncompleteColumn,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
index 3fcf98f712bef..7af3252584819 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts
@@ -8,7 +8,6 @@
import { ConfigProps, DataSeries } from '../../types';
import { FieldLabels } from '../constants';
import { buildPhraseFilter } from '../utils';
-import { OperationType } from '../../../../../../../lens/public';
export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
return {
@@ -20,11 +19,11 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'transaction.duration.us',
label: 'Latency',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: [
'user_agent.name',
'user_agent.os.name',
@@ -37,7 +36,7 @@ export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigPr
'client.geo.country_name',
'user_agent.device.name',
],
- filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)],
+ filters: buildPhraseFilter('transaction.type', 'request', indexPattern),
labels: { ...FieldLabels },
reportDefinitions: [
{
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
index c0f3d6dc9b010..7b1d472ac8bbf 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts
@@ -8,7 +8,6 @@
import { ConfigProps, DataSeries } from '../../types';
import { FieldLabels } from '../constants/constants';
import { buildPhraseFilter } from '../utils';
-import { OperationType } from '../../../../../../../lens/public';
export function getServiceThroughputLensConfig({
seriesId,
@@ -16,18 +15,18 @@ export function getServiceThroughputLensConfig({
}: ConfigProps): DataSeries {
return {
id: seriesId,
- reportType: 'service-latency',
+ reportType: 'service-throughput',
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'transaction.duration.us',
label: 'Throughput',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: [
'user_agent.name',
'user_agent.os.name',
@@ -40,7 +39,7 @@ export function getServiceThroughputLensConfig({
'client.geo.country_name',
'user_agent.device.name',
],
- filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)],
+ filters: buildPhraseFilter('transaction.type', 'request', indexPattern),
labels: { ...FieldLabels },
reportDefinitions: [
{
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 ed849c1eb47b3..14cd24c42e6a2 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
@@ -8,6 +8,8 @@
import { AppDataType, ReportViewTypeId } from '../../types';
import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
+export const DEFAULT_TIME = { from: 'now-1h', to: 'now' };
+
export const FieldLabels: Record = {
'user_agent.name': 'Browser family',
'user_agent.version': 'Browser version',
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 5b99c19dbabb7..67d72a656744c 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
@@ -6,7 +6,7 @@
*/
export enum URL_KEYS {
- METRIC_TYPE = 'mt',
+ OPERATION_TYPE = 'op',
REPORT_TYPE = 'rt',
SERIES_TYPE = 'st',
BREAK_DOWN = 'bd',
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 139f3ab0d82ed..0de78c45041d4 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
@@ -42,14 +42,18 @@ describe('Lens Attribute', () => {
it('should return expected field type', function () {
expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual(
JSON.stringify({
- count: 0,
- name: 'transaction.type',
- type: 'string',
- esTypes: ['keyword'],
- scripted: false,
- searchable: true,
- aggregatable: true,
- readFromDocValues: true,
+ fieldMeta: {
+ count: 0,
+ name: 'transaction.type',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ fieldName: 'transaction.type',
+ columnType: null,
})
);
});
@@ -57,14 +61,18 @@ describe('Lens Attribute', () => {
it('should return expected field type for custom field with default value', function () {
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
JSON.stringify({
- count: 0,
- name: 'transaction.duration.us',
- type: 'number',
- esTypes: ['long'],
- scripted: false,
- searchable: true,
- aggregatable: true,
- readFromDocValues: true,
+ fieldMeta: {
+ count: 0,
+ name: 'transaction.duration.us',
+ type: 'number',
+ esTypes: ['long'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ fieldName: 'transaction.duration.us',
+ columnType: null,
})
);
});
@@ -76,20 +84,45 @@ describe('Lens Attribute', () => {
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
JSON.stringify({
- count: 0,
- name: LCP_FIELD,
- type: 'number',
- esTypes: ['scaled_float'],
- scripted: false,
- searchable: true,
- aggregatable: true,
- readFromDocValues: true,
+ fieldMeta: {
+ count: 0,
+ name: LCP_FIELD,
+ type: 'number',
+ esTypes: ['scaled_float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ fieldName: LCP_FIELD,
})
);
});
- it('should return expected number column', function () {
- expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({
+ it('should return expected number range column', function () {
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
+ dataType: 'number',
+ isBucketed: true,
+ label: 'Page load time (Seconds)',
+ operationType: 'range',
+ params: {
+ maxBars: 'auto',
+ ranges: [
+ {
+ from: 0,
+ label: '',
+ to: 1000,
+ },
+ ],
+ type: 'histogram',
+ },
+ scale: 'interval',
+ sourceField: 'transaction.duration.us',
+ });
+ });
+
+ it('should return expected number operation column', function () {
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time (Seconds)',
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 589a93d160068..12a5b19fb02fc 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
@@ -5,10 +5,14 @@
* 2.0.
*/
+import { i18n } from '@kbn/i18n';
+import { capitalize } from 'lodash';
import {
CountIndexPatternColumn,
DateHistogramIndexPatternColumn,
- LastValueIndexPatternColumn,
+ AvgIndexPatternColumn,
+ MedianIndexPatternColumn,
+ PercentileIndexPatternColumn,
OperationType,
PersistedIndexPatternLayer,
RangeIndexPatternColumn,
@@ -17,6 +21,8 @@ import {
XYState,
XYCurveType,
DataType,
+ OperationMetadata,
+ FieldBasedIndexPatternColumn,
} from '../../../../../../lens/public';
import {
buildPhraseFilter,
@@ -30,6 +36,15 @@ function getLayerReferenceName(layerId: string) {
return `indexpattern-datasource-layer-${layerId}`;
}
+function buildNumberColumn(sourceField: string) {
+ return {
+ sourceField,
+ dataType: 'number' as DataType,
+ isBucketed: false,
+ scale: 'ratio' as OperationMetadata['scale'],
+ };
+}
+
export class LensAttributes {
indexPattern: IndexPattern;
layers: Record;
@@ -44,7 +59,7 @@ export class LensAttributes {
reportViewConfig: DataSeries,
seriesType?: SeriesType,
filters?: UrlFilter[],
- metricType?: OperationType,
+ operationType?: OperationType,
reportDefinitions?: Record
) {
this.indexPattern = indexPattern;
@@ -52,8 +67,8 @@ export class LensAttributes {
this.filters = filters ?? [];
this.reportDefinitions = reportDefinitions ?? {};
- if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && metricType) {
- reportViewConfig.yAxisColumn.operationType = metricType;
+ if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && operationType) {
+ reportViewConfig.yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
}
this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType;
this.reportViewConfig = reportViewConfig;
@@ -93,7 +108,7 @@ export class LensAttributes {
this.visualization.layers[0].splitAccessor = undefined;
}
- getNumberColumn(sourceField: string): RangeIndexPatternColumn {
+ getNumberRangeColumn(sourceField: string): RangeIndexPatternColumn {
return {
sourceField,
label: this.reportViewConfig.labels[sourceField],
@@ -109,6 +124,38 @@ export class LensAttributes {
};
}
+ getNumberOperationColumn(
+ sourceField: string,
+ operationType: 'average' | 'median'
+ ): AvgIndexPatternColumn | MedianIndexPatternColumn {
+ return {
+ ...buildNumberColumn(sourceField),
+ label: i18n.translate('xpack.observability.expView.columns.operation.label', {
+ defaultMessage: '{operationType} of {sourceField}',
+ values: {
+ sourceField: this.reportViewConfig.labels[sourceField],
+ operationType: capitalize(operationType),
+ },
+ }),
+ operationType,
+ };
+ }
+
+ getPercentileNumberColumn(
+ sourceField: string,
+ percentileValue: string
+ ): PercentileIndexPatternColumn {
+ return {
+ ...buildNumberColumn(sourceField),
+ label: i18n.translate('xpack.observability.expView.columns.label', {
+ defaultMessage: '{percentileValue} percentile of {sourceField}',
+ values: { sourceField, percentileValue },
+ }),
+ operationType: 'percentile',
+ params: { percentile: Number(percentileValue.split('th')[0]) },
+ };
+ }
+
getDateHistogramColumn(sourceField: string): DateHistogramIndexPatternColumn {
return {
sourceField,
@@ -121,56 +168,89 @@ export class LensAttributes {
};
}
- getXAxis():
- | LastValueIndexPatternColumn
- | DateHistogramIndexPatternColumn
- | RangeIndexPatternColumn {
+ getXAxis() {
const { xAxisColumn } = this.reportViewConfig;
- const { type: fieldType, name: fieldName } = this.getFieldMeta(xAxisColumn.sourceField)!;
+ return this.getColumnBasedOnType(xAxisColumn.sourceField!);
+ }
+
+ getColumnBasedOnType(sourceField: string, operationType?: OperationType) {
+ const { fieldMeta, columnType, fieldName } = this.getFieldMeta(sourceField);
+ const { type: fieldType } = fieldMeta ?? {};
+
+ if (fieldName === 'Records') {
+ return this.getRecordsColumn();
+ }
if (fieldType === 'date') {
return this.getDateHistogramColumn(fieldName);
}
if (fieldType === 'number') {
- return this.getNumberColumn(fieldName);
+ if (columnType === 'operation' || operationType) {
+ if (operationType === 'median' || operationType === 'average') {
+ return this.getNumberOperationColumn(fieldName, operationType);
+ }
+ if (operationType?.includes('th')) {
+ return this.getPercentileNumberColumn(sourceField, operationType);
+ }
+ }
+ return this.getNumberRangeColumn(fieldName);
}
// FIXME review my approach again
return this.getDateHistogramColumn(fieldName);
}
- getFieldMeta(sourceField?: string) {
- let xAxisField = sourceField;
+ getCustomFieldName(sourceField: string) {
+ let fieldName = sourceField;
+ let columnType = null;
- if (xAxisField) {
- const rdf = this.reportViewConfig.reportDefinitions ?? [];
+ const rdf = this.reportViewConfig.reportDefinitions ?? [];
- const customField = rdf.find(({ field }) => field === xAxisField);
+ const customField = rdf.find(({ field }) => field === fieldName);
- if (customField) {
- if (this.reportDefinitions[xAxisField]) {
- xAxisField = this.reportDefinitions[xAxisField];
- } else if (customField.defaultValue) {
- xAxisField = customField.defaultValue;
- } else if (customField.options?.[0].field) {
- xAxisField = customField.options?.[0].field;
- }
+ if (customField) {
+ if (this.reportDefinitions[fieldName]) {
+ fieldName = this.reportDefinitions[fieldName];
+ if (customField?.options)
+ columnType = customField?.options?.find(({ field }) => field === fieldName)?.columnType;
+ } else if (customField.defaultValue) {
+ fieldName = customField.defaultValue;
+ } else if (customField.options?.[0].field) {
+ fieldName = customField.options?.[0].field;
+ columnType = customField.options?.[0].columnType;
}
-
- return this.indexPattern.getFieldByName(xAxisField);
}
+
+ return { fieldName, columnType };
+ }
+
+ getFieldMeta(sourceField: string) {
+ const { fieldName, columnType } = this.getCustomFieldName(sourceField);
+
+ const fieldMeta = this.indexPattern.getFieldByName(fieldName);
+
+ return { fieldMeta, fieldName, columnType };
}
getMainYAxis() {
+ const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumn;
+
+ if (sourceField === 'Records' || !sourceField) {
+ return this.getRecordsColumn(label);
+ }
+
+ return this.getColumnBasedOnType(sourceField!, operationType);
+ }
+
+ getRecordsColumn(label?: string): CountIndexPatternColumn {
return {
dataType: 'number',
isBucketed: false,
- label: 'Count of records',
+ label: label || 'Count of records',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
- ...this.reportViewConfig.yAxisColumn,
} as CountIndexPatternColumn;
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
index 8a27d7ddd428b..9f8a336b59d34 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts
@@ -24,7 +24,7 @@ export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries {
yAxisColumn: {
operationType: 'count',
},
- hasMetricType: false,
+ hasOperationType: false,
defaultFilters: [],
breakdowns: ['agent.hostname'],
filters: [],
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
index 6214975d8f1dd..d4b807de11f4e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts
@@ -7,7 +7,6 @@
import { DataSeries } from '../../types';
import { FieldLabels } from '../constants';
-import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,11 +22,11 @@ export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'system.cpu.user.pct',
label: 'CPU Usage %',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: [],
breakdowns: ['host.hostname'],
filters: [],
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
index 6f46c175f7882..38d1c425fc09a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts
@@ -7,7 +7,6 @@
import { DataSeries } from '../../types';
import { FieldLabels } from '../constants';
-import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,11 +22,11 @@ export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'system.memory.used.pct',
label: 'Memory Usage %',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: [],
breakdowns: ['host.hostname'],
filters: [],
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
index 1bc9fed9c3f80..07a521225b38d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts
@@ -7,7 +7,6 @@
import { DataSeries } from '../../types';
import { FieldLabels } from '../constants';
-import { OperationType } from '../../../../../../../lens/public';
interface Props {
seriesId: string;
@@ -23,10 +22,10 @@ export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'system.memory.used.pct',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: [],
breakdowns: ['host.hostname'],
filters: [],
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
index a1a3acd51f89c..cd38d912850cf 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts
@@ -10,14 +10,21 @@ import { FieldLabels } from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
+ CLS_FIELD,
+ FCP_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
+ TBT_FIELD,
+ TRANSACTION_DURATION,
TRANSACTION_TYPE,
USER_AGENT_DEVICE,
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
+ TRANSACTION_TIME_TO_FIRST_BYTE,
} from '../constants/elasticsearch_fieldnames';
export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
@@ -30,10 +37,10 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps):
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'count',
- label: 'Page views',
+ sourceField: 'business.kpi',
+ operationType: 'median',
},
- hasMetricType: false,
+ hasOperationType: false,
defaultFilters: [
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
@@ -45,10 +52,10 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps):
],
breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE],
filters: [
- buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
- buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
+ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
+ ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
],
- labels: { ...FieldLabels, SERVICE_NAME: 'Web Application' },
+ labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' },
reportDefinitions: [
{
field: SERVICE_NAME,
@@ -58,14 +65,18 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps):
field: SERVICE_ENVIRONMENT,
},
{
- field: 'Business.KPI',
+ field: 'business.kpi',
custom: true,
defaultValue: 'Records',
options: [
- {
- field: 'Records',
- label: 'Page views',
- },
+ { field: 'Records', label: 'Page views' },
+ { label: 'Page load time', field: TRANSACTION_DURATION, columnType: 'operation' },
+ { label: 'Backend time', field: TRANSACTION_TIME_TO_FIRST_BYTE, columnType: 'operation' },
+ { label: 'First contentful paint', field: FCP_FIELD, columnType: 'operation' },
+ { label: 'Total blocking time', field: TBT_FIELD, columnType: 'operation' },
+ { label: 'Largest contentful paint', field: LCP_FIELD, columnType: 'operation' },
+ { label: 'First input delay', field: FID_FIELD, columnType: 'operation' },
+ { label: 'Cumulative layout shift', field: CLS_FIELD, columnType: 'operation' },
],
},
],
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
index 7005dea29d60d..4b6d5dd6e741b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts
@@ -19,6 +19,7 @@ import {
SERVICE_NAME,
TBT_FIELD,
TRANSACTION_DURATION,
+ TRANSACTION_TIME_TO_FIRST_BYTE,
TRANSACTION_TYPE,
USER_AGENT_DEVICE,
USER_AGENT_NAME,
@@ -36,10 +37,10 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP
sourceField: 'performance.metric',
},
yAxisColumn: {
- operationType: 'count',
+ sourceField: 'Records',
label: 'Pages loaded',
},
- hasMetricType: false,
+ hasOperationType: false,
defaultFilters: [
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
@@ -64,6 +65,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP
defaultValue: TRANSACTION_DURATION,
options: [
{ label: 'Page load time', field: TRANSACTION_DURATION },
+ { label: 'Backend time', field: TRANSACTION_TIME_TO_FIRST_BYTE },
{ label: 'First contentful paint', field: FCP_FIELD },
{ label: 'Total blocking time', field: TBT_FIELD },
// FIXME, review if we need these descriptions
@@ -74,8 +76,8 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP
},
],
filters: [
- buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
- buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
+ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
+ ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
],
labels: {
...FieldLabels,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts
index 4f036f0b9be65..8dad1839f0bcd 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts
@@ -16,6 +16,7 @@ export const syntheticsFieldFormats: FieldFormat[] = [
inputFormat: 'microseconds',
outputFormat: 'asMilliseconds',
outputPrecision: 0,
+ showSuffix: true,
},
},
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
index f0ec3f0c31bef..efbc3d14441c2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts
@@ -6,8 +6,7 @@
*/
import { DataSeries } from '../../types';
-import { FieldLabels } from '../constants/constants';
-import { OperationType } from '../../../../../../../lens/public';
+import { FieldLabels } from '../constants';
interface Props {
seriesId: string;
@@ -23,11 +22,11 @@ export function getMonitorDurationConfig({ seriesId }: Props): DataSeries {
sourceField: '@timestamp',
},
yAxisColumn: {
- operationType: 'average' as OperationType,
+ operationType: 'average',
sourceField: 'monitor.duration.us',
label: 'Monitor duration (ms)',
},
- hasMetricType: true,
+ hasOperationType: true,
defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'],
breakdowns: [
'observer.geo.name',
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
index 40c9f5750fb4d..68a36dcdcaf85 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts
@@ -25,7 +25,7 @@ export function getMonitorPingsConfig({ seriesId }: Props): DataSeries {
operationType: 'count',
label: 'Monitor pings',
},
- hasMetricType: false,
+ hasOperationType: false,
defaultFilters: ['observer.geo.name'],
breakdowns: ['monitor.status', 'observer.geo.name', 'monitor.type'],
filters: [],
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 c885673134786..c6b7b5d92d5f8 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
@@ -13,7 +13,7 @@ import { URL_KEYS } from './constants/url_constants';
export function convertToShortUrl(series: SeriesUrl) {
const {
- metric,
+ operationType,
seriesType,
reportType,
breakdown,
@@ -23,7 +23,7 @@ export function convertToShortUrl(series: SeriesUrl) {
} = series;
return {
- [URL_KEYS.METRIC_TYPE]: metric,
+ [URL_KEYS.OPERATION_TYPE]: operationType,
[URL_KEYS.REPORT_TYPE]: reportType,
[URL_KEYS.SERIES_TYPE]: seriesType,
[URL_KEYS.BREAK_DOWN]: breakdown,
@@ -49,6 +49,9 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
}
export function buildPhraseFilter(field: string, value: any, indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field)!;
- return esFilters.buildPhraseFilter(fieldMeta, value, indexPattern);
+ const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ if (fieldMeta) {
+ return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)];
+ }
+ return [];
}
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 0e7bc80e8659c..6bc069aafa5b8 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
@@ -6,8 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
-import styled from 'styled-components';
-import { EuiLoadingSpinner, EuiPanel, EuiTitle } from '@elastic/eui';
+import { EuiPanel, EuiTitle } from '@elastic/eui';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
@@ -15,7 +14,6 @@ import { SeriesEditor } from './series_editor/series_editor';
import { useUrlStorage } from './hooks/use_url_storage';
import { useLensAttributes } from './hooks/use_lens_attributes';
import { EmptyView } from './components/empty_view';
-import { useIndexPatternContext } from './hooks/use_default_index_pattern';
import { TypedLensByValueInput } from '../../../../../lens/public';
export function ExploratoryView() {
@@ -27,15 +25,12 @@ export function ExploratoryView() {
null
);
- const { indexPattern } = useIndexPatternContext();
-
const LensComponent = lens?.EmbeddableComponent;
const { firstSeriesId: seriesId, firstSeries: series } = useUrlStorage();
const lensAttributesT = useLensAttributes({
seriesId,
- indexPattern,
});
useEffect(() => {
@@ -48,11 +43,6 @@ export function ExploratoryView() {
{lens ? (
<>
- {!indexPattern && (
-
-
-
- )}
{lensAttributes && seriesId && series?.reportType && series?.time ? (
);
}
-
-const SpinnerWrap = styled.div`
- height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
index 7ead7d5e3cfad..c5a4d02492662 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx
@@ -39,6 +39,7 @@ export function IndexPatternContextProvider({
} = useKibana();
const loadIndexPattern = async (dataType: AppDataType) => {
+ setIndexPattern(undefined);
const obsvIndexP = new ObservabilityIndexPatterns(data);
const indPattern = await obsvIndexP.getIndexPattern(dataType);
setIndexPattern(indPattern!);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
index 76fd64ef86736..de4343b290118 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts
@@ -27,15 +27,17 @@ export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => {
const firstSeries = allSeries[firstSeriesId];
+ let dataType: DataType = firstSeries?.dataType ?? 'rum';
+
+ if (firstSeries?.rt) {
+ dataType = ReportToDataTypeMap[firstSeries?.rt];
+ }
+
const { data: indexPattern, error } = useFetcher(() => {
const obsvIndexP = new ObservabilityIndexPatterns(data);
- let reportType: DataType = 'apm';
- if (firstSeries?.rt) {
- reportType = ReportToDataTypeMap[firstSeries?.rt];
- }
- return obsvIndexP.getIndexPattern(reportType);
- }, [firstSeries?.rt, data]);
+ return obsvIndexP.getIndexPattern(dataType);
+ }, [dataType, data]);
if (error) {
throw error;
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 274542380c137..555b21618c4b2 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
@@ -11,12 +11,11 @@ import { LensAttributes } from '../configurations/lens_attributes';
import { useUrlStorage } from './use_url_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
-import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { DataSeries, SeriesUrl, UrlFilter } from '../types';
+import { useIndexPatternContext } from './use_default_index_pattern';
interface Props {
seriesId: string;
- indexPattern?: IndexPattern | null;
}
export const getFiltersFromDefs = (
@@ -39,12 +38,12 @@ export const getFiltersFromDefs = (
export const useLensAttributes = ({
seriesId,
- indexPattern,
}: Props): TypedLensByValueInput['attributes'] | null => {
const { series } = useUrlStorage(seriesId);
- const { breakdown, seriesType, metric: metricType, reportType, reportDefinitions = {} } =
- series ?? {};
+ const { breakdown, seriesType, operationType, reportType, reportDefinitions = {} } = series ?? {};
+
+ const { indexPattern } = useIndexPatternContext();
return useMemo(() => {
if (!indexPattern || !reportType) {
@@ -66,7 +65,7 @@ export const useLensAttributes = ({
dataViewConfig,
seriesType,
filters,
- metricType,
+ operationType,
reportDefinitions
);
@@ -79,7 +78,7 @@ export const useLensAttributes = ({
indexPattern,
breakdown,
seriesType,
- metricType,
+ operationType,
reportType,
reportDefinitions,
seriesId,
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
index 6256b3b134f8c..a4fe15025245a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_storage.tsx
@@ -26,9 +26,9 @@ export function UrlStorageContextProvider({
}
function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
- const { mt, st, rt, bd, ft, time, rdf, ...restSeries } = newValue;
+ const { op, st, rt, bd, ft, time, rdf, ...restSeries } = newValue;
return {
- metric: mt,
+ operationType: op,
reportType: rt!,
seriesType: st,
breakdown: bd,
@@ -40,7 +40,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
}
interface ShortUrlSeries {
- [URL_KEYS.METRIC_TYPE]?: OperationType;
+ [URL_KEYS.OPERATION_TYPE]?: OperationType;
[URL_KEYS.REPORT_TYPE]?: ReportViewTypeId;
[URL_KEYS.SERIES_TYPE]?: SeriesType;
[URL_KEYS.BREAK_DOWN]?: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
similarity index 74%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
index f291d0de4dac0..bac935dbecbe7 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
@@ -7,14 +7,14 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { SeriesChartTypes, XYChartTypes } from './chart_types';
import { mockUrlStorage, render } from '../../rtl_helpers';
+import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types';
-describe.skip('SeriesChartTypes', function () {
+describe.skip('SeriesChartTypesSelect', function () {
it('should render properly', async function () {
mockUrlStorage({});
- render();
+ render();
await waitFor(() => {
screen.getByText(/chart type/i);
@@ -24,7 +24,7 @@ describe.skip('SeriesChartTypes', function () {
it('should call set series on change', async function () {
const { setSeries } = mockUrlStorage({});
- render();
+ render();
await waitFor(() => {
screen.getByText(/chart type/i);
@@ -42,11 +42,11 @@ describe.skip('SeriesChartTypes', function () {
expect(setSeries).toHaveBeenCalledTimes(3);
});
- describe('XYChartTypes', function () {
+ describe('XYChartTypesSelect', function () {
it('should render properly', async function () {
mockUrlStorage({});
- render();
+ render();
await waitFor(() => {
screen.getByText(/chart type/i);
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_builder/columns/chart_types.tsx
new file mode 100644
index 0000000000000..029c39df13aad
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
@@ -0,0 +1,104 @@
+/*
+ * Copyright 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, 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 { useUrlStorage } from '../../hooks/use_url_storage';
+import { SeriesType } from '../../../../../../../lens/public';
+
+export function SeriesChartTypesSelect({
+ seriesId,
+ defaultChartType,
+}: {
+ seriesId: string;
+ defaultChartType: SeriesType;
+}) {
+ const { series, setSeries, allSeries } = useUrlStorage(seriesId);
+
+ const seriesType = series?.seriesType ?? defaultChartType;
+
+ const onChange = (value: SeriesType) => {
+ Object.keys(allSeries).forEach((seriesKey) => {
+ const seriesN = allSeries[seriesKey];
+
+ setSeries(seriesKey, { ...seriesN, seriesType: value });
+ });
+ };
+
+ return (
+
+ );
+}
+
+export interface XYChartTypesProps {
+ label?: string;
+ value: SeriesType;
+ includeChartTypes?: SeriesType[];
+ excludeChartTypes?: SeriesType[];
+ onChange: (value: SeriesType) => void;
+}
+
+export function XYChartTypesSelect({
+ onChange,
+ value,
+ includeChartTypes,
+ excludeChartTypes,
+}: XYChartTypesProps) {
+ const {
+ services: { lens },
+ } = useKibana();
+
+ const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]);
+
+ let vizTypes = data ?? [];
+
+ if ((excludeChartTypes ?? []).length > 0) {
+ vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id as SeriesType));
+ }
+
+ if ((includeChartTypes ?? []).length > 0) {
+ vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id as SeriesType));
+ }
+
+ const options = (vizTypes ?? []).map(({ id, fullLabel, label, icon }) => {
+ const LabelWithIcon = (
+
+
+
+
+ {fullLabel || label}
+
+ );
+ return {
+ value: id as SeriesType,
+ inputDisplay: LabelWithIcon,
+ dropdownDisplay: LabelWithIcon,
+ };
+ });
+
+ return (
+
+ );
+}
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
index 039cdfc9b73f5..41b9f7d22ba00 100644
--- 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
@@ -32,7 +32,7 @@ describe('DataTypesCol', function () {
});
it('should set series on change on already selected', function () {
- const { setSeries } = mockUrlStorage({
+ const { removeSeries } = mockUrlStorage({
data: {
[NEW_SERIES_KEY]: {
dataType: 'synthetics',
@@ -54,6 +54,6 @@ describe('DataTypesCol', function () {
fireEvent.click(button);
// undefined on click selected
- expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: undefined });
+ expect(removeSeries).toHaveBeenCalledWith('newSeriesKey');
});
});
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
index b6464bbe3c6ed..d7e90d34a2596 100644
--- 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
@@ -20,15 +20,19 @@ export const dataTypes: Array<{ id: AppDataType; label: string }> = [
];
export function DataTypesCol() {
- const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY);
+ const { series, setSeries, removeSeries } = useUrlStorage(NEW_SERIES_KEY);
- const { loadIndexPattern } = useIndexPatternContext();
+ const { loadIndexPattern, indexPattern } = useIndexPatternContext();
const onDataTypeChange = (dataType?: AppDataType) => {
if (dataType) {
loadIndexPattern(dataType);
}
- setSeries(NEW_SERIES_KEY, { dataType } as any);
+ if (!dataType) {
+ removeSeries(NEW_SERIES_KEY);
+ } else {
+ setSeries(NEW_SERIES_KEY, { dataType } as any);
+ }
};
const selectedDataType = series.dataType;
@@ -43,6 +47,8 @@ export function DataTypesCol() {
iconType="arrowRight"
color={selectedDataType === dataTypeId ? 'primary' : 'text'}
fill={selectedDataType === dataTypeId}
+ isDisabled={!indexPattern}
+ isLoading={!indexPattern && selectedDataType === dataTypeId}
onClick={() => {
onDataTypeChange(dataTypeId === selectedDataType ? undefined : dataTypeId);
}}
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_builder/columns/operation_type_select.test.tsx
new file mode 100644
index 0000000000000..e05f91b4bb0bd
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright 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 { mockUrlStorage, render } from '../../rtl_helpers';
+import { OperationTypeSelect } from './operation_type_select';
+
+describe('OperationTypeSelect', function () {
+ it('should render properly', function () {
+ render();
+
+ screen.getByText('Select an option: , is selected');
+ });
+
+ it('should display selected value', function () {
+ mockUrlStorage({
+ data: {
+ 'performance-distribution': {
+ reportType: 'kpi',
+ operationType: 'median',
+ time: { from: 'now-15m', to: 'now' },
+ },
+ },
+ });
+
+ render();
+
+ screen.getByText('Median');
+ });
+
+ it('should call set series on change', function () {
+ const { setSeries } = mockUrlStorage({
+ data: {
+ 'series-id': {
+ reportType: 'kpi',
+ operationType: 'median',
+ time: { from: 'now-15m', to: 'now' },
+ },
+ },
+ });
+
+ render();
+
+ fireEvent.click(screen.getByTestId('operationTypeSelect'));
+
+ expect(setSeries).toHaveBeenCalledWith('series-id', {
+ operationType: 'median',
+ reportType: 'kpi',
+ time: { from: 'now-15m', to: 'now' },
+ });
+
+ fireEvent.click(screen.getByText('95th Percentile'));
+ expect(setSeries).toHaveBeenCalledWith('series-id', {
+ operationType: '95th',
+ reportType: 'kpi',
+ time: { from: 'now-15m', to: 'now' },
+ });
+ });
+});
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_builder/columns/operation_type_select.tsx
new file mode 100644
index 0000000000000..46167af0b244a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSuperSelect } from '@elastic/eui';
+
+import { useUrlStorage } from '../../hooks/use_url_storage';
+import { OperationType } from '../../../../../../../lens/public';
+
+export function OperationTypeSelect({
+ seriesId,
+ defaultOperationType,
+}: {
+ seriesId: string;
+ defaultOperationType?: OperationType;
+}) {
+ const { series, setSeries } = useUrlStorage(seriesId);
+
+ const operationType = series?.operationType;
+
+ const onChange = (value: OperationType) => {
+ setSeries(seriesId, { ...series, operationType: value });
+ };
+
+ useEffect(() => {
+ setSeries(seriesId, { ...series, operationType: operationType || defaultOperationType });
+ }, [defaultOperationType, seriesId, operationType, setSeries, series]);
+
+ const options = [
+ {
+ value: 'average' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.average', {
+ defaultMessage: 'Average',
+ }),
+ },
+ {
+ value: 'median' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.median', {
+ defaultMessage: 'Median',
+ }),
+ },
+ {
+ value: '75th' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.75thPercentile', {
+ defaultMessage: '75th Percentile',
+ }),
+ },
+ {
+ value: '90th' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.90thPercentile', {
+ defaultMessage: '90th Percentile',
+ }),
+ },
+ {
+ value: '95th' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.95thPercentile', {
+ defaultMessage: '95th Percentile',
+ }),
+ },
+ {
+ value: '99th' as OperationType,
+ inputDisplay: i18n.translate('xpack.observability.expView.operationType.99thPercentile', {
+ defaultMessage: '99th Percentile',
+ }),
+ },
+ ];
+
+ 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
index b907efb57d5c2..a386b73a8f917 100644
--- 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
@@ -12,6 +12,8 @@ import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage';
import { CustomReportField } from '../custom_report_field';
import FieldValueSuggestions from '../../../field_value_suggestions';
import { DataSeries } from '../../types';
+import { SeriesChartTypesSelect } from './chart_types';
+import { OperationTypeSelect } from './operation_type_select';
export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSeries }) {
const { indexPattern } = useIndexPatternContext();
@@ -20,7 +22,14 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe
const { reportDefinitions: rtd = {} } = series;
- const { reportDefinitions, labels, filters } = dataViewSeries;
+ const {
+ reportDefinitions,
+ labels,
+ filters,
+ defaultSeriesType,
+ hasOperationType,
+ yAxisColumn,
+ } = dataViewSeries;
const onChange = (field: string, value?: string) => {
if (!value) {
@@ -91,6 +100,17 @@ export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSe
)}
))}
+
+
+
+ {hasOperationType && (
+
+
+
+ )}
);
}
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
index 567e2654130e8..f845bf9885af9 100644
--- 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
@@ -10,6 +10,7 @@ import { fireEvent, screen } from '@testing-library/react';
import { mockUrlStorage, 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 () {
it('should render properly', function () {
@@ -60,6 +61,9 @@ describe('ReportTypesCol', function () {
fireEvent.click(button);
// undefined on click selected
- expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'synthetics' });
+ expect(setSeries).toHaveBeenCalledWith('newSeriesKey', {
+ dataType: 'synthetics',
+ time: DEFAULT_TIME,
+ });
});
});
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
index a473ddb570526..a8f98b98026b6 100644
--- 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
@@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { ReportViewTypeId, SeriesUrl } from '../../types';
import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_storage';
+import { DEFAULT_TIME } from '../../configurations/constants';
+import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
interface Props {
reportTypes: Array<{ id: ReportViewTypeId; label: string }>;
@@ -21,6 +23,8 @@ export function ReportTypesCol({ reportTypes }: Props) {
setSeries,
} = useUrlStorage(NEW_SERIES_KEY);
+ const { indexPattern } = useIndexPatternContext();
+
return reportTypes?.length > 0 ? (
{reportTypes.map(({ id: reportType, label }) => (
@@ -31,16 +35,19 @@ export function ReportTypesCol({ reportTypes }: Props) {
iconType="arrowRight"
color={selectedReportType === reportType ? 'primary' : 'text'}
fill={selectedReportType === reportType}
+ isDisabled={!indexPattern}
onClick={() => {
if (reportType === selectedReportType) {
setSeries(NEW_SERIES_KEY, {
dataType: restSeries.dataType,
+ time: DEFAULT_TIME,
} as SeriesUrl);
} else {
setSeries(NEW_SERIES_KEY, {
...restSeries,
reportType,
reportDefinitions: {},
+ time: restSeries?.time ?? DEFAULT_TIME,
});
}
}}
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
index 053f301529635..2280109fdacdf 100644
--- 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
@@ -49,7 +49,14 @@ export const ReportTypes: Record {
@@ -145,7 +154,7 @@ export function SeriesBuilder() {
columns={columns}
cellProps={{ style: { borderRight: '1px solid #d3dae6' } }}
/>
-
+
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/series_date_picker/index.tsx
index 922d33ffd39ac..960c2978287bc 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/series_date_picker/index.tsx
@@ -10,6 +10,7 @@ import React, { useEffect } from 'react';
import { useHasData } from '../../../../hooks/use_has_data';
import { useUrlStorage } from '../hooks/use_url_storage';
import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
+import { DEFAULT_TIME } from '../configurations/constants';
export interface TimePickerTime {
from: string;
@@ -38,7 +39,7 @@ export function SeriesDatePicker({ seriesId }: Props) {
useEffect(() => {
if (!series || !series.time) {
- setSeries(seriesId, { ...series, time: { from: 'now-5h', to: 'now' } });
+ setSeries(seriesId, { ...series, time: DEFAULT_TIME });
}
}, [seriesId, series, setSeries]);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
index acc9ba9658a08..8fe1d5ed9f2ac 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import { mockUrlStorage, mockUseHasData, render } from '../rtl_helpers';
import { fireEvent, waitFor } from '@testing-library/react';
import { SeriesDatePicker } from './index';
+import { DEFAULT_TIME } from '../configurations/constants';
describe('SeriesDatePicker', function () {
it('should render properly', function () {
@@ -40,7 +41,7 @@ describe('SeriesDatePicker', function () {
expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
breakdown: 'monitor.status',
reportType: 'upp',
- time: { from: 'now-5h', to: 'now' },
+ time: DEFAULT_TIME,
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx
index c6209381a4da1..fe54262e13844 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx
@@ -8,8 +8,8 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { DataSeries } from '../../types';
-import { SeriesChartTypes } from './chart_types';
-import { MetricSelection } from './metric_selection';
+import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select';
+import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types';
interface Props {
series: DataSeries;
@@ -17,13 +17,13 @@ interface Props {
export function ActionsCol({ series }: Props) {
return (
-
+
-
+
- {series.hasMetricType && (
+ {series.hasOperationType && (
-
+
)}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
deleted file mode 100644
index f83630cff414a..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
+++ /dev/null
@@ -1,149 +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, { useState } from 'react';
-
-import {
- EuiButton,
- EuiButtonGroup,
- EuiButtonIcon,
- EuiLoadingSpinner,
- EuiPopover,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import styled from 'styled-components';
-import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
-import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
-import { useFetcher } from '../../../../..';
-import { useUrlStorage } from '../../hooks/use_url_storage';
-import { SeriesType } from '../../../../../../../lens/public';
-
-export function SeriesChartTypes({
- seriesId,
- defaultChartType,
-}: {
- seriesId: string;
- defaultChartType: SeriesType;
-}) {
- const { series, setSeries, allSeries } = useUrlStorage(seriesId);
-
- const seriesType = series?.seriesType ?? defaultChartType;
-
- const onChange = (value: SeriesType) => {
- Object.keys(allSeries).forEach((seriesKey) => {
- const seriesN = allSeries[seriesKey];
-
- setSeries(seriesKey, { ...seriesN, seriesType: value });
- });
- };
-
- return (
-
- );
-}
-
-export interface XYChartTypesProps {
- onChange: (value: SeriesType) => void;
- value: SeriesType;
- label?: string;
- includeChartTypes?: string[];
- excludeChartTypes?: string[];
-}
-
-export function XYChartTypes({
- onChange,
- value,
- label,
- includeChartTypes,
- excludeChartTypes,
-}: XYChartTypesProps) {
- const [isOpen, setIsOpen] = useState(false);
-
- const {
- services: { lens },
- } = useKibana();
-
- const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]);
-
- let vizTypes = data ?? [];
-
- if ((excludeChartTypes ?? []).length > 0) {
- vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id));
- }
-
- if ((includeChartTypes ?? []).length > 0) {
- vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id));
- }
-
- return loading ? (
-
- ) : (
- id === value)?.icon}
- onClick={() => {
- setIsOpen((prevState) => !prevState);
- }}
- >
- {label}
-
- ) : (
- id === value)?.label}
- iconType={vizTypes.find(({ id }) => id === value)?.icon!}
- onClick={() => {
- setIsOpen((prevState) => !prevState);
- }}
- />
- )
- }
- closePopover={() => setIsOpen(false)}
- >
- ({
- id: t.id,
- label: t.label,
- title: t.label,
- iconType: t.icon || 'empty',
- 'data-test-subj': `lnsXY_seriesType-${t.id}`,
- }))}
- idSelected={value}
- onChange={(valueN: string) => {
- onChange(valueN as SeriesType);
- }}
- />
-
- );
-}
-
-const ButtonGroup = styled(EuiButtonGroup)`
- &&& {
- .euiButtonGroupButton-isSelected {
- background-color: #a5a9b1 !important;
- }
- }
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx
deleted file mode 100644
index ced04f0a59c8c..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx
+++ /dev/null
@@ -1,112 +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 { mockUrlStorage, render } from '../../rtl_helpers';
-import { MetricSelection } from './metric_selection';
-
-describe('MetricSelection', function () {
- it('should render properly', function () {
- render();
-
- screen.getByText('Average');
- });
-
- it('should display selected value', function () {
- mockUrlStorage({
- data: {
- 'performance-distribution': {
- reportType: 'kpi',
- metric: 'median',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- });
-
- render();
-
- screen.getByText('Median');
- });
-
- it('should be disabled on disabled state', function () {
- render();
-
- const btn = screen.getByRole('button');
-
- expect(btn.classList).toContain('euiButton-isDisabled');
- });
-
- it('should call set series on change', function () {
- const { setSeries } = mockUrlStorage({
- data: {
- 'performance-distribution': {
- reportType: 'kpi',
- metric: 'median',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- });
-
- render();
-
- fireEvent.click(screen.getByText('Median'));
-
- screen.getByText('Chart metric group');
-
- fireEvent.click(screen.getByText('95th Percentile'));
-
- expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', {
- metric: '95th',
- reportType: 'kpi',
- time: { from: 'now-15m', to: 'now' },
- });
- // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times
- // This should be one https://github.com/elastic/eui/issues/4629
- expect(setSeries).toHaveBeenCalledTimes(3);
- });
-
- it('should call set series on change for all series', function () {
- const { setSeries } = mockUrlStorage({
- data: {
- 'page-views': {
- reportType: 'kpi',
- metric: 'median',
- time: { from: 'now-15m', to: 'now' },
- },
- 'performance-distribution': {
- reportType: 'kpi',
- metric: 'median',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- });
-
- render();
-
- fireEvent.click(screen.getByText('Median'));
-
- screen.getByText('Chart metric group');
-
- fireEvent.click(screen.getByText('95th Percentile'));
-
- expect(setSeries).toHaveBeenNthCalledWith(1, 'page-views', {
- metric: '95th',
- reportType: 'kpi',
- time: { from: 'now-15m', to: 'now' },
- });
-
- expect(setSeries).toHaveBeenNthCalledWith(2, 'performance-distribution', {
- metric: '95th',
- reportType: 'kpi',
- time: { from: 'now-15m', to: 'now' },
- });
- // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times
- // This should be one https://github.com/elastic/eui/issues/4629
- expect(setSeries).toHaveBeenCalledTimes(6);
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
deleted file mode 100644
index fa4202d2c30ad..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useState } from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui';
-import { useUrlStorage } from '../../hooks/use_url_storage';
-import { OperationType } from '../../../../../../../lens/public';
-
-const toggleButtons = [
- {
- id: `average`,
- label: i18n.translate('xpack.observability.expView.metricsSelect.average', {
- defaultMessage: 'Average',
- }),
- },
- {
- id: `median`,
- label: i18n.translate('xpack.observability.expView.metricsSelect.median', {
- defaultMessage: 'Median',
- }),
- },
- {
- id: `95th`,
- label: i18n.translate('xpack.observability.expView.metricsSelect.9thPercentile', {
- defaultMessage: '95th Percentile',
- }),
- },
- {
- id: `99th`,
- label: i18n.translate('xpack.observability.expView.metricsSelect.99thPercentile', {
- defaultMessage: '99th Percentile',
- }),
- },
-];
-
-export function MetricSelection({
- seriesId,
- isDisabled,
-}: {
- seriesId: string;
- isDisabled: boolean;
-}) {
- const { series, setSeries, allSeries } = useUrlStorage(seriesId);
-
- const [isOpen, setIsOpen] = useState(false);
-
- const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'average');
-
- const onChange = (optionId: OperationType) => {
- setToggleIdSelected(optionId);
-
- Object.keys(allSeries).forEach((seriesKey) => {
- const seriesN = allSeries[seriesKey];
-
- setSeries(seriesKey, { ...seriesN, metric: optionId });
- });
- };
- const button = (
- setIsOpen((prevState) => !prevState)}
- size="s"
- color="text"
- isDisabled={isDisabled}
- >
- {toggleButtons.find(({ id }) => id === toggleIdSelected)!.label}
-
- );
-
- return (
- setIsOpen(false)}>
- onChange(id as OperationType)}
- />
-
- );
-}
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 d673fc4d6f6ee..141dcecd0ba5b 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
@@ -9,9 +9,9 @@ import { PaletteOutput } from 'src/plugins/charts/public';
import {
LastValueIndexPatternColumn,
DateHistogramIndexPatternColumn,
+ FieldBasedIndexPatternColumn,
SeriesType,
OperationType,
- IndexPatternColumn,
} from '../../../../../lens/public';
import { PersistableFilter } from '../../../../../lens/common';
@@ -41,14 +41,19 @@ export interface ReportDefinition {
required?: boolean;
custom?: boolean;
defaultValue?: string;
- options?: Array<{ field: string; label: string; description?: string }>;
+ options?: Array<{
+ field: string;
+ label: string;
+ description?: string;
+ columnType?: 'range' | 'operation';
+ }>;
}
export interface DataSeries {
reportType: ReportViewType;
id: string;
xAxisColumn: Partial | Partial;
- yAxisColumn: Partial;
+ yAxisColumn: Partial;
breakdowns: string[];
defaultSeriesType: SeriesType;
@@ -57,7 +62,7 @@ export interface DataSeries {
filters?: PersistableFilter[];
reportDefinitions: ReportDefinition[];
labels: Record;
- hasMetricType: boolean;
+ hasOperationType: boolean;
palette?: PaletteOutput;
}
@@ -70,7 +75,7 @@ export interface SeriesUrl {
filters?: UrlFilter[];
seriesType?: SeriesType;
reportType: ReportViewTypeId;
- metric?: OperationType;
+ operationType?: OperationType;
dataType?: AppDataType;
reportDefinitions?: Record;
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
index e0a2941b24d3c..527ef48364d22 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
@@ -47,12 +47,16 @@ const appToPatternMap: Record = {
};
export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) {
- return (
+ const isSame =
param1?.inputFormat === param2?.inputFormat &&
param1?.outputFormat === param2?.outputFormat &&
- param1?.showSuffix === param2?.showSuffix &&
- param2?.outputPrecision === param1?.outputPrecision
- );
+ param1?.showSuffix === param2?.showSuffix;
+
+ if (param2.outputPrecision !== undefined) {
+ return param2?.outputPrecision === param1?.outputPrecision && isSame;
+ }
+
+ return isSame;
}
export class ObservabilityIndexPatterns {
From 98f40a216a7188b97568f2363af1f757b3bfe97e Mon Sep 17 00:00:00 2001
From: Alexey Antonov
Date: Mon, 12 Apr 2021 16:56:28 +0300
Subject: [PATCH 09/79] [TSVB] Visualize runtime fields (#95772)
* [TSVB] Visualize runtime fields
* fix CI
* Update visualization_error.tsx
* Update build_request_body.ts
* fix group by for table view
* fix issue on switching the index pattern mode
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/calculate_label.test.ts | 23 +++++++
.../common/calculate_label.ts | 12 ++--
.../vis_type_timeseries/common/constants.ts | 1 +
.../common/fields_utils.test.ts | 13 +---
.../common/fields_utils.ts | 60 +++++++++++++---
.../common/index_patterns_utils.test.ts | 12 ++--
.../common/index_patterns_utils.ts | 18 +++--
.../vis_type_timeseries/common/types.ts | 4 +-
.../components/aggs/field_select.tsx | 69 ++++++++++++++-----
.../components/aggs/filter_ratio.js | 20 +++---
.../components/aggs/metric_select.js | 4 +-
.../application/components/aggs/percentile.js | 20 +++---
.../aggs/percentile_rank/percentile_rank.tsx | 20 +++---
.../components/aggs/positive_rate.js | 23 +++----
.../application/components/aggs/std_agg.js | 32 +++------
.../components/aggs/std_deviation.js | 20 +++---
.../application/components/aggs/top_hit.js | 38 ++++------
.../components/annotations_editor.js | 21 +++---
.../application/components/index_pattern.js | 24 +++----
.../index_pattern_select.tsx | 7 +-
.../components/panel_config/table.tsx | 21 +++---
.../splits/__snapshots__/terms.test.js.snap | 56 +++++++--------
.../application/components/splits/terms.js | 22 +++---
.../components/vis_types/table/config.js | 18 ++---
.../public/timeseries_vis_renderer.tsx | 3 +-
.../lib/cached_index_pattern_fetcher.test.ts | 23 +------
.../search_strategies/lib/fields_fetcher.ts | 15 ++--
.../annotations/build_request_body.ts | 16 +----
.../annotations/get_request_params.ts | 9 ++-
....js => get_interval_and_timefield.test.ts} | 19 +++--
...field.js => get_interval_and_timefield.ts} | 23 ++++---
.../server/lib/vis_data/get_table_data.ts | 15 ++--
.../server/lib/vis_data/helpers/get_splits.js | 5 +-
.../annotations/date_histogram.js | 6 +-
.../request_processors/annotations/query.js | 19 +++--
.../annotations/top_hits.js | 5 +-
.../series/date_histogram.js | 7 +-
.../series/filter_ratios.js | 6 +-
.../series/filter_ratios.test.js | 2 +-
.../series/metric_buckets.js | 4 +-
.../series/positive_rate.js | 4 +-
.../request_processors/series/query.js | 10 +--
.../request_processors/series/query.test.js | 21 +++---
.../series/sibling_buckets.js | 4 +-
.../series/split_by_filter.js | 4 +-
.../series/split_by_filter.test.js | 11 ++-
.../series/split_by_filters.js | 9 ++-
.../series/split_by_filters.test.js | 11 ++-
.../series/split_by_terms.js | 5 +-
.../series/split_by_terms.test.js | 20 ++++--
.../table/date_histogram.js | 8 ++-
.../request_processors/table/filter_ratios.js | 6 +-
.../table/metric_buckets.js | 4 +-
.../request_processors/table/positive_rate.js | 4 +-
.../request_processors/table/query.js | 8 +--
.../table/sibling_buckets.js | 4 +-
.../table/split_by_everything.js | 4 +-
.../table/split_by_terms.js | 4 +-
.../response_processors/series/series_agg.js | 10 ++-
.../response_processors/table/series_agg.js | 10 ++-
.../lib/vis_data/series/build_request_body.ts | 2 +-
.../lib/vis_data/series/get_request_params.ts | 3 +-
.../components/visualization_container.tsx | 11 ++-
.../public/components/visualization_error.tsx | 42 +++++++++++
.../test/functional/apps/rollup_job/tsvb.js | 1 +
65 files changed, 532 insertions(+), 423 deletions(-)
rename src/plugins/vis_type_timeseries/server/lib/vis_data/{get_interval_and_timefield.test.js => get_interval_and_timefield.test.ts} (68%)
rename src/plugins/vis_type_timeseries/server/lib/vis_data/{get_interval_and_timefield.js => get_interval_and_timefield.ts} (57%)
create mode 100644 src/plugins/visualizations/public/components/visualization_error.tsx
diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.test.ts b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts
index d5277623a136d..eab9665436c01 100644
--- a/src/plugins/vis_type_timeseries/common/calculate_label.test.ts
+++ b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts
@@ -8,6 +8,7 @@
import { calculateLabel } from './calculate_label';
import type { MetricsItemsSchema } from './types';
+import { SanitizedFieldType } from './types';
describe('calculateLabel(metric, metrics)', () => {
test('returns the metric.alias if set', () => {
@@ -82,4 +83,26 @@ describe('calculateLabel(metric, metrics)', () => {
expect(label).toEqual('Derivative of Outbound Traffic');
});
+
+ test('should throw an error if field not found', () => {
+ const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema;
+ const metrics = ([
+ { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
+ metric,
+ ] as unknown) as MetricsItemsSchema[];
+ const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }];
+
+ expect(() => calculateLabel(metric, metrics, fields)).toThrowError('Field "3" not found');
+ });
+
+ test('should not throw an error if field not found (isThrowErrorOnFieldNotFound is false)', () => {
+ const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema;
+ const metrics = ([
+ { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
+ metric,
+ ] as unknown) as MetricsItemsSchema[];
+ const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }];
+
+ expect(calculateLabel(metric, metrics, fields, false)).toBe('Max of 3');
+ });
});
diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.ts b/src/plugins/vis_type_timeseries/common/calculate_label.ts
index 73b5d3f652644..bd1482e14f4f4 100644
--- a/src/plugins/vis_type_timeseries/common/calculate_label.ts
+++ b/src/plugins/vis_type_timeseries/common/calculate_label.ts
@@ -10,6 +10,7 @@ import { includes, startsWith } from 'lodash';
import { i18n } from '@kbn/i18n';
import { lookup } from './agg_lookup';
import { MetricsItemsSchema, SanitizedFieldType } from './types';
+import { extractFieldLabel } from './fields_utils';
const paths = [
'cumulative_sum',
@@ -26,14 +27,11 @@ const paths = [
'positive_only',
];
-export const extractFieldLabel = (fields: SanitizedFieldType[], name: string) => {
- return fields.find((f) => f.name === name)?.label ?? name;
-};
-
export const calculateLabel = (
metric: MetricsItemsSchema,
metrics: MetricsItemsSchema[] = [],
- fields: SanitizedFieldType[] = []
+ fields: SanitizedFieldType[] = [],
+ isThrowErrorOnFieldNotFound: boolean = true
): string => {
if (!metric) {
return i18n.translate('visTypeTimeseries.calculateLabel.unknownLabel', {
@@ -71,7 +69,7 @@ export const calculateLabel = (
if (metric.type === 'positive_rate') {
return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', {
defaultMessage: 'Counter Rate of {field}',
- values: { field: extractFieldLabel(fields, metric.field!) },
+ values: { field: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound) },
});
}
if (metric.type === 'static') {
@@ -115,7 +113,7 @@ export const calculateLabel = (
defaultMessage: '{lookupMetricType} of {metricField}',
values: {
lookupMetricType: lookup[metric.type],
- metricField: extractFieldLabel(fields, metric.field!),
+ metricField: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound),
},
});
};
diff --git a/src/plugins/vis_type_timeseries/common/constants.ts b/src/plugins/vis_type_timeseries/common/constants.ts
index 66617c8518985..1debfaf951e99 100644
--- a/src/plugins/vis_type_timeseries/common/constants.ts
+++ b/src/plugins/vis_type_timeseries/common/constants.ts
@@ -13,3 +13,4 @@ export const ROUTES = {
VIS_DATA: '/api/metrics/vis/data',
FIELDS: '/api/metrics/fields',
};
+export const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes';
diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts
index d1036aab2dc3e..9550697e22851 100644
--- a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts
+++ b/src/plugins/vis_type_timeseries/common/fields_utils.test.ts
@@ -7,7 +7,7 @@
*/
import { toSanitizedFieldType } from './fields_utils';
-import type { FieldSpec, RuntimeField } from '../../data/common';
+import type { FieldSpec } from '../../data/common';
describe('fields_utils', () => {
describe('toSanitizedFieldType', () => {
@@ -34,17 +34,6 @@ describe('fields_utils', () => {
`);
});
- test('should filter runtime fields', async () => {
- const fields: FieldSpec[] = [
- {
- ...mockedField,
- runtimeField: {} as RuntimeField,
- },
- ];
-
- expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`);
- });
-
test('should filter non-aggregatable fields', async () => {
const fields: FieldSpec[] = [
{
diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_type_timeseries/common/fields_utils.ts
index 04499d5320ab8..6a83dd323b3fd 100644
--- a/src/plugins/vis_type_timeseries/common/fields_utils.ts
+++ b/src/plugins/vis_type_timeseries/common/fields_utils.ts
@@ -6,17 +6,60 @@
* Side Public License, v 1.
*/
+import { i18n } from '@kbn/i18n';
import { FieldSpec } from '../../data/common';
import { isNestedField } from '../../data/common';
-import { SanitizedFieldType } from './types';
+import { FetchedIndexPattern, SanitizedFieldType } from './types';
-export const toSanitizedFieldType = (fields: FieldSpec[]) => {
- return fields
- .filter(
- (field) =>
- // Make sure to only include mapped fields, e.g. no index pattern runtime fields
- !field.runtimeField && field.aggregatable && !isNestedField(field)
- )
+export class FieldNotFoundError extends Error {
+ constructor(name: string) {
+ super(
+ i18n.translate('visTypeTimeseries.fields.fieldNotFound', {
+ defaultMessage: `Field "{field}" not found`,
+ values: { field: name },
+ })
+ );
+ }
+
+ public get name() {
+ return this.constructor.name;
+ }
+
+ public get body() {
+ return this.message;
+ }
+}
+
+export const extractFieldLabel = (
+ fields: SanitizedFieldType[],
+ name: string,
+ isThrowErrorOnFieldNotFound: boolean = true
+) => {
+ if (fields.length && name) {
+ const field = fields.find((f) => f.name === name);
+
+ if (field) {
+ return field.label || field.name;
+ }
+ if (isThrowErrorOnFieldNotFound) {
+ throw new FieldNotFoundError(name);
+ }
+ }
+ return name;
+};
+
+export function validateField(name: string, index: FetchedIndexPattern) {
+ if (name && index.indexPattern) {
+ const field = index.indexPattern.fields.find((f) => f.name === name);
+ if (!field) {
+ throw new FieldNotFoundError(name);
+ }
+ }
+}
+
+export const toSanitizedFieldType = (fields: FieldSpec[]) =>
+ fields
+ .filter((field) => field.aggregatable && !isNestedField(field))
.map(
(field) =>
({
@@ -25,4 +68,3 @@ export const toSanitizedFieldType = (fields: FieldSpec[]) => {
type: field.type,
} as SanitizedFieldType)
);
-};
diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts
index 0428e6e80ae78..1111a9c525243 100644
--- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts
+++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts
@@ -81,7 +81,7 @@ describe('fetchIndexPattern', () => {
});
describe('text-based index', () => {
- test('should return the Kibana index if it exists', async () => {
+ test('should return the Kibana index if it exists (fetchKibabaIndexForStringIndexes is true)', async () => {
mockedIndices = [
{
id: 'indexId',
@@ -89,7 +89,9 @@ describe('fetchIndexPattern', () => {
},
] as IndexPattern[];
- const value = await fetchIndexPattern('indexTitle', indexPatternsService);
+ const value = await fetchIndexPattern('indexTitle', indexPatternsService, {
+ fetchKibabaIndexForStringIndexes: true,
+ });
expect(value).toMatchInlineSnapshot(`
Object {
@@ -102,8 +104,10 @@ describe('fetchIndexPattern', () => {
`);
});
- test('should return only indexPatternString if Kibana index does not exist', async () => {
- const value = await fetchIndexPattern('indexTitle', indexPatternsService);
+ test('should return only indexPatternString if Kibana index does not exist (fetchKibabaIndexForStringIndexes is true)', async () => {
+ const value = await fetchIndexPattern('indexTitle', indexPatternsService, {
+ fetchKibabaIndexForStringIndexes: true,
+ });
expect(value).toMatchInlineSnapshot(`
Object {
diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts
index af9f0750b2604..5dacad338e7a8 100644
--- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts
+++ b/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts
@@ -52,7 +52,12 @@ export const extractIndexPatternValues = (
export const fetchIndexPattern = async (
indexPatternValue: IndexPatternValue | undefined,
- indexPatternsService: Pick
+ indexPatternsService: Pick,
+ options: {
+ fetchKibabaIndexForStringIndexes: boolean;
+ } = {
+ fetchKibabaIndexForStringIndexes: false,
+ }
): Promise => {
let indexPattern: FetchedIndexPattern['indexPattern'];
let indexPatternString: string = '';
@@ -61,13 +66,16 @@ export const fetchIndexPattern = async (
indexPattern = await indexPatternsService.getDefault();
} else {
if (isStringTypeIndexPattern(indexPatternValue)) {
- indexPattern = (await indexPatternsService.find(indexPatternValue)).find(
- (index) => index.title === indexPatternValue
- );
-
+ if (options.fetchKibabaIndexForStringIndexes) {
+ indexPattern = (await indexPatternsService.find(indexPatternValue)).find(
+ (index) => index.title === indexPatternValue
+ );
+ }
if (!indexPattern) {
indexPatternString = indexPatternValue;
}
+
+ indexPatternString = indexPatternValue;
} else if (indexPatternValue.id) {
indexPattern = await indexPatternsService.get(indexPatternValue.id);
}
diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts
index 74e247b7af06d..240b3e68cf65d 100644
--- a/src/plugins/vis_type_timeseries/common/types.ts
+++ b/src/plugins/vis_type_timeseries/common/types.ts
@@ -46,6 +46,7 @@ interface TableData {
export type SeriesData = {
type: Exclude;
uiRestrictions: TimeseriesUIRestrictions;
+ error?: string;
} & {
[key: string]: PanelSeries;
};
@@ -56,7 +57,7 @@ interface PanelSeries {
};
id: string;
series: PanelData[];
- error?: unknown;
+ error?: string;
}
export interface PanelData {
@@ -66,6 +67,7 @@ export interface PanelData {
seriesId: string;
splitByLabel: string;
isSplitByTerms: boolean;
+ error?: string;
}
export const isVisTableData = (data: TimeseriesVisData): data is TableData =>
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
index 82989cc15d6c9..7d42eb3f40ac5 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
@@ -5,19 +5,26 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
-import React from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui';
-import { METRIC_TYPES } from '../../../../common/metric_types';
+import React, { ReactNode, useContext } from 'react';
+import {
+ EuiComboBox,
+ EuiComboBoxProps,
+ EuiComboBoxOptionOption,
+ EuiFormRow,
+ htmlIdGenerator,
+} from '@elastic/eui';
import { getIndexPatternKey } from '../../../../common/index_patterns_utils';
import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types';
import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
// @ts-ignore
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
+import { PanelModelContext } from '../../contexts/panel_model_context';
+import { USE_KIBANA_INDEXES_KEY } from '../../../../common/constants';
interface FieldSelectProps {
+ label: string | ReactNode;
type: string;
fields: Record;
indexPattern: IndexPatternValue;
@@ -45,6 +52,7 @@ const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOpt
};
export function FieldSelect({
+ label,
type,
fields,
indexPattern = '',
@@ -56,11 +64,10 @@ export function FieldSelect({
uiRestrictions,
'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect',
}: FieldSelectProps) {
- if (type === METRIC_TYPES.COUNT) {
- return null;
- }
+ const panelModel = useContext(PanelModelContext);
+ const htmlId = htmlIdGenerator();
- const selectedOptions: Array> = [];
+ let selectedOptions: Array> = [];
let newPlaceholder = placeholder;
const fieldsSelector = getIndexPatternKey(indexPattern);
@@ -112,19 +119,43 @@ export function FieldSelect({
}
});
- if (value && !selectedOptions.length) {
- onChange([]);
+ let isInvalid;
+
+ if (Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY])) {
+ isInvalid = Boolean(value && fields[fieldsSelector] && !selectedOptions.length);
+
+ if (value && !selectedOptions.length) {
+ selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }];
+ }
+ } else {
+ if (value && !selectedOptions.length) {
+ onChange([]);
+ }
}
return (
-
+
+
+
);
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
index 90353f9af8e35..c380b0e09e7d3 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js
@@ -153,24 +153,20 @@ export const FilterRatioAgg = (props) => {
{model.metric_agg !== 'count' ? (
-
}
- >
-
-
+ fields={fields}
+ type={model.metric_agg}
+ restrict={getSupportedFieldsByMetricType(model.metric_agg)}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ />
) : null}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
index 964017cf886ec..7ce432a3bf676 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
@@ -70,7 +70,7 @@ export function MetricSelect(props) {
const percentileOptions = siblings
.filter((row) => /^percentile/.test(row.type))
.reduce((acc, row) => {
- const label = calculateLabel(row, calculatedMetrics, fields);
+ const label = calculateLabel(row, calculatedMetrics, fields, false);
switch (row.type) {
case METRIC_TYPES.PERCENTILE_RANK:
@@ -100,7 +100,7 @@ export function MetricSelect(props) {
}, []);
const options = siblings.filter(filterRows(includeSiblings)).map((row) => {
- const label = calculateLabel(row, calculatedMetrics, fields);
+ const label = calculateLabel(row, calculatedMetrics, fields, false);
return { value: row.id, label };
});
const allOptions = [...options, ...additionalOptions, ...percentileOptions];
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
index 77b2e2f020307..45bb5387c5cd3 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js
@@ -78,24 +78,20 @@ export function PercentileAgg(props) {
/>
-
}
- >
-
-
+ fields={fields}
+ type={model.type}
+ restrict={RESTRICT_FIELDS}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ />
{
/>
-
}
- >
-
-
+ fields={fields}
+ type={model.type}
+ restrict={RESTRICT_FIELDS}
+ indexPattern={indexPattern}
+ value={model.field ?? ''}
+ onChange={handleSelectChange('field')}
+ />
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
index 4b1528ca27081..09d9f2f1a62f2 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js
@@ -99,27 +99,22 @@ export const PositiveRateAgg = (props) => {
/>
-
}
+ fields={props.fields}
+ type={model.type}
+ restrict={[KBN_FIELD_TYPES.NUMBER]}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ uiRestrictions={props.uiRestrictions}
fullWidth
- >
-
-
+ />
-
}
+ fields={fields}
+ type={model.type}
+ restrict={restrictFields}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ uiRestrictions={uiRestrictions}
fullWidth
- >
-
-
+ />
) : null}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js
index 749a97fa79f28..d4caa8a94652f 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js
@@ -107,24 +107,20 @@ const StandardDeviationAggUi = (props) => {
/>
-
}
- >
-
-
+ fields={fields}
+ type={model.type}
+ restrict={RESTRICT_FIELDS}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ />
{
/>
-
}
- >
-
-
+ fields={fields}
+ type={model.type}
+ restrict={aggWithOptionsRestrictFields}
+ indexPattern={indexPattern}
+ value={model.field}
+ onChange={handleSelectChange('field')}
+ />
@@ -223,23 +219,19 @@ const TopHitAggUi = (props) => {
-
}
- >
-
-
+ restrict={ORDER_DATE_RESTRICT_FIELDS}
+ value={model.order_by}
+ onChange={handleSelectChange('order_by')}
+ indexPattern={indexPattern}
+ fields={fields}
+ />
-
}
+ restrict={RESTRICT_FIELDS}
+ value={model.time_field}
+ onChange={this.handleChange(model, 'time_field')}
+ indexPattern={model.index_pattern}
+ fields={this.props.fields}
fullWidth
- >
-
-
+ />
diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
index 5a991238d10f8..e7a34c6e6596d 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
@@ -77,8 +77,8 @@ export const IndexPattern = ({
const intervalName = `${prefix}interval`;
const maxBarsName = `${prefix}max_bars`;
const dropBucketName = `${prefix}drop_last_bucket`;
- const defaultIndex = useContext(DefaultIndexPatternContext);
const updateControlValidity = useContext(FormValidationContext);
+ const defaultIndex = useContext(DefaultIndexPatternContext);
const uiRestrictions = get(useContext(VisDataContext), 'uiRestrictions');
const maxBarsUiSettings = config.get(UI_SETTINGS.HISTOGRAM_MAX_BARS);
@@ -192,22 +192,18 @@ export const IndexPattern = ({
/>
-
-
-
+ restrict={RESTRICT_FIELDS}
+ value={model[timeFieldName]}
+ disabled={disabled}
+ onChange={handleSelectChange(timeFieldName)}
+ indexPattern={model[indexPatternName]}
+ fields={fields}
+ placeholder={!model[indexPatternName] ? defaultIndex?.timeFieldName : undefined}
+ />
-
}
- >
-
-
+ fields={this.props.fields}
+ value={model.pivot_id}
+ indexPattern={model.index_pattern}
+ onChange={this.handlePivotChange}
+ uiRestrictions={this.context.uiRestrictions}
+ type={BUCKET_TYPES.TERMS}
+ />
diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap
index 09cd6d550fd96..562c463f6c83c 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap
+++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap
@@ -26,13 +26,25 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
-
}
- labelType="label"
- >
-
-
+ onChange={[Function]}
+ type="terms"
+ value="OriginCityName"
+ />
diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
index ab5342e925bd7..7db6a75e2392c 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
@@ -110,8 +110,7 @@ export const SplitByTermsUI = ({
-
}
- >
-
-
+ data-test-subj="groupByField"
+ indexPattern={indexPattern}
+ onChange={handleSelectChange('terms_field')}
+ value={model.terms_field}
+ fields={fields}
+ uiRestrictions={uiRestrictions}
+ type={'terms'}
+ />
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js
index 0ba8d3e855365..1940ac8b2e9b9 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js
@@ -186,20 +186,16 @@ export class TableSeriesConfig extends Component {
-
}
- >
-
-
+ fields={this.props.fields}
+ indexPattern={this.props.panel.index_pattern}
+ value={model.aggregate_by}
+ onChange={handleSelectChange('aggregate_by')}
+ fullWidth
+ />
{
});
describe('text-based index', () => {
- test('should return the Kibana index if it exists', async () => {
- mockedIndices = [
- {
- id: 'indexId',
- title: 'indexTitle',
- },
- ] as IndexPattern[];
-
- const value = await cachedIndexPatternFetcher('indexTitle');
-
- expect(value).toMatchInlineSnapshot(`
- Object {
- "indexPattern": Object {
- "id": "indexId",
- "title": "indexTitle",
- },
- "indexPatternString": "indexTitle",
- }
- `);
- });
-
- test('should return only indexPatternString if Kibana index does not exist', async () => {
+ test('should return only indexPatternString', async () => {
const value = await cachedIndexPatternFetcher('indexTitle');
expect(value).toMatchInlineSnapshot(`
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts
index 9003eb7fc2ced..4b13e62430c47 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts
@@ -6,10 +6,13 @@
* Side Public License, v 1.
*/
+import { getIndexPatternKey } from '../../../../common/index_patterns_utils';
+
import type { VisTypeTimeseriesVisDataRequest } from '../../../types';
import type { AbstractSearchStrategy, DefaultSearchCapabilities } from '../index';
import type { IndexPatternsService } from '../../../../../data/common';
import type { CachedIndexPatternFetcher } from './cached_index_pattern_fetcher';
+import type { IndexPatternValue } from '../../../../common/types';
export interface FieldsFetcherServices {
indexPatternsService: IndexPatternsService;
@@ -29,11 +32,13 @@ export const createFieldsFetcher = (
) => {
const fieldsCacheMap = new Map();
- return async (index: string) => {
- if (fieldsCacheMap.has(index)) {
- return fieldsCacheMap.get(index);
+ return async (indexPatternValue: IndexPatternValue) => {
+ const key = getIndexPatternKey(indexPatternValue);
+
+ if (fieldsCacheMap.has(key)) {
+ return fieldsCacheMap.get(key);
}
- const fetchedIndex = await cachedIndexPatternFetcher(index);
+ const fetchedIndex = await cachedIndexPatternFetcher(indexPatternValue);
const fields = await searchStrategy.getFieldsForWildcard(
fetchedIndex,
@@ -41,7 +46,7 @@ export const createFieldsFetcher = (
capabilities
);
- fieldsCacheMap.set(index, fields);
+ fieldsCacheMap.set(key, fields);
return fields;
};
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts
index 5a84598bb5ed2..1350e56b68f59 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts
@@ -7,8 +7,8 @@
*/
import { IUiSettingsClient } from 'kibana/server';
-import { EsQueryConfig, IndexPattern } from 'src/plugins/data/server';
-import { AnnotationItemsSchema, PanelSchema } from '../../../../common/types';
+import { EsQueryConfig } from 'src/plugins/data/server';
+import { AnnotationItemsSchema, FetchedIndexPattern, PanelSchema } from '../../../../common/types';
import { VisTypeTimeseriesVisDataRequest } from '../../../types';
import { DefaultSearchCapabilities } from '../../search_strategies';
import { buildProcessorFunction } from '../build_processor_function';
@@ -17,16 +17,6 @@ import { processors } from '../request_processors/annotations';
/**
* Builds annotation request body
- *
- * @param {...args}: [
- * req: {Object} - a request object,
- * panel: {Object} - a panel object,
- * annotation: {Object} - an annotation object,
- * esQueryConfig: {Object} - es query config object,
- * indexPatternObject: {Object} - an index pattern object,
- * capabilities: {Object} - a search capabilities object
- * ]
- * @returns {Object} doc - processed body
*/
export async function buildAnnotationRequest(
...args: [
@@ -34,7 +24,7 @@ export async function buildAnnotationRequest(
PanelSchema,
AnnotationItemsSchema,
EsQueryConfig,
- IndexPattern | null | undefined,
+ FetchedIndexPattern,
DefaultSearchCapabilities,
IUiSettingsClient
]
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts
index 32086fbf4f5b4..40f1b4f2cc051 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts
@@ -33,24 +33,23 @@ export async function getAnnotationRequestParams(
cachedIndexPatternFetcher,
}: AnnotationServices
) {
- const { indexPattern, indexPatternString } = await cachedIndexPatternFetcher(
- annotation.index_pattern
- );
+ const annotationIndex = await cachedIndexPatternFetcher(annotation.index_pattern);
const request = await buildAnnotationRequest(
req,
panel,
annotation,
esQueryConfig,
- indexPattern,
+ annotationIndex,
capabilities,
uiSettings
);
return {
- index: indexPatternString,
+ index: annotationIndex.indexPatternString,
body: {
...request,
+ runtime_mappings: annotationIndex.indexPattern?.getComputedFields().runtimeFields ?? {},
timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined,
},
};
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts
similarity index 68%
rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js
rename to src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts
index ceb867e4e6d1e..7c0a0f5deb601 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts
@@ -7,25 +7,30 @@
*/
import { getIntervalAndTimefield } from './get_interval_and_timefield';
+import { FetchedIndexPattern, PanelSchema, SeriesItemsSchema } from '../../../common/types';
describe('getIntervalAndTimefield(panel, series)', () => {
+ const index: FetchedIndexPattern = {} as FetchedIndexPattern;
+
test('returns the panel interval and timefield', () => {
- const panel = { time_field: '@timestamp', interval: 'auto' };
- const series = {};
- expect(getIntervalAndTimefield(panel, series)).toEqual({
+ const panel = { time_field: '@timestamp', interval: 'auto' } as PanelSchema;
+ const series = {} as SeriesItemsSchema;
+
+ expect(getIntervalAndTimefield(panel, series, index)).toEqual({
timeField: '@timestamp',
interval: 'auto',
});
});
test('returns the series interval and timefield', () => {
- const panel = { time_field: '@timestamp', interval: 'auto' };
- const series = {
+ const panel = { time_field: '@timestamp', interval: 'auto' } as PanelSchema;
+ const series = ({
override_index_pattern: true,
series_interval: '1m',
series_time_field: 'time',
- };
- expect(getIntervalAndTimefield(panel, series)).toEqual({
+ } as unknown) as SeriesItemsSchema;
+
+ expect(getIntervalAndTimefield(panel, series, index)).toEqual({
timeField: 'time',
interval: '1m',
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts
similarity index 57%
rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js
rename to src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts
index ebab984ff25aa..e3d0cec1a6939 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts
@@ -7,28 +7,31 @@
*/
import { AUTO_INTERVAL } from '../../../common/constants';
+import { FetchedIndexPattern, PanelSchema, SeriesItemsSchema } from '../../../common/types';
+import { validateField } from '../../../common/fields_utils';
-const DEFAULT_TIME_FIELD = '@timestamp';
-
-export function getIntervalAndTimefield(panel, series = {}, indexPattern) {
- const getDefaultTimeField = () => indexPattern?.timeFieldName ?? DEFAULT_TIME_FIELD;
-
+export function getIntervalAndTimefield(
+ panel: PanelSchema,
+ series: SeriesItemsSchema,
+ index: FetchedIndexPattern
+) {
const timeField =
- (series.override_index_pattern && series.series_time_field) ||
- panel.time_field ||
- getDefaultTimeField();
+ (series.override_index_pattern ? series.series_time_field : panel.time_field) ||
+ index.indexPattern?.timeFieldName;
+
+ validateField(timeField!, index);
let interval = panel.interval;
let maxBars = panel.max_bars;
if (series.override_index_pattern) {
- interval = series.series_interval;
+ interval = series.series_interval || AUTO_INTERVAL;
maxBars = series.series_max_bars;
}
return {
+ maxBars,
timeField,
interval: interval || AUTO_INTERVAL,
- maxBars,
};
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts
index 0cc1188086b7b..b50fdb6b8226d 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts
@@ -18,7 +18,7 @@ import { handleErrorResponse } from './handle_error_response';
import { processBucket } from './table/process_bucket';
import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher';
-import { extractFieldLabel } from '../../../common/calculate_label';
+import { extractFieldLabel } from '../../../common/fields_utils';
import type {
VisTypeTimeseriesRequestHandlerContext,
VisTypeTimeseriesRequestServices,
@@ -58,8 +58,8 @@ export async function getTableData(
});
const calculatePivotLabel = async () => {
- if (panel.pivot_id && panelIndex.indexPattern?.title) {
- const fields = await extractFields(panelIndex.indexPattern.title);
+ if (panel.pivot_id && panelIndex.indexPattern?.id) {
+ const fields = await extractFields({ id: panelIndex.indexPattern.id });
return extractFieldLabel(fields, panel.pivot_id);
}
@@ -68,7 +68,6 @@ export async function getTableData(
const meta = {
type: panel.type,
- pivot_label: panel.pivot_label || (await calculatePivotLabel()),
uiRestrictions: capabilities.uiRestrictions,
};
@@ -77,14 +76,17 @@ export async function getTableData(
req,
panel,
services.esQueryConfig,
- panelIndex.indexPattern,
+ panelIndex,
capabilities,
services.uiSettings
);
const [resp] = await searchStrategy.search(requestContext, req, [
{
- body,
+ body: {
+ ...body,
+ runtime_mappings: panelIndex.indexPattern?.getComputedFields().runtimeFields ?? {},
+ },
index: panelIndex.indexPatternString,
},
]);
@@ -101,6 +103,7 @@ export async function getTableData(
return {
...meta,
+ pivot_label: panel.pivot_label || (await calculatePivotLabel()),
series,
};
} catch (err) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
index 268c26115233e..27e7c5c908b9a 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js
@@ -23,9 +23,8 @@ export async function getSplits(resp, panel, series, meta, extractFields) {
const color = new Color(series.color);
const metric = getLastMetric(series);
const buckets = _.get(resp, `aggregations.${series.id}.buckets`);
-
- const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
- const splitByLabel = calculateLabel(metric, series.metrics, fieldsForMetaIndex);
+ const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : [];
+ const splitByLabel = calculateLabel(metric, series.metrics, fieldsForSeries);
if (buckets) {
if (Array.isArray(buckets)) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js
index 22a475a9997a7..f3ee416be81a8 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js
@@ -10,6 +10,7 @@ import { overwrite } from '../../helpers';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { getTimerange } from '../../helpers/get_timerange';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
+import { validateField } from '../../../../../common/fields_utils';
const { dateHistogramInterval } = search.aggs;
@@ -18,13 +19,16 @@ export function dateHistogram(
panel,
annotation,
esQueryConfig,
- indexPattern,
+ annotationIndex,
capabilities,
uiSettings
) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const timeField = annotation.time_field;
+
+ validateField(timeField, annotationIndex);
+
const { bucketSize, intervalString } = getBucketSize(
req,
'auto',
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js
index e7270371a3fdc..46a3c369e548d 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js
@@ -9,26 +9,30 @@
import { getBucketSize } from '../../helpers/get_bucket_size';
import { getTimerange } from '../../helpers/get_timerange';
import { esQuery, UI_SETTINGS } from '../../../../../../data/server';
+import { validateField } from '../../../../../common/fields_utils';
export function query(
req,
panel,
annotation,
esQueryConfig,
- indexPattern,
+ annotationIndex,
capabilities,
uiSettings
) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const timeField = annotation.time_field;
+ const timeField = (annotation.time_field || annotationIndex.indexPattern?.timeField) ?? '';
+
+ validateField(timeField, annotationIndex);
+
const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings);
const { from, to } = getTimerange(req);
doc.size = 0;
const queries = !annotation.ignore_global_filters ? req.body.query : [];
const filters = !annotation.ignore_global_filters ? req.body.filters : [];
- doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig);
+ doc.query = esQuery.buildEsQuery(annotationIndex.indexPattern, queries, filters, esQueryConfig);
const timerange = {
range: {
[timeField]: {
@@ -42,13 +46,18 @@ export function query(
if (annotation.query_string) {
doc.query.bool.must.push(
- esQuery.buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig)
+ esQuery.buildEsQuery(
+ annotationIndex.indexPattern,
+ [annotation.query_string],
+ [],
+ esQueryConfig
+ )
);
}
if (!annotation.ignore_panel_filters && panel.filter) {
doc.query.bool.must.push(
- esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(annotationIndex.indexPattern, [panel.filter], [], esQueryConfig)
);
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js
index 2e759cb6b8b74..1b4434c4867c8 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js
@@ -7,12 +7,15 @@
*/
import { overwrite } from '../../helpers';
+import { validateField } from '../../../../../common/fields_utils';
-export function topHits(req, panel, annotation) {
+export function topHits(req, panel, annotation, esQueryConfig, annotationIndex) {
return (next) => (doc) => {
const fields = (annotation.fields && annotation.fields.split(/[,\s]+/)) || [];
const timeField = annotation.time_field;
+ validateField(timeField, annotationIndex);
+
overwrite(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, {
sort: [
{
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js
index a9b4f99fdb693..41ed472c31936 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js
@@ -12,6 +12,7 @@ import { offsetTime } from '../../offset_time';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
+
const { dateHistogramInterval } = search.aggs;
export function dateHistogram(
@@ -19,7 +20,7 @@ export function dateHistogram(
panel,
series,
esQueryConfig,
- indexPattern,
+ seriesIndex,
capabilities,
uiSettings
) {
@@ -27,7 +28,7 @@ export function dateHistogram(
const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS);
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { timeField, interval, maxBars } = getIntervalAndTimefield(panel, series, indexPattern);
+ const { timeField, interval, maxBars } = getIntervalAndTimefield(panel, series, seriesIndex);
const { bucketSize, intervalString } = getBucketSize(
req,
interval,
@@ -64,9 +65,9 @@ export function dateHistogram(
overwrite(doc, `aggs.${series.id}.meta`, {
timeField,
intervalString,
- index: indexPattern?.title,
bucketSize,
seriesId: series.id,
+ index: seriesIndex.indexPattern?.id,
});
return next(doc);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js
index 4639af9db83b8..d45943f6f21ac 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js
@@ -12,19 +12,19 @@ import { esQuery } from '../../../../../../data/server';
const filter = (metric) => metric.type === 'filter_ratio';
-export function ratios(req, panel, series, esQueryConfig, indexPattern) {
+export function ratios(req, panel, series, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
if (series.metrics.some(filter)) {
series.metrics.filter(filter).forEach((metric) => {
overwrite(
doc,
`aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`,
- esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, metric.numerator, [], esQueryConfig)
);
overwrite(
doc,
`aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`,
- esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, metric.denominator, [], esQueryConfig)
);
let numeratorPath = `${metric.id}-numerator>_count`;
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js
index 345488ec01d5e..a93827ba82cd6 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js
@@ -8,7 +8,7 @@
import { ratios } from './filter_ratios';
-describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => {
+describe('ratios(req, panel, series, esQueryConfig, seriesIndex)', () => {
let panel;
let series;
let req;
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js
index 86b691f6496c9..29a11bf163e0b 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js
@@ -17,14 +17,14 @@ export function metricBuckets(
panel,
series,
esQueryConfig,
- indexPattern,
+ seriesIndex,
capabilities,
uiSettings
) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, series, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, series, seriesIndex);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
series.metrics
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js
index ce61374c0b124..208321a98737e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js
@@ -56,14 +56,14 @@ export function positiveRate(
panel,
series,
esQueryConfig,
- indexPattern,
+ seriesIndex,
capabilities,
uiSettings
) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, series, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, series, seriesIndex);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
if (series.metrics.some(filter)) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js
index d0e92c9157cb5..a5f4e17289e06 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js
@@ -10,16 +10,16 @@ import { offsetTime } from '../../offset_time';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { esQuery } from '../../../../../../data/server';
-export function query(req, panel, series, esQueryConfig, indexPattern) {
+export function query(req, panel, series, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
- const { timeField } = getIntervalAndTimefield(panel, series, indexPattern);
+ const { timeField } = getIntervalAndTimefield(panel, series, seriesIndex);
const { from, to } = offsetTime(req, series.offset_time);
doc.size = 0;
const ignoreGlobalFilter = panel.ignore_global_filter || series.ignore_global_filter;
const queries = !ignoreGlobalFilter ? req.body.query : [];
const filters = !ignoreGlobalFilter ? req.body.filters : [];
- doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig);
+ doc.query = esQuery.buildEsQuery(seriesIndex.indexPattern, queries, filters, esQueryConfig);
const timerange = {
range: {
@@ -34,13 +34,13 @@ export function query(req, panel, series, esQueryConfig, indexPattern) {
if (panel.filter) {
doc.query.bool.must.push(
- esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [panel.filter], [], esQueryConfig)
);
}
if (series.filter) {
doc.query.bool.must.push(
- esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [series.filter], [], esQueryConfig)
);
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js
index 2772aed822517..b3e88dbf1c6b9 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js
@@ -8,15 +8,17 @@
import { query } from './query';
-describe('query(req, panel, series)', () => {
+describe('query', () => {
let panel;
let series;
let req;
+ let seriesIndex;
const config = {
allowLeadingWildcards: true,
queryStringOptions: { analyze_wildcard: true },
};
+
beforeEach(() => {
req = {
body: {
@@ -32,17 +34,18 @@ describe('query(req, panel, series)', () => {
interval: '10s',
};
series = { id: 'test' };
+ seriesIndex = {};
});
test('calls next when finished', () => {
const next = jest.fn();
- query(req, panel, series, config)(next)({});
+ query(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns doc with query for timerange', () => {
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -69,7 +72,7 @@ describe('query(req, panel, series)', () => {
test('returns doc with query for timerange (offset by 1h)', () => {
series.offset_time = '1h';
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -108,7 +111,7 @@ describe('query(req, panel, series)', () => {
},
];
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -147,7 +150,7 @@ describe('query(req, panel, series)', () => {
test('returns doc with series filter', () => {
series.filter = { query: 'host:web-server', language: 'lucene' };
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -201,7 +204,7 @@ describe('query(req, panel, series)', () => {
];
panel.filter = { query: 'host:web-server', language: 'lucene' };
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -269,7 +272,7 @@ describe('query(req, panel, series)', () => {
panel.filter = { query: 'host:web-server', language: 'lucene' };
panel.ignore_global_filter = true;
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
@@ -325,7 +328,7 @@ describe('query(req, panel, series)', () => {
panel.filter = { query: 'host:web-server', language: 'lucene' };
series.ignore_global_filter = true;
const next = (doc) => doc;
- const doc = query(req, panel, series, config)(next)({});
+ const doc = query(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
size: 0,
query: {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js
index 401344d48f865..dbeb3b1393bd5 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js
@@ -17,13 +17,13 @@ export function siblingBuckets(
panel,
series,
esQueryConfig,
- indexPattern,
+ seriesIndex,
capabilities,
uiSettings
) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, series, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, series, seriesIndex);
const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
series.metrics
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js
index 25d62d4f7fe07..01e1b9f8d1dce 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js
@@ -9,7 +9,7 @@
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
-export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) {
+export function splitByFilter(req, panel, series, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
if (series.split_mode !== 'filter') {
return next(doc);
@@ -18,7 +18,7 @@ export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) {
overwrite(
doc,
`aggs.${series.id}.filter`,
- esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [series.filter], [], esQueryConfig)
);
return next(doc);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js
index ad6e84dbc7842..9722833837167 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js
@@ -12,8 +12,12 @@ describe('splitByFilter(req, panel, series)', () => {
let panel;
let series;
let req;
+ let config;
+ let seriesIndex;
+
beforeEach(() => {
panel = {};
+ config = {};
series = {
id: 'test',
split_mode: 'filter',
@@ -27,17 +31,18 @@ describe('splitByFilter(req, panel, series)', () => {
},
},
};
+ seriesIndex = {};
});
test('calls next when finished', () => {
const next = jest.fn();
- splitByFilter(req, panel, series)(next)({});
+ splitByFilter(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns a valid filter with a query_string', () => {
const next = (doc) => doc;
- const doc = splitByFilter(req, panel, series)(next)({});
+ const doc = splitByFilter(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@@ -63,7 +68,7 @@ describe('splitByFilter(req, panel, series)', () => {
test('calls next and does not add a filter', () => {
series.split_mode = 'terms';
const next = jest.fn((doc) => doc);
- const doc = splitByFilter(req, panel, series)(next)({});
+ const doc = splitByFilter(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
expect(doc).toEqual({});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js
index 237ed16e5a8b6..77b9ccc5880fe 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js
@@ -9,11 +9,16 @@
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
-export function splitByFilters(req, panel, series, esQueryConfig, indexPattern) {
+export function splitByFilters(req, panel, series, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
if (series.split_mode === 'filters' && series.split_filters) {
series.split_filters.forEach((filter) => {
- const builtEsQuery = esQuery.buildEsQuery(indexPattern, [filter.filter], [], esQueryConfig);
+ const builtEsQuery = esQuery.buildEsQuery(
+ seriesIndex.indexPattern,
+ [filter.filter],
+ [],
+ esQueryConfig
+ );
overwrite(doc, `aggs.${series.id}.filters.filters.${filter.id}`, builtEsQuery);
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js
index fdcdfe45d2fd2..2a44bf2538a4b 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js
@@ -12,7 +12,11 @@ describe('splitByFilters(req, panel, series)', () => {
let panel;
let series;
let req;
+ let config;
+ let seriesIndex;
+
beforeEach(() => {
+ config = {};
panel = {
time_field: 'timestamp',
};
@@ -43,17 +47,18 @@ describe('splitByFilters(req, panel, series)', () => {
},
},
};
+ seriesIndex = {};
});
test('calls next when finished', () => {
const next = jest.fn();
- splitByFilters(req, panel, series)(next)({});
+ splitByFilters(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns a valid terms agg', () => {
const next = (doc) => doc;
- const doc = splitByFilters(req, panel, series)(next)({});
+ const doc = splitByFilters(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@@ -97,7 +102,7 @@ describe('splitByFilters(req, panel, series)', () => {
test('calls next and does not add a terms agg', () => {
series.split_mode = 'everything';
const next = jest.fn((doc) => doc);
- const doc = splitByFilters(req, panel, series)(next)({});
+ const doc = splitByFilters(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
expect(doc).toEqual({});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js
index 8f72bd2d12951..9c2bdbe03f886 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js
@@ -10,13 +10,16 @@ import { overwrite } from '../../helpers';
import { basicAggs } from '../../../../../common/basic_aggs';
import { getBucketsPath } from '../../helpers/get_buckets_path';
import { bucketTransform } from '../../helpers/bucket_transform';
+import { validateField } from '../../../../../common/fields_utils';
-export function splitByTerms(req, panel, series) {
+export function splitByTerms(req, panel, series, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
if (series.split_mode === 'terms' && series.terms_field) {
const termsField = series.terms_field;
const orderByTerms = series.terms_order_by;
+ validateField(termsField, seriesIndex);
+
const direction = series.terms_direction || 'desc';
const metric = series.metrics.find((item) => item.id === orderByTerms);
overwrite(doc, `aggs.${series.id}.terms.field`, termsField);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js
index 37d188c00eee3..984eb385ca4a6 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js
@@ -8,11 +8,18 @@
import { splitByTerms } from './split_by_terms';
-describe('splitByTerms(req, panel, series)', () => {
+describe('splitByTerms', () => {
let panel;
let series;
let req;
+ let config;
+ let seriesIndex;
+
beforeEach(() => {
+ config = {
+ allowLeadingWildcards: true,
+ queryStringOptions: { analyze_wildcard: true },
+ };
panel = {
time_field: 'timestamp',
};
@@ -31,17 +38,18 @@ describe('splitByTerms(req, panel, series)', () => {
},
},
};
+ seriesIndex = {};
});
test('calls next when finished', () => {
const next = jest.fn();
- splitByTerms(req, panel, series)(next)({});
+ splitByTerms(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns a valid terms agg', () => {
const next = (doc) => doc;
- const doc = splitByTerms(req, panel, series)(next)({});
+ const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@@ -61,7 +69,7 @@ describe('splitByTerms(req, panel, series)', () => {
const next = (doc) => doc;
series.terms_order_by = '_key';
series.terms_direction = 'asc';
- const doc = splitByTerms(req, panel, series)(next)({});
+ const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@@ -80,7 +88,7 @@ describe('splitByTerms(req, panel, series)', () => {
test('returns a valid terms agg with custom sort', () => {
series.terms_order_by = 'avgmetric';
const next = (doc) => doc;
- const doc = splitByTerms(req, panel, series)(next)({});
+ const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@@ -106,7 +114,7 @@ describe('splitByTerms(req, panel, series)', () => {
test('calls next and does not add a terms agg', () => {
series.split_mode = 'everything';
const next = jest.fn((doc) => doc);
- const doc = splitByTerms(req, panel, series)(next)({});
+ const doc = splitByTerms(req, panel, series, config, seriesIndex)(next)({});
expect(next.mock.calls.length).toEqual(1);
expect(doc).toEqual({});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js
index aff1bd5041be5..4840e625383ca 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js
@@ -13,15 +13,17 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { getTimerange } from '../../helpers/get_timerange';
import { calculateAggRoot } from './calculate_agg_root';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
+
const { dateHistogramInterval } = search.aggs;
-export function dateHistogram(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) {
+export function dateHistogram(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPattern);
+ const { timeField, interval } = getIntervalAndTimefield(panel, {}, seriesIndex);
+
const meta = {
timeField,
- index: indexPattern?.title,
+ index: seriesIndex.indexPattern?.id,
};
const getDateHistogramForLastBucketMode = () => {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js
index abb5971908771..e15330334639f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js
@@ -13,7 +13,7 @@ import { calculateAggRoot } from './calculate_agg_root';
const filter = (metric) => metric.type === 'filter_ratio';
-export function ratios(req, panel, esQueryConfig, indexPattern) {
+export function ratios(req, panel, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
panel.series.forEach((column) => {
const aggRoot = calculateAggRoot(doc, column);
@@ -22,12 +22,12 @@ export function ratios(req, panel, esQueryConfig, indexPattern) {
overwrite(
doc,
`${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`,
- esQuery.buildEsQuery(indexPattern, metric.numerator, [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, metric.numerator, [], esQueryConfig)
);
overwrite(
doc,
`${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`,
- esQuery.buildEsQuery(indexPattern, metric.denominator, [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, metric.denominator, [], esQueryConfig)
);
let numeratorPath = `${metric.id}-numerator>_count`;
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js
index 5ce508bd9b279..421f9d2d75f0c 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js
@@ -13,10 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { calculateAggRoot } from './calculate_agg_root';
import { UI_SETTINGS } from '../../../../../../data/common';
-export function metricBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) {
+export function metricBuckets(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, {}, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
panel.series.forEach((column) => {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js
index 176721e7b563a..3390362b56115 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js
@@ -12,10 +12,10 @@ import { calculateAggRoot } from './calculate_agg_root';
import { createPositiveRate, filter } from '../series/positive_rate';
import { UI_SETTINGS } from '../../../../../../data/common';
-export function positiveRate(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) {
+export function positiveRate(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, {}, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
panel.series.forEach((column) => {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js
index 76df07b76e80e..66783e0cdfaef 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.js
@@ -10,16 +10,16 @@ import { getTimerange } from '../../helpers/get_timerange';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { esQuery } from '../../../../../../data/server';
-export function query(req, panel, esQueryConfig, indexPattern) {
+export function query(req, panel, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
- const { timeField } = getIntervalAndTimefield(panel, {}, indexPattern);
+ const { timeField } = getIntervalAndTimefield(panel, {}, seriesIndex);
const { from, to } = getTimerange(req);
doc.size = 0;
const queries = !panel.ignore_global_filter ? req.body.query : [];
const filters = !panel.ignore_global_filter ? req.body.filters : [];
- doc.query = esQuery.buildEsQuery(indexPattern, queries, filters, esQueryConfig);
+ doc.query = esQuery.buildEsQuery(seriesIndex.indexPattern, queries, filters, esQueryConfig);
const timerange = {
range: {
@@ -33,7 +33,7 @@ export function query(req, panel, esQueryConfig, indexPattern) {
doc.query.bool.must.push(timerange);
if (panel.filter) {
doc.query.bool.must.push(
- esQuery.buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [panel.filter], [], esQueryConfig)
);
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js
index 5539f16df41e0..9b4b0f244fc2c 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js
@@ -13,10 +13,10 @@ import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { calculateAggRoot } from './calculate_agg_root';
import { UI_SETTINGS } from '../../../../../../data/common';
-export function siblingBuckets(req, panel, esQueryConfig, indexPattern, capabilities, uiSettings) {
+export function siblingBuckets(req, panel, esQueryConfig, seriesIndex, capabilities, uiSettings) {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
- const { interval } = getIntervalAndTimefield(panel, {}, indexPattern);
+ const { interval } = getIntervalAndTimefield(panel, {}, seriesIndex);
const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings);
panel.series.forEach((column) => {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js
index 595d49ebbd836..cda022294507f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js
@@ -9,7 +9,7 @@
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
-export function splitByEverything(req, panel, esQueryConfig, indexPattern) {
+export function splitByEverything(req, panel, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
panel.series
.filter((c) => !(c.aggregate_by && c.aggregate_function))
@@ -18,7 +18,7 @@ export function splitByEverything(req, panel, esQueryConfig, indexPattern) {
overwrite(
doc,
`aggs.pivot.aggs.${column.id}.filter`,
- esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [column.filter], [], esQueryConfig)
);
} else {
overwrite(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js
index b4e07455be0fb..b3afc334ac2dd 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js
@@ -9,7 +9,7 @@
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
-export function splitByTerms(req, panel, esQueryConfig, indexPattern) {
+export function splitByTerms(req, panel, esQueryConfig, seriesIndex) {
return (next) => (doc) => {
panel.series
.filter((c) => c.aggregate_by && c.aggregate_function)
@@ -21,7 +21,7 @@ export function splitByTerms(req, panel, esQueryConfig, indexPattern) {
overwrite(
doc,
`aggs.pivot.aggs.${column.id}.column_filter.filter`,
- esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)
+ esQuery.buildEsQuery(seriesIndex.indexPattern, [column.filter], [], esQueryConfig)
);
}
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js
index ba0271ba286a1..a803439c7581f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js
@@ -5,9 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import { last, first } from 'lodash';
import { SeriesAgg } from './_series_agg';
-import _ from 'lodash';
import { getDefaultDecoration } from '../../helpers/get_default_decoration';
import { calculateLabel } from '../../../../../common/calculate_label';
@@ -33,15 +32,14 @@ export function seriesAgg(resp, panel, series, meta, extractFields) {
return (fn && fn(acc)) || acc;
}, targetSeries);
- const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
+ const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : [];
results.push({
id: `${series.id}`,
label:
- series.label ||
- calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex),
+ series.label || calculateLabel(last(series.metrics), series.metrics, fieldsForSeries),
color: series.color,
- data: _.first(data),
+ data: first(data),
...decoration,
});
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js
index 9af05afd41182..ae4968e007b1d 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js
@@ -7,7 +7,7 @@
*/
import { SeriesAgg } from './_series_agg';
-import _ from 'lodash';
+import { last, first } from 'lodash';
import { calculateLabel } from '../../../../../common/calculate_label';
export function seriesAgg(resp, panel, series, meta, extractFields) {
@@ -25,15 +25,13 @@ export function seriesAgg(resp, panel, series, meta, extractFields) {
});
const fn = SeriesAgg[series.aggregate_function];
const data = fn(targetSeries);
-
- const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
+ const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : [];
results.push({
id: `${series.id}`,
label:
- series.label ||
- calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex),
- data: _.first(data),
+ series.label || calculateLabel(last(series.metrics), series.metrics, fieldsForSeries),
+ data: first(data),
});
}
return next(results);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts
index bab3abe13bcb0..bc046cbdcf8aa 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts
@@ -18,7 +18,7 @@ import { processors } from '../request_processors/series/index';
* panel: {Object} - a panel object,
* series: {Object} - an series object,
* esQueryConfig: {Object} - es query config object,
- * indexPatternObject: {Object} - an index pattern object,
+ * seriesIndex: {Object} - an index pattern object,
* capabilities: {Object} - a search capabilities object
* ]
* @returns {Object} doc - processed body
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts
index 1f2735da8fb06..827df30dacf6d 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts
@@ -39,7 +39,7 @@ export async function getSeriesRequestParams(
panel,
series,
esQueryConfig,
- seriesIndex.indexPattern,
+ seriesIndex,
capabilities,
uiSettings
);
@@ -48,6 +48,7 @@ export async function getSeriesRequestParams(
index: seriesIndex.indexPatternString,
body: {
...request,
+ runtime_mappings: seriesIndex.indexPattern?.getComputedFields().runtimeFields ?? {},
timeout: esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined,
},
};
diff --git a/src/plugins/visualizations/public/components/visualization_container.tsx b/src/plugins/visualizations/public/components/visualization_container.tsx
index 3081c39530d75..063715b6438eb 100644
--- a/src/plugins/visualizations/public/components/visualization_container.tsx
+++ b/src/plugins/visualizations/public/components/visualization_container.tsx
@@ -10,6 +10,7 @@ import React, { ReactNode, Suspense } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';
import { VisualizationNoResults } from './visualization_noresults';
+import { VisualizationError } from './visualization_error';
import { IInterpreterRenderHandlers } from '../../../expressions/common';
interface VisualizationContainerProps {
@@ -18,6 +19,7 @@ interface VisualizationContainerProps {
children: ReactNode;
handlers: IInterpreterRenderHandlers;
showNoResult?: boolean;
+ error?: string;
}
export const VisualizationContainer = ({
@@ -26,6 +28,7 @@ export const VisualizationContainer = ({
children,
handlers,
showNoResult = false,
+ error,
}: VisualizationContainerProps) => {
const classes = classNames('visualization', className);
@@ -38,7 +41,13 @@ export const VisualizationContainer = ({
return (
- {showNoResult ? handlers.done()} /> : children}
+ {error ? (
+ handlers.done()} error={error} />
+ ) : showNoResult ? (
+ handlers.done()} />
+ ) : (
+ children
+ )}
);
diff --git a/src/plugins/visualizations/public/components/visualization_error.tsx b/src/plugins/visualizations/public/components/visualization_error.tsx
new file mode 100644
index 0000000000000..81600a4e3601c
--- /dev/null
+++ b/src/plugins/visualizations/public/components/visualization_error.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 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 { EuiEmptyPrompt } from '@elastic/eui';
+import React from 'react';
+
+interface VisualizationNoResultsProps {
+ onInit?: () => void;
+ error: string;
+}
+
+export class VisualizationError extends React.Component {
+ public render() {
+ return (
+ {this.props.error}
}
+ />
+ );
+ }
+
+ public componentDidMount() {
+ this.afterRender();
+ }
+
+ public componentDidUpdate() {
+ this.afterRender();
+ }
+
+ private afterRender() {
+ if (this.props.onInit) {
+ this.props.onInit();
+ }
+ }
+}
diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js
index d0c7c86d6d5c3..891805acb3256 100644
--- a/x-pack/test/functional/apps/rollup_job/tsvb.js
+++ b/x-pack/test/functional/apps/rollup_job/tsvb.js
@@ -83,6 +83,7 @@ export default function ({ getService, getPageObjects }) {
);
await PageObjects.visualBuilder.clickPanelOptions('metric');
await PageObjects.visualBuilder.setIndexPatternValue(rollupTargetIndexName, false);
+ await PageObjects.visualBuilder.selectIndexPatternTimeField('@timestamp');
await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value');
await PageObjects.visualBuilder.setIntervalValue('1d');
await PageObjects.visualBuilder.setDropLastBucket(false);
From 2d0b32a40afc2e095b035d27cfc95c5e5f6c74b2 Mon Sep 17 00:00:00 2001
From: Maja Grubic
Date: Mon, 12 Apr 2021 15:25:50 +0100
Subject: [PATCH 10/79] [Discover] Integration of Runtime Fields editor - edit
operation (#95498)
* [Discover] Updating a functional test
* [Discover] Support for edit operation
* Fix unit tests
* Fix typescript
* Fixing failing functional test
* Fixing wrongly commented line
* Uncomment accidentally commented line
* Reintroducing accidnetally removed unit test
* Trigger data refetch onSave
* Remove refreshAppState variable
* Bundling observers together
* Clean state before refetch
* Update formatting in data grid
* [Discover] Updating a functional test
* Adding a functional test
* Fixing package.json
* Reset fieldCount after data fetch
* [Discover] Updating a functional test
* Don't allow editing of unmapped fields
* Fix issues with mobile display
* Allow editing if it's a runtime field
* [Discover] Updating a functional test
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/discover/kibana.json | 3 +-
.../public/application/angular/discover.js | 7 +++
.../application/angular/discover_legacy.html | 1 +
.../application/angular/discover_state.ts | 2 +-
.../angular/helpers/row_formatter.test.ts | 5 +-
.../angular/helpers/row_formatter.ts | 8 +++-
.../components/create_discover_directive.ts | 1 +
.../application/components/discover.tsx | 8 ++++
.../discover_grid/get_render_cell_value.tsx | 10 +++-
.../components/sidebar/discover_field.tsx | 39 +++++++++++++--
.../sidebar/discover_sidebar.test.tsx | 7 +++
.../components/sidebar/discover_sidebar.tsx | 41 ++++++++++++++++
.../discover_sidebar_responsive.test.tsx | 1 +
.../sidebar/discover_sidebar_responsive.tsx | 35 +++++++++++++-
.../public/application/components/types.ts | 2 +
src/plugins/discover/public/build_services.ts | 3 ++
src/plugins/discover/public/plugin.tsx | 2 +
src/plugins/discover/tsconfig.json | 3 +-
.../apps/discover/_data_grid_context.ts | 2 +-
.../apps/discover/_runtime_fields_editor.ts | 47 +++++++++++++++++++
test/functional/apps/discover/index.ts | 1 +
test/functional/page_objects/discover_page.ts | 8 ++++
test/functional/services/field_editor.ts | 6 +++
23 files changed, 227 insertions(+), 15 deletions(-)
create mode 100644 test/functional/apps/discover/_runtime_fields_editor.ts
diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json
index 7db03f726e6f5..6ea22001f5d80 100644
--- a/src/plugins/discover/kibana.json
+++ b/src/plugins/discover/kibana.json
@@ -12,7 +12,8 @@
"urlForwarding",
"navigation",
"uiActions",
- "savedObjects"
+ "savedObjects",
+ "indexPatternFieldEditor"
],
"optionalPlugins": ["home", "share", "usageCollection"],
"requiredBundles": ["kibanaUtils", "home", "kibanaReact"]
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 45382af098644..35a89eb45f35e 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -458,6 +458,13 @@ function discoverController($route, $scope) {
$scope.fetchStatus = fetchStatuses.COMPLETE;
}
+ $scope.refreshAppState = async () => {
+ $scope.hits = [];
+ $scope.rows = [];
+ $scope.fieldCounts = {};
+ await refetch$.next();
+ };
+
function getRequestResponder({ searchSessionId = null } = { searchSessionId: null }) {
inspectorAdapters.requests.reset();
const title = i18n.translate('discover.inspectorRequestDataTitle', {
diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html
index f14800f81d08e..fadaffde5c5c3 100644
--- a/src/plugins/discover/public/application/angular/discover_legacy.html
+++ b/src/plugins/discover/public/application/angular/discover_legacy.html
@@ -16,6 +16,7 @@
top-nav-menu="topNavMenu"
use-new-fields-api="useNewFieldsApi"
unmapped-fields-config="unmappedFieldsConfig"
+ refresh-app-state="refreshAppState"
>
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index e7d5ed469525f..9ebeff69d7542 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -177,7 +177,7 @@ export function getState({
},
uiSettings
);
- // todo filter source depending on fields fetchinbg flag (if no columns remain and source fetching is enabled, use default columns)
+ // todo filter source depending on fields fetching flag (if no columns remain and source fetching is enabled, use default columns)
let previousAppState: AppState;
const appStateContainer = createStateContainer(initialAppState);
diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts
index 050959dff98a4..4c6b9002ce867 100644
--- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts
+++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts
@@ -90,6 +90,7 @@ describe('Row formatter', () => {
},
{
'object.value': [5, 10],
+ getByName: jest.fn(),
},
indexPattern
).trim()
@@ -107,7 +108,7 @@ describe('Row formatter', () => {
});
const formatted = formatTopLevelObject(
{ fields: { 'a.zzz': [100], 'a.ccc': [50] } },
- { 'a.zzz': [100], 'a.ccc': [50] },
+ { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() },
indexPattern
).trim();
expect(formatted.indexOf('a.ccc:')).toBeLessThan(formatted.indexOf('a.zzz:'));
@@ -134,6 +135,7 @@ describe('Row formatter', () => {
{
'object.value': [5, 10],
'object.keys': ['a', 'b'],
+ getByName: jest.fn(),
},
indexPattern
).trim()
@@ -154,6 +156,7 @@ describe('Row formatter', () => {
},
{
'object.value': [5, 10],
+ getByName: jest.fn(),
},
indexPattern
).trim()
diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts
index a226cefb53960..02902b0634797 100644
--- a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts
+++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts
@@ -28,11 +28,13 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern)
const highlights = hit?.highlight ?? {};
// Keys are sorted in the hits object
const formatted = indexPattern.formatHit(hit);
+ const fields = indexPattern.fields;
const highlightPairs: Array<[string, unknown]> = [];
const sourcePairs: Array<[string, unknown]> = [];
Object.entries(formatted).forEach(([key, val]) => {
+ const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined;
const pairs = highlights[key] ? highlightPairs : sourcePairs;
- pairs.push([key, val]);
+ pairs.push([displayKey ? displayKey : key, val]);
});
return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] });
};
@@ -48,9 +50,11 @@ export const formatTopLevelObject = (
const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
sorted.forEach(([key, values]) => {
const field = indexPattern.getFieldByName(key);
+ const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined;
const formatter = field
? indexPattern.getFormatterForField(field)
: { convert: (v: string, ...rest: unknown[]) => String(v) };
+ if (!values.map) return;
const formatted = values
.map((val: unknown) =>
formatter.convert(val, 'html', {
@@ -61,7 +65,7 @@ export const formatTopLevelObject = (
)
.join(', ');
const pairs = highlights[key] ? highlightPairs : sourcePairs;
- pairs.push([key, formatted]);
+ pairs.push([displayKey ? displayKey : key, formatted]);
});
return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] });
};
diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts
index 5abf87fdfbc08..cc88ef03c5d03 100644
--- a/src/plugins/discover/public/application/components/create_discover_directive.ts
+++ b/src/plugins/discover/public/application/components/create_discover_directive.ts
@@ -28,5 +28,6 @@ export function createDiscoverDirective(reactDirective: any) {
['updateQuery', { watchDepth: 'reference' }],
['updateSavedQueryId', { watchDepth: 'reference' }],
['unmappedFieldsConfig', { watchDepth: 'value' }],
+ ['refreshAppState', { watchDepth: 'reference' }],
]);
}
diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx
index 9615a1c10ea8e..6b71bd892b520 100644
--- a/src/plugins/discover/public/application/components/discover.tsx
+++ b/src/plugins/discover/public/application/components/discover.tsx
@@ -68,6 +68,7 @@ export function Discover({
searchSource,
state,
unmappedFieldsConfig,
+ refreshAppState,
}: DiscoverProps) {
const [expandedDoc, setExpandedDoc] = useState(undefined);
const scrollableDesktop = useRef(null);
@@ -203,6 +204,12 @@ export function Discover({
[opts, state]
);
+ const onEditRuntimeField = () => {
+ if (refreshAppState) {
+ refreshAppState();
+ }
+ };
+
const columns = useMemo(() => {
if (!state.columns) {
return [];
@@ -245,6 +252,7 @@ export function Discover({
trackUiMetric={trackUiMetric}
unmappedFieldsConfig={unmappedFieldsConfig}
useNewFieldsApi={useNewFieldsApi}
+ onEditRuntimeField={onEditRuntimeField}
/>
diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx
index dce0a82934c25..03203a79d9dd0 100644
--- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx
+++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx
@@ -77,6 +77,9 @@ export const getRenderCellValueFn = (
const sourcePairs: Array<[string, string]> = [];
Object.entries(innerColumns).forEach(([key, values]) => {
const subField = indexPattern.getFieldByName(key);
+ const displayKey = indexPattern.fields.getByName
+ ? indexPattern.fields.getByName(key)?.displayName
+ : undefined;
const formatter = subField
? indexPattern.getFormatterForField(subField)
: { convert: (v: string, ...rest: unknown[]) => String(v) };
@@ -90,7 +93,7 @@ export const getRenderCellValueFn = (
)
.join(', ');
const pairs = highlights[key] ? highlightPairs : sourcePairs;
- pairs.push([key, formatted]);
+ pairs.push([displayKey ? displayKey : key, formatted]);
});
return (
@@ -130,7 +133,10 @@ export const getRenderCellValueFn = (
Object.entries(formatted).forEach(([key, val]) => {
const pairs = highlights[key] ? highlightPairs : sourcePairs;
- pairs.push([key, val as string]);
+ const displayKey = indexPattern.fields.getByName
+ ? indexPattern.fields.getByName(key)?.displayName
+ : undefined;
+ pairs.push([displayKey ? displayKey : key, val as string]);
});
return (
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx
index b0d71c774f445..a630ddda40f30 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx
@@ -16,6 +16,8 @@ import {
EuiToolTip,
EuiTitle,
EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { UiCounterMetricType } from '@kbn/analytics';
@@ -69,6 +71,8 @@ export interface DiscoverFieldProps {
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
multiFields?: Array<{ field: IndexPatternField; isSelected: boolean }>;
+
+ onEditField?: (fieldName: string) => void;
}
export function DiscoverField({
@@ -82,6 +86,7 @@ export function DiscoverField({
selected,
trackUiMetric,
multiFields,
+ onEditField,
}: DiscoverFieldProps) {
const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
defaultMessage: 'Add {field} to table',
@@ -250,7 +255,6 @@ export function DiscoverField({
};
const fieldInfoIcon = getFieldInfoIcon();
-
const shouldRenderMultiFields = !!multiFields;
const renderMultiFields = () => {
if (!multiFields) {
@@ -282,6 +286,35 @@ export function DiscoverField({
);
};
+ const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField);
+ const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected';
+ const canEditField = onEditField && (!isUnknownField || isRuntimeField);
+ const displayNameGrow = canEditField ? 9 : 10;
+ const popoverTitle = (
+
+
+ {field.displayName}
+ {canEditField && (
+
+ {
+ if (onEditField) {
+ togglePopover();
+ onEditField(field.name);
+ }
+ }}
+ iconType="pencil"
+ data-test-subj={`discoverFieldListPanelEdit-${field.name}`}
+ aria-label={i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', {
+ defaultMessage: 'Edit index pattern field',
+ })}
+ />
+
+ )}
+
+
+ );
+
return (
-
- {field.displayName}
-
+ {popoverTitle}
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx
index 947972ce1cfc5..0b3f55b5630cc 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx
@@ -48,6 +48,12 @@ const mockServices = ({
}
},
},
+ indexPatternFieldEditor: {
+ openEditor: jest.fn(),
+ userPermissions: {
+ editIndexPattern: jest.fn(),
+ },
+ },
} as unknown) as DiscoverServices;
jest.mock('../../../kibana_services', () => ({
@@ -102,6 +108,7 @@ function getCompProps(): DiscoverSidebarProps {
fieldFilter: getDefaultFieldFilter(),
setFieldFilter: jest.fn(),
setAppState: jest.fn(),
+ onEditRuntimeField: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
index 1be42e1cd6b17..a3bf2e150d088 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
@@ -49,6 +49,17 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
* Change current state of fieldFilter
*/
setFieldFilter: (next: FieldFilterState) => void;
+
+ /**
+ * Callback to close the flyout sidebar rendered in a flyout, close flyout
+ */
+ closeFlyout?: () => void;
+
+ /**
+ * Pass the reference to field editor component to the parent, so it can be properly unmounted
+ * @param ref reference to the field editor component
+ */
+ setFieldEditorRef?: (ref: () => void | undefined) => void;
}
export function DiscoverSidebar({
@@ -72,8 +83,14 @@ export function DiscoverSidebar({
useNewFieldsApi = false,
useFlyout = false,
unmappedFieldsConfig,
+ onEditRuntimeField,
+ setFieldEditorRef,
+ closeFlyout,
}: DiscoverSidebarProps) {
const [fields, setFields] = useState(null);
+ const { indexPatternFieldEditor } = services;
+ const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
+ const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi;
const [scrollContainer, setScrollContainer] = useState(null);
const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE);
const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE);
@@ -220,6 +237,27 @@ export function DiscoverSidebar({
return null;
}
+ const editField = (fieldName: string) => {
+ if (!canEditIndexPatternField) {
+ return;
+ }
+ const ref = indexPatternFieldEditor.openEditor({
+ ctx: {
+ indexPattern: selectedIndexPattern,
+ },
+ fieldName,
+ onSave: async () => {
+ onEditRuntimeField();
+ },
+ });
+ if (setFieldEditorRef) {
+ setFieldEditorRef(ref);
+ }
+ if (closeFlyout) {
+ closeFlyout();
+ }
+ };
+
if (useFlyout) {
return (
);
@@ -388,6 +427,7 @@ export function DiscoverSidebar({
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
+ onEditField={canEditIndexPatternField ? editField : undefined}
/>
);
@@ -414,6 +454,7 @@ export function DiscoverSidebar({
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
+ onEditField={canEditIndexPatternField ? editField : undefined}
/>
);
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx
index 79e8caabd4930..caec61cc501b9 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx
@@ -102,6 +102,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
setAppState: jest.fn(),
state: {},
trackUiMetric: jest.fn(),
+ onEditRuntimeField: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
index 0808ef47c0dc1..6a16399f0e2e1 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -121,6 +121,8 @@ export interface DiscoverSidebarResponsiveProps {
*/
showUnmappedFields: boolean;
};
+
+ onEditRuntimeField: () => void;
}
/**
@@ -132,15 +134,42 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
+ const closeFieldEditor = useRef<() => void | undefined>();
+
+ useEffect(() => {
+ const cleanup = () => {
+ if (closeFieldEditor?.current) {
+ closeFieldEditor?.current();
+ }
+ };
+ return () => {
+ // Make sure to close the editor when unmounting
+ cleanup();
+ };
+ }, []);
+
if (!props.selectedIndexPattern) {
return null;
}
+ const setFieldEditorRef = (ref: () => void | undefined) => {
+ closeFieldEditor.current = ref;
+ };
+
+ const closeFlyout = () => {
+ setIsFlyoutVisible(false);
+ };
+
return (
<>
{props.isClosed ? null : (
-
+
)}
@@ -215,6 +244,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
fieldFilter={fieldFilter}
setFieldFilter={setFieldFilter}
alwaysShowActionButtons={true}
+ setFieldEditorRef={setFieldEditorRef}
+ closeFlyout={closeFlyout}
/>
diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts
index 23a3cc9a9bc74..93620bc1d6bca 100644
--- a/src/plugins/discover/public/application/components/types.ts
+++ b/src/plugins/discover/public/application/components/types.ts
@@ -167,4 +167,6 @@ export interface DiscoverProps {
*/
showUnmappedFields: boolean;
};
+
+ refreshAppState?: () => void;
}
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index 252265692d203..cf95d5a85b9f2 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -34,6 +34,7 @@ import { getHistory } from './kibana_services';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { UrlForwardingStart } from '../../url_forwarding/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
+import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public';
export interface DiscoverServices {
addBasePath: (path: string) => string;
@@ -59,6 +60,7 @@ export interface DiscoverServices {
getEmbeddableInjector: any;
uiSettings: IUiSettingsClient;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
+ indexPatternFieldEditor: IndexPatternFieldEditorStart;
}
export async function buildServices(
@@ -100,5 +102,6 @@ export async function buildServices(
toastNotifications: core.notifications.toasts,
uiSettings: core.uiSettings,
trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'),
+ indexPatternFieldEditor: plugins.indexPatternFieldEditor,
};
}
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 0e0836e3d9573..692704c92356e 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -62,6 +62,7 @@ import {
import { SearchEmbeddableFactory } from './application/embeddable';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
+import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
@@ -133,6 +134,7 @@ export interface DiscoverStartPlugins {
inspector: InspectorPublicPluginStart;
savedObjects: SavedObjectsStart;
usageCollection?: UsageCollectionSetup;
+ indexPatternFieldEditor: IndexPatternFieldEditorStart;
}
const innerAngularName = 'app/discover';
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index ec98199c3423e..c0179ad3c8d20 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -23,6 +23,7 @@
{ "path": "../usage_collection/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../kibana_react/tsconfig.json" },
- { "path": "../kibana_legacy/tsconfig.json" }
+ { "path": "../kibana_legacy/tsconfig.json" },
+ { "path": "../index_pattern_field_editor/tsconfig.json"}
]
}
diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts
index 326fba9e6c087..bc259c71b47b4 100644
--- a/test/functional/apps/discover/_data_grid_context.ts
+++ b/test/functional/apps/discover/_data_grid_context.ts
@@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await alert?.accept();
expect(await browser.getCurrentUrl()).to.contain('#/context');
await PageObjects.header.waitUntilLoadingHasFinished();
- expect(await docTable.getRowsText()).to.have.length(6);
+ expect(await docTable.getBodyRows()).to.have.length(6);
});
});
}
diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts
new file mode 100644
index 0000000000000..729ad08db81aa
--- /dev/null
+++ b/test/functional/apps/discover/_runtime_fields_editor.ts
@@ -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 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 expect from '@kbn/expect';
+import { FtrProviderContext } from './ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const log = getService('log');
+ const kibanaServer = getService('kibanaServer');
+ const esArchiver = getService('esArchiver');
+ const fieldEditor = getService('fieldEditor');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ 'discover:searchFieldsFromSource': false,
+ };
+ describe('discover integration with runtime fields editor', function describeIndexTests() {
+ before(async function () {
+ await esArchiver.load('discover');
+ await esArchiver.loadIfNeeded('logstash_functional');
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ log.debug('discover');
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ });
+
+ after(async () => {
+ await kibanaServer.uiSettings.replace({ 'discover:searchFieldsFromSource': true });
+ });
+
+ it('allows adding custom label to existing fields', async function () {
+ await PageObjects.discover.clickFieldListItemAdd('bytes');
+ await PageObjects.discover.editField('bytes');
+ await fieldEditor.enableCustomLabel();
+ await fieldEditor.setCustomLabel('megabytes');
+ await fieldEditor.save();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await PageObjects.discover.getDocHeader()).to.have.string('megabytes');
+ expect((await PageObjects.discover.getAllFieldNames()).includes('megabytes')).to.be(true);
+ });
+ });
+}
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index e526cdaccbd4c..db76cd1c20c38 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -47,6 +47,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_grid_doc_navigation'));
loadTestFile(require.resolve('./_data_grid_doc_table'));
loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields'));
+ loadTestFile(require.resolve('./_runtime_fields_editor'));
loadTestFile(require.resolve('./_huge_fields'));
});
}
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 32288239f9848..b4042e7072d7f 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -255,6 +255,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
.map((field) => $(field).text());
}
+ public async editField(field: string) {
+ await retry.try(async () => {
+ await testSubjects.click(`field-${field}`);
+ await testSubjects.click(`discoverFieldListPanelEdit-${field}`);
+ await find.byClassName('indexPatternFieldEditor__form');
+ });
+ }
+
public async hasNoResults() {
return await testSubjects.exists('discoverNoResults');
}
diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts
index 7d6dad4f7858e..342e2afec28d3 100644
--- a/test/functional/services/field_editor.ts
+++ b/test/functional/services/field_editor.ts
@@ -16,6 +16,12 @@ export function FieldEditorProvider({ getService }: FtrProviderContext) {
public async setName(name: string) {
await testSubjects.setValue('nameField > input', name);
}
+ public async enableCustomLabel() {
+ await testSubjects.setEuiSwitch('customLabelRow > toggle', 'check');
+ }
+ public async setCustomLabel(name: string) {
+ await testSubjects.setValue('customLabelRow > input', name);
+ }
public async enableValue() {
await testSubjects.setEuiSwitch('valueRow > toggle', 'check');
}
From 2ab94f05e1e8846c77a41cfc36eaa722b207f80e Mon Sep 17 00:00:00 2001
From: Matthew Kime
Date: Mon, 12 Apr 2021 09:27:44 -0500
Subject: [PATCH 11/79] Index pattern management - fix refresh of index pattern
list after delete (#92619)
* refresh id and title list
* add functional test
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../index_pattern_management/public/components/utils.ts | 2 +-
.../apps/management/_create_index_pattern_wizard.js | 8 +++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts
index 5701a1e375204..68e78199798b4 100644
--- a/src/plugins/index_pattern_management/public/components/utils.ts
+++ b/src/plugins/index_pattern_management/public/components/utils.ts
@@ -14,7 +14,7 @@ export async function getIndexPatterns(
indexPatternManagementStart: IndexPatternManagementStart,
indexPatternsService: IndexPatternsContract
) {
- const existingIndexPatterns = await indexPatternsService.getIdsWithTitle();
+ const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true);
const indexPatternsListItems = await Promise.all(
existingIndexPatterns.map(async ({ id, title }) => {
const isDefault = defaultIndex === id;
diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js
index 8db11052d5ed0..306d251629396 100644
--- a/test/functional/apps/management/_create_index_pattern_wizard.js
+++ b/test/functional/apps/management/_create_index_pattern_wizard.js
@@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const es = getService('legacyEs');
- const PageObjects = getPageObjects(['settings', 'common']);
+ const PageObjects = getPageObjects(['settings', 'common', 'header']);
const security = getService('security');
describe('"Create Index Pattern" wizard', function () {
@@ -60,6 +60,12 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.createIndexPattern('alias1', false);
});
+ it('can delete an index pattern', async () => {
+ await PageObjects.settings.removeIndexPattern();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await testSubjects.exists('indexPatternTable');
+ });
+
after(async () => {
await es.transport.request({
path: '/_aliases',
From 1f9700ec65b0ee2574f2828bfd24f79def46abb9 Mon Sep 17 00:00:00 2001
From: Vadim Dalecky
Date: Mon, 12 Apr 2021 16:44:48 +0200
Subject: [PATCH 12/79] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20enable=20drilldown?=
=?UTF-8?q?=20actions=20in=20"edit"=20mode=20(#96023)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 🎸 enable drilldown actions in "edit" mode
* style: 💄 remove unused import
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/embeddable_enhanced/public/plugin.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
index 96224644a457f..4b27b31ad3e0e 100644
--- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
@@ -18,7 +18,6 @@ import {
defaultEmbeddableFactoryProvider,
EmbeddableContext,
PANEL_NOTIFICATION_TRIGGER,
- ViewMode,
} from '../../../../src/plugins/embeddable/public';
import { EnhancedEmbeddable } from './types';
import {
@@ -119,7 +118,6 @@ export class EmbeddableEnhancedPlugin
const dynamicActions = new DynamicActionManager({
isCompatible: async (context: unknown) => {
if (!this.isEmbeddableContext(context)) return false;
- if (context.embeddable.getInput().viewMode !== ViewMode.VIEW) return false;
return context.embeddable.runtimeId === embeddable.runtimeId;
},
storage,
From 60d8fab88d08cef5ded3f380bcf0b000b121eae2 Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Mon, 12 Apr 2021 16:49:49 +0200
Subject: [PATCH 13/79] Document more "xpack.data_enhanced.search.sessions.*"
settings (#96542)
---
.../search-sessions-settings.asciidoc | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc
index cf64d08e4806c..abd6a8f12b568 100644
--- a/docs/settings/search-sessions-settings.asciidoc
+++ b/docs/settings/search-sessions-settings.asciidoc
@@ -11,15 +11,33 @@ Configure the search session settings in your `kibana.yml` configuration file.
[cols="2*<"]
|===
a| `xpack.data_enhanced.`
-`search.sessions.enabled`
+`search.sessions.enabled` {ess-icon}
| Set to `true` (default) to enable search sessions.
a| `xpack.data_enhanced.`
-`search.sessions.trackingInterval`
-| The frequency for updating the state of a search session. The default is 10s.
+`search.sessions.trackingInterval` {ess-icon}
+| The frequency for updating the state of a search session. The default is `10s`.
a| `xpack.data_enhanced.`
-`search.sessions.defaultExpiration`
+`search.sessions.pageSize` {ess-icon}
+| How many search sessions {kib} processes at once while monitoring
+session progress. The default is `100`.
+
+a| `xpack.data_enhanced.`
+`search.sessions.notTouchedTimeout` {ess-icon}
+| How long {kib} stores search results from unsaved sessions,
+after the last search in the session completes. The default is `5m`.
+
+a| `xpack.data_enhanced.`
+`search.sessions.notTouchedInProgressTimeout` {ess-icon}
+| How long a search session can run after a user navigates away without saving a session. The default is `1m`.
+
+a| `xpack.data_enhanced.`
+`search.sessions.maxUpdateRetries` {ess-icon}
+| How many retries {kib} can perform while attempting to save a search session. The default is `3`.
+
+a| `xpack.data_enhanced.`
+`search.sessions.defaultExpiration` {ess-icon}
| How long search session results are stored before they are deleted.
-Extending a search session resets the expiration by the same value. The default is 7d.
+Extending a search session resets the expiration by the same value. The default is `7d`.
|===
From 9bbf1faf4e38673d153070c047860ac616ac8ec1 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 12 Apr 2021 16:56:24 +0200
Subject: [PATCH 14/79] [Lens] Rename table dimensions (#96602)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../lens/public/datatable_visualization/visualization.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index f8b56f4ff2f81..9bd482c73bff5 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -183,7 +183,7 @@ export const datatableVisualization: Visualization
{
groupId: 'rows',
groupLabel: i18n.translate('xpack.lens.datatable.breakdownRows', {
- defaultMessage: 'Split rows',
+ defaultMessage: 'Rows',
}),
groupTooltip: i18n.translate('xpack.lens.datatable.breakdownRows.description', {
defaultMessage:
@@ -210,7 +210,7 @@ export const datatableVisualization: Visualization
{
groupId: 'columns',
groupLabel: i18n.translate('xpack.lens.datatable.breakdownColumns', {
- defaultMessage: 'Split columns',
+ defaultMessage: 'Columns',
}),
groupTooltip: i18n.translate('xpack.lens.datatable.breakdownColumns.description', {
defaultMessage:
From 3cf599502269b87defac42e8f6bc36a75bac0c03 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 12 Apr 2021 16:56:44 +0200
Subject: [PATCH 15/79] [Lens] Fix transferable logic to handle newer
operations on datasource change (#96617)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../operations/layer_helpers.test.ts | 32 +++++++++++++++++++
.../operations/layer_helpers.ts | 14 ++++++--
2 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 62cce21ead636..34e2eb2c90122 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -2089,6 +2089,38 @@ describe('state_helpers', () => {
});
});
+ it('should remove operations indirectly referencing unavailable fields', () => {
+ const layer: IndexPatternLayer = {
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: '',
+ dataType: 'number',
+ operationType: 'moving_average',
+ isBucketed: false,
+ scale: 'ratio',
+ references: ['col2'],
+ timeScale: undefined,
+ filter: undefined,
+ params: {
+ window: 7,
+ },
+ },
+ col2: {
+ dataType: 'number',
+ isBucketed: false,
+ label: '',
+ operationType: 'average',
+ sourceField: 'xxx',
+ },
+ },
+ indexPatternId: 'original',
+ };
+ const updatedLayer = updateLayerIndexPattern(layer, newIndexPattern);
+ expect(updatedLayer.columnOrder).toEqual([]);
+ expect(updatedLayer.columns).toEqual({});
+ });
+
it('should remove operations referencing fields with insufficient capabilities', () => {
const layer: IndexPatternLayer = {
columnOrder: ['col1', 'col2'],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index 7853b7da7956e..1661e5de8248e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -929,9 +929,17 @@ export function updateLayerIndexPattern(
layer: IndexPatternLayer,
newIndexPattern: IndexPattern
): IndexPatternLayer {
- const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) =>
- isColumnTransferable(column, newIndexPattern)
- );
+ const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => {
+ if ('references' in column) {
+ return (
+ isColumnTransferable(column, newIndexPattern) &&
+ column.references.every((columnId) =>
+ isColumnTransferable(layer.columns[columnId], newIndexPattern)
+ )
+ );
+ }
+ return isColumnTransferable(column, newIndexPattern);
+ });
const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => {
const operationDefinition = operationDefinitionMap[column.operationType];
return operationDefinition.transfer
From d338f1c3de637bece7f684d696702e8447d4f2eb Mon Sep 17 00:00:00 2001
From: John Schulz
Date: Mon, 12 Apr 2021 11:01:38 -0400
Subject: [PATCH 16/79] Allow integrations of hosted policies to be updated
(#96705)
## Summary
Remove the restriction against updating integrations on hosted policies.
I described the current behavior and asked if it should change in [1]. Based on the responses in [2] & [3] and looking back at prior discussion around hosted policies, I don't think updates should be restricted.
Adding or removing integrations is still blocked for hosted policies. Updated API tests to confirm behavior.
[1] https://github.com/elastic/kibana/issues/76843#issuecomment-816096760
[2] https://github.com/elastic/kibana/issues/76843#issuecomment-816153871
[3] https://github.com/elastic/kibana/issues/76843#issuecomment-816538672
## Screenshots
Current behavior
Error about updating integrations of a managed policy
via flow A
via flow B
This PR
Successful updates using either form
### Checklist
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../fleet/server/services/package_policy.ts | 20 ++++++++-----------
.../apis/package_policy/update.ts | 16 ++++++---------
2 files changed, 14 insertions(+), 22 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 418a10225edad..210c9128b1ec7 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -316,18 +316,14 @@ class PackagePolicyService {
const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id);
if (!parentAgentPolicy) {
throw new Error('Agent policy not found');
- } else {
- if (parentAgentPolicy.is_managed) {
- throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`);
- }
- if (
- (parentAgentPolicy.package_policies as PackagePolicy[]).find(
- (siblingPackagePolicy) =>
- siblingPackagePolicy.id !== id && siblingPackagePolicy.name === packagePolicy.name
- )
- ) {
- throw new Error('There is already a package with the same name on this agent policy');
- }
+ }
+ if (
+ (parentAgentPolicy.package_policies as PackagePolicy[]).find(
+ (siblingPackagePolicy) =>
+ siblingPackagePolicy.id !== id && siblingPackagePolicy.name === packagePolicy.name
+ )
+ ) {
+ throw new Error('There is already a package with the same name on this agent policy');
}
let inputs = restOfPackagePolicy.inputs.map((input) =>
diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts
index 3e652d47ac425..6e6a475cd4824 100644
--- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts
+++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
@@ -115,15 +114,15 @@ export default function (providerContext: FtrProviderContext) {
await getService('esArchiver').unload('empty_kibana');
});
- it('should fail on managed agent policies', async function () {
- const { body } = await supertest
+ it('should work with valid values on "regular" policies', async function () {
+ await supertest
.put(`/api/fleet/package_policies/${packagePolicyId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-1',
description: '',
namespace: 'updated_namespace',
- policy_id: managedAgentPolicyId,
+ policy_id: agentPolicyId,
enabled: true,
output_id: '',
inputs: [],
@@ -132,13 +131,10 @@ export default function (providerContext: FtrProviderContext) {
title: 'For File Tests',
version: '0.1.0',
},
- })
- .expect(400);
-
- expect(body.message).to.contain('Cannot update integrations of managed policy');
+ });
});
- it('should work with valid values', async function () {
+ it('should work with valid values on hosted policies', async function () {
await supertest
.put(`/api/fleet/package_policies/${packagePolicyId}`)
.set('kbn-xsrf', 'xxxx')
@@ -146,7 +142,7 @@ export default function (providerContext: FtrProviderContext) {
name: 'filetest-1',
description: '',
namespace: 'updated_namespace',
- policy_id: agentPolicyId,
+ policy_id: managedAgentPolicyId,
enabled: true,
output_id: '',
inputs: [],
From 7448238444b9e36ae15286aa2897f055f30d42a7 Mon Sep 17 00:00:00 2001
From: Vadim Dalecky
Date: Mon, 12 Apr 2021 17:55:50 +0200
Subject: [PATCH 17/79] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20improve=20UI?=
=?UTF-8?q?=20actions=20plugin=20readme=20(#96030)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* docs: ✏️ improve UI actions plugin readme
* docs: improve trigger description
* docs: remove unnecessary comma
---
src/plugins/ui_actions/README.asciidoc | 73 +++++++++++++++++++++++---
1 file changed, 65 insertions(+), 8 deletions(-)
diff --git a/src/plugins/ui_actions/README.asciidoc b/src/plugins/ui_actions/README.asciidoc
index 577aa2eae354b..27b3eae3a52a7 100644
--- a/src/plugins/ui_actions/README.asciidoc
+++ b/src/plugins/ui_actions/README.asciidoc
@@ -1,14 +1,71 @@
[[uiactions-plugin]]
== UI Actions
-An API for:
-
-- creating custom functionality (`actions`)
-- creating custom user interaction events (`triggers`)
-- attaching and detaching `actions` to `triggers`.
-- emitting `trigger` events
-- executing `actions` attached to a given `trigger`.
-- exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger.
+UI Actions plugins provides API to manage *triggers* and *actions*.
+
+*Trigger* is an abstract description of user's intent to perform an action
+(like user clicking on a value inside chart). It allows us to do runtime
+binding between code from different plugins. For, example one such
+trigger is when somebody applies filters on dashboard; another one is when
+somebody opens a Dashboard panel context menu.
+
+*Actions* are pieces of code that execute in response to a trigger. For example,
+to the dashboard filtering trigger multiple actions can be attached. Once a user
+filters on the dashboard all possible actions are displayed to the user in a
+popup menu and the user has to chose one.
+
+In general this plugin provides:
+
+- Creating custom functionality (actions).
+- Creating custom user interaction events (triggers).
+- Attaching and detaching actions to triggers.
+- Emitting trigger events.
+- Executing actions attached to a given trigger.
+- Exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger.
+
+=== Basic usage
+
+To get started, first you need to know a trigger you will attach your actions to.
+You can either pick an existing one, or register your own one:
+
+[source,typescript jsx]
+----
+plugins.uiActions.registerTrigger({
+ id: 'MY_APP_PIE_CHART_CLICK',
+ title: 'Pie chart click',
+ description: 'When user clicks on a pie chart slice.',
+});
+----
+
+Now, when user clicks on a pie slice you need to "trigger" your trigger and
+provide some context data:
+
+[source,typescript jsx]
+----
+plugins.uiActions.getTrigger('MY_APP_PIE_CHART_CLICK').exec({
+ /* Custom context data. */
+});
+----
+
+Finally, your code or developers from other plugins can register UI actions that
+listen for the above trigger and execute some code when the trigger is triggered.
+
+[source,typescript jsx]
+----
+plugins.uiActions.registerAction({
+ id: 'DO_SOMETHING',
+ isCompatible: async (context) => true,
+ execute: async (context) => {
+ // Do something.
+ },
+});
+plugins.uiActions.attachAction('MY_APP_PIE_CHART_CLICK', 'DO_SOMETHING');
+----
+
+Now your `DO_SOMETHING` action will automatically execute when `MY_APP_PIE_CHART_CLICK`
+trigger is triggered; or, if more than one compatible action is attached to
+that trigger, user will be presented with a context menu popup to select one
+action to execute.
=== Examples
From b33022f680db69400b37b359bf8b82e8ed21877a Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 12 Apr 2021 11:58:19 -0400
Subject: [PATCH 18/79] [Security Solution][Artifacts] Artifact creation for
Endpoint Event Filtering (#96499)
* generate endpoint event filters artifacts
* Add ExperimentalFeature object to the initialization params of ManifestManager
* create event filters artifacts if feature flag is on
* change artifact migration to be less chatty in the logs (also: don't reference Fleet)
---
.../exception_lists/exception_list_client.ts | 13 +++++
.../endpoint/endpoint_app_context_services.ts | 14 +++++
.../server/endpoint/lib/artifacts/common.ts | 3 +
.../server/endpoint/lib/artifacts/lists.ts | 41 +++++++++++---
.../migrate_artifacts_to_fleet.test.ts | 6 +-
.../artifacts/migrate_artifacts_to_fleet.ts | 10 ++--
.../server/endpoint/mocks.ts | 1 +
.../manifest_manager/manifest_manager.mock.ts | 2 +
.../manifest_manager/manifest_manager.ts | 55 ++++++++++++++++---
.../security_solution/server/plugin.ts | 26 ++++-----
10 files changed, 135 insertions(+), 36 deletions(-)
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 4b371b6dcb930..84b6de1672cd6 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -52,6 +52,7 @@ import {
} from './find_exception_list_items';
import { createEndpointList } from './create_endpoint_list';
import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list';
+import { createEndpointEventFiltersList } from './create_endoint_event_filters_list';
export class ExceptionListClient {
private readonly user: string;
@@ -108,6 +109,18 @@ export class ExceptionListClient {
});
};
+ /**
+ * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist)
+ */
+ public createEndpointEventFiltersList = async (): Promise => {
+ const { savedObjectsClient, user } = this;
+ return createEndpointEventFiltersList({
+ savedObjectsClient,
+ user,
+ version: 1,
+ });
+ };
+
/**
* This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will
* auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
index f4a5d6add4f41..103e3ae80831a 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
@@ -37,6 +37,10 @@ import { metadataTransformPrefix } from '../../common/endpoint/constants';
import { AppClientFactory } from '../client';
import { ConfigType } from '../config';
import { LicenseService } from '../../common/license/license';
+import {
+ ExperimentalFeatures,
+ parseExperimentalConfigValue,
+} from '../../common/experimental_features';
export interface MetadataService {
queryStrategy(
@@ -107,6 +111,9 @@ export class EndpointAppContextService {
private agentPolicyService: AgentPolicyServiceInterface | undefined;
private savedObjectsStart: SavedObjectsServiceStart | undefined;
private metadataService: MetadataService | undefined;
+ private config: ConfigType | undefined;
+
+ private experimentalFeatures: ExperimentalFeatures | undefined;
public start(dependencies: EndpointAppContextServiceStartContract) {
this.agentService = dependencies.agentService;
@@ -115,6 +122,9 @@ export class EndpointAppContextService {
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;
this.metadataService = createMetadataService(dependencies.packageService!);
+ this.config = dependencies.config;
+
+ this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental);
if (this.manifestManager && dependencies.registerIngestCallback) {
dependencies.registerIngestCallback(
@@ -140,6 +150,10 @@ export class EndpointAppContextService {
public stop() {}
+ public getExperimentalFeatures(): Readonly | undefined {
+ return this.experimentalFeatures;
+ }
+
public getAgentService(): AgentService | undefined {
return this.agentService;
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
index 65bd6ffd15f5f..7cfcf11379dd8 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
@@ -22,6 +22,9 @@ export const ArtifactConstants = {
SUPPORTED_OPERATING_SYSTEMS: ['macos', 'windows'],
SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'],
GLOBAL_TRUSTED_APPS_NAME: 'endpoint-trustlist',
+
+ SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'],
+ GLOBAL_EVENT_FILTERS_NAME: 'endpoint-eventfilterlist',
};
export const ManifestConstants = {
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index 322bb2ca47a45..1c3c92c50afd3 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -14,20 +14,21 @@ import { Entry, EntryNested } from '../../../../../lists/common/schemas/types';
import { ExceptionListClient } from '../../../../../lists/server';
import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports';
import {
+ internalArtifactCompleteSchema,
+ InternalArtifactCompleteSchema,
InternalArtifactSchema,
TranslatedEntry,
- WrappedTranslatedExceptionList,
- wrappedTranslatedExceptionList,
- TranslatedEntryNestedEntry,
- translatedEntryNestedEntry,
translatedEntry as translatedEntryType,
+ translatedEntryMatchAnyMatcher,
TranslatedEntryMatcher,
translatedEntryMatchMatcher,
- translatedEntryMatchAnyMatcher,
+ TranslatedEntryNestedEntry,
+ translatedEntryNestedEntry,
TranslatedExceptionListItem,
- internalArtifactCompleteSchema,
- InternalArtifactCompleteSchema,
+ WrappedTranslatedExceptionList,
+ wrappedTranslatedExceptionList,
} from '../../schemas';
+import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../../lists/common/constants';
export async function buildArtifact(
exceptions: WrappedTranslatedExceptionList,
@@ -77,7 +78,10 @@ export async function getFilteredEndpointExceptionList(
eClient: ExceptionListClient,
schemaVersion: string,
filter: string,
- listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID
+ listId:
+ | typeof ENDPOINT_LIST_ID
+ | typeof ENDPOINT_TRUSTED_APPS_LIST_ID
+ | typeof ENDPOINT_EVENT_FILTERS_LIST_ID
): Promise {
const exceptions: WrappedTranslatedExceptionList = { entries: [] };
let page = 1;
@@ -142,6 +146,27 @@ export async function getEndpointTrustedAppsList(
);
}
+export async function getEndpointEventFiltersList(
+ eClient: ExceptionListClient,
+ schemaVersion: string,
+ os: string,
+ policyId?: string
+): Promise {
+ const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`;
+ const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${
+ policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : ''
+ })`;
+
+ await eClient.createEndpointEventFiltersList();
+
+ return getFilteredEndpointExceptionList(
+ eClient,
+ schemaVersion,
+ `${osFilter} and ${policyFilter}`,
+ ENDPOINT_EVENT_FILTERS_LIST_ID
+ );
+}
+
/**
* Translates Exception list items to Exceptions the endpoint can understand
* @param exceptions
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts
index d0ad6e4734baf..cf1f178a80e78 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts
@@ -66,8 +66,8 @@ describe('When migrating artifacts to fleet', () => {
it('should do nothing if `fleetServerEnabled` flag is false', async () => {
await migrateArtifactsToFleet(soClient, artifactClient, logger, false);
- expect(logger.info).toHaveBeenCalledWith(
- 'Skipping Artifacts migration to fleet. [fleetServerEnabled] flag is off'
+ expect(logger.debug).toHaveBeenCalledWith(
+ 'Skipping Artifacts migration. [fleetServerEnabled] flag is off'
);
expect(soClient.find).not.toHaveBeenCalled();
});
@@ -94,7 +94,7 @@ describe('When migrating artifacts to fleet', () => {
const error = new Error('test: delete failed');
soClient.delete.mockRejectedValue(error);
await expect(migrateArtifactsToFleet(soClient, artifactClient, logger, true)).rejects.toThrow(
- 'Artifact SO migration to fleet failed'
+ 'Artifact SO migration failed'
);
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts
index bcbcb7f63e3ca..ba3c15cecf217 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts
@@ -27,7 +27,7 @@ export const migrateArtifactsToFleet = async (
isFleetServerEnabled: boolean
): Promise => {
if (!isFleetServerEnabled) {
- logger.info('Skipping Artifacts migration to fleet. [fleetServerEnabled] flag is off');
+ logger.debug('Skipping Artifacts migration. [fleetServerEnabled] flag is off');
return;
}
@@ -49,14 +49,16 @@ export const migrateArtifactsToFleet = async (
if (totalArtifactsMigrated === -1) {
totalArtifactsMigrated = total;
if (total > 0) {
- logger.info(`Migrating artifacts from SavedObject to Fleet`);
+ logger.info(`Migrating artifacts from SavedObject`);
}
}
// If nothing else to process, then exit out
if (total === 0) {
hasMore = false;
- logger.info(`Total Artifacts migrated to Fleet: ${totalArtifactsMigrated}`);
+ if (totalArtifactsMigrated > 0) {
+ logger.info(`Total Artifacts migrated: ${totalArtifactsMigrated}`);
+ }
return;
}
@@ -78,7 +80,7 @@ export const migrateArtifactsToFleet = async (
}
}
} catch (e) {
- const error = new ArtifactMigrationError('Artifact SO migration to fleet failed', e);
+ const error = new ArtifactMigrationError('Artifact SO migration failed', e);
logger.error(error);
throw error;
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
index c82d2b6524773..d1911a39166dc 100644
--- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
@@ -56,6 +56,7 @@ export const createMockEndpointAppContextService = (
return ({
start: jest.fn(),
stop: jest.fn(),
+ getExperimentalFeatures: jest.fn(),
getAgentService: jest.fn(),
getAgentPolicyService: jest.fn(),
getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()),
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
index ececb425af657..6f41fe3578496 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
@@ -22,6 +22,7 @@ import {
} from '../../../lib/artifacts/mocks';
import { createEndpointArtifactClientMock, getManifestClientMock } from '../mocks';
import { ManifestManager, ManifestManagerContext } from './manifest_manager';
+import { parseExperimentalConfigValue } from '../../../../../common/experimental_features';
export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({
data,
@@ -85,6 +86,7 @@ export const buildManifestManagerContextMock = (
...fullOpts,
artifactClient: createEndpointArtifactClientMock(),
logger: loggingSystemMock.create().get() as jest.Mocked,
+ experimentalFeatures: parseExperimentalConfigValue([]),
};
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index 9ed17686fd2bc..b3d8b63687d31 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -22,6 +22,7 @@ import {
ArtifactConstants,
buildArtifact,
getArtifactId,
+ getEndpointEventFiltersList,
getEndpointExceptionList,
getEndpointTrustedAppsList,
isCompressed,
@@ -34,6 +35,7 @@ import {
} from '../../../schemas/artifacts';
import { EndpointArtifactClientInterface } from '../artifact_client';
import { ManifestClient } from '../manifest_client';
+import { ExperimentalFeatures } from '../../../../../common/experimental_features';
interface ArtifactsBuildResult {
defaultArtifacts: InternalArtifactCompleteSchema[];
@@ -81,6 +83,7 @@ export interface ManifestManagerContext {
packagePolicyService: PackagePolicyServiceInterface;
logger: Logger;
cache: LRU;
+ experimentalFeatures: ExperimentalFeatures;
}
const getArtifactIds = (manifest: ManifestSchema) =>
@@ -99,11 +102,9 @@ export class ManifestManager {
protected logger: Logger;
protected cache: LRU;
protected schemaVersion: ManifestSchemaVersion;
+ protected experimentalFeatures: ExperimentalFeatures;
- constructor(
- context: ManifestManagerContext,
- private readonly isFleetServerEnabled: boolean = false
- ) {
+ constructor(context: ManifestManagerContext) {
this.artifactClient = context.artifactClient;
this.exceptionListClient = context.exceptionListClient;
this.packagePolicyService = context.packagePolicyService;
@@ -111,6 +112,7 @@ export class ManifestManager {
this.logger = context.logger;
this.cache = context.cache;
this.schemaVersion = 'v1';
+ this.experimentalFeatures = context.experimentalFeatures;
}
/**
@@ -198,6 +200,41 @@ export class ManifestManager {
return { defaultArtifacts, policySpecificArtifacts };
}
+ /**
+ * Builds an array of endpoint event filters (one per supported OS) based on the current state of the
+ * Event Filters list
+ * @protected
+ */
+ protected async buildEventFiltersArtifacts(): Promise {
+ const defaultArtifacts: InternalArtifactCompleteSchema[] = [];
+ const policySpecificArtifacts: Record = {};
+
+ for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) {
+ defaultArtifacts.push(await this.buildEventFiltersForOs(os));
+ }
+
+ await iterateAllListItems(
+ (page) => this.listEndpointPolicyIds(page),
+ async (policyId) => {
+ for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) {
+ policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || [];
+ policySpecificArtifacts[policyId].push(await this.buildEventFiltersForOs(os, policyId));
+ }
+ }
+ );
+
+ return { defaultArtifacts, policySpecificArtifacts };
+ }
+
+ protected async buildEventFiltersForOs(os: string, policyId?: string) {
+ return buildArtifact(
+ await getEndpointEventFiltersList(this.exceptionListClient, this.schemaVersion, os, policyId),
+ this.schemaVersion,
+ os,
+ ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME
+ );
+ }
+
/**
* Writes new artifact SO.
*
@@ -286,7 +323,7 @@ export class ManifestManager {
semanticVersion: manifestSo.attributes.semanticVersion,
soVersion: manifestSo.version,
},
- this.isFleetServerEnabled
+ this.experimentalFeatures.fleetServerEnabled
);
for (const entry of manifestSo.attributes.artifacts) {
@@ -327,12 +364,16 @@ export class ManifestManager {
public async buildNewManifest(
baselineManifest: Manifest = ManifestManager.createDefaultManifest(
this.schemaVersion,
- this.isFleetServerEnabled
+ this.experimentalFeatures.fleetServerEnabled
)
): Promise {
const results = await Promise.all([
this.buildExceptionListArtifacts(),
this.buildTrustedAppsArtifacts(),
+ // If Endpoint Event Filtering feature is ON, then add in the exceptions for them
+ ...(this.experimentalFeatures.eventFilteringEnabled
+ ? [this.buildEventFiltersArtifacts()]
+ : []),
]);
const manifest = new Manifest(
@@ -341,7 +382,7 @@ export class ManifestManager {
semanticVersion: baselineManifest.getSemanticVersion(),
soVersion: baselineManifest.getSavedObjectVersion(),
},
- this.isFleetServerEnabled
+ this.experimentalFeatures.fleetServerEnabled
);
for (const result of results) {
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 04f98e53ea9a3..8dab308affad8 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -349,24 +349,22 @@ export class Plugin implements IPlugin {
@@ -376,7 +374,7 @@ export class Plugin implements IPlugin {
- logger.info('Fleet setup complete - Starting ManifestTask');
+ logger.info('Dependent plugin setup complete - Starting ManifestTask');
if (this.manifestTask) {
this.manifestTask.start({
From f544d8d458ef1612b5da1950b0e00c4d88ca4225 Mon Sep 17 00:00:00 2001
From: Rudolf Meijering
Date: Mon, 12 Apr 2021 18:19:42 +0200
Subject: [PATCH 19/79] Migrations v2 ignore fleet agent events (#96690)
* migrationsv2: ignore fleet agent events and tsvb telemetry
* migrationsv1: ignore tsvb-validation-telemetry
* Skip fleet test that depends on fleet-agent-events
* Fix typescript errors
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../migrations/core/elastic_index.test.ts | 15 +++-
.../migrations/core/elastic_index.ts | 23 +++---
.../migrations/kibana/kibana_migrator.test.ts | 6 +-
.../migrationsv2/actions/index.test.ts | 3 +-
.../migrationsv2/actions/index.ts | 17 ++++-
.../integration_tests/actions.test.ts | 75 +++++++++++++++----
.../migrations_state_action_machine.test.ts | 28 +++++++
.../saved_objects/migrationsv2/model.test.ts | 8 ++
.../saved_objects/migrationsv2/model.ts | 6 ++
.../server/saved_objects/migrationsv2/next.ts | 12 ++-
.../saved_objects/migrationsv2/types.ts | 5 ++
.../apis/agents_setup.ts | 2 +-
12 files changed, 164 insertions(+), 36 deletions(-)
diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts
index 5cb2a88c4733f..2fc78fc619cab 100644
--- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts
+++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts
@@ -414,11 +414,18 @@ describe('ElasticIndex', () => {
size: 100,
query: {
bool: {
- must_not: {
- term: {
- type: 'fleet-agent-events',
+ must_not: [
+ {
+ term: {
+ type: 'fleet-agent-events',
+ },
},
- },
+ {
+ term: {
+ type: 'tsvb-validation-telemetry',
+ },
+ },
+ ],
},
},
},
diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts
index a5f3cb36e736b..462425ff6e3e0 100644
--- a/src/core/server/saved_objects/migrations/core/elastic_index.ts
+++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts
@@ -70,16 +70,19 @@ export function reader(
let scrollId: string | undefined;
// When migrating from the outdated index we use a read query which excludes
- // saved objects which are no longer used. These saved objects will still be
- // kept in the outdated index for backup purposes, but won't be availble in
- // the upgraded index.
- const excludeUnusedTypes = {
+ // saved object types which are no longer used. These saved objects will
+ // still be kept in the outdated index for backup purposes, but won't be
+ // availble in the upgraded index.
+ const EXCLUDE_UNUSED_TYPES = [
+ 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869
+ 'tsvb-validation-telemetry', // https://github.com/elastic/kibana/issues/95617
+ ];
+
+ const excludeUnusedTypesQuery = {
bool: {
- must_not: {
- term: {
- type: 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869
- },
- },
+ must_not: EXCLUDE_UNUSED_TYPES.map((type) => ({
+ term: { type },
+ })),
},
};
@@ -92,7 +95,7 @@ export function reader(
: client.search>({
body: {
size: batchSize,
- query: excludeUnusedTypes,
+ query: excludeUnusedTypesQuery,
},
index,
scroll,
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
index 40d18c3b5063a..221e78e3e12e2 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
@@ -321,7 +321,7 @@ describe('KibanaMigrator', () => {
options.client.tasks.get.mockReturnValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({
completed: true,
- error: { type: 'elatsicsearch_exception', reason: 'task failed with an error' },
+ error: { type: 'elasticsearch_exception', reason: 'task failed with an error' },
failures: [],
task: { description: 'task description' } as any,
})
@@ -331,11 +331,11 @@ describe('KibanaMigrator', () => {
migrator.prepareMigrations();
await expect(migrator.runMigrations()).rejects.toMatchInlineSnapshot(`
[Error: Unable to complete saved object migrations for the [.my-index] index. Error: Reindex failed with the following error:
- {"_tag":"Some","value":{"type":"elatsicsearch_exception","reason":"task failed with an error"}}]
+ {"_tag":"Some","value":{"type":"elasticsearch_exception","reason":"task failed with an error"}}]
`);
expect(loggingSystemMock.collect(options.logger).error[0][0]).toMatchInlineSnapshot(`
[Error: Reindex failed with the following error:
- {"_tag":"Some","value":{"type":"elatsicsearch_exception","reason":"task failed with an error"}}]
+ {"_tag":"Some","value":{"type":"elasticsearch_exception","reason":"task failed with an error"}}]
`);
});
});
diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts
index 14ca73e7fcca0..bee17f42d7bdb 100644
--- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts
@@ -85,7 +85,8 @@ describe('actions', () => {
'my_source_index',
'my_target_index',
Option.none,
- false
+ false,
+ Option.none
);
try {
await task();
diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts
index 8ac683a29d657..d759c0c9be20e 100644
--- a/src/core/server/saved_objects/migrationsv2/actions/index.ts
+++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts
@@ -14,6 +14,7 @@ import { errors as EsErrors } from '@elastic/elasticsearch';
import type { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors';
import { pipe } from 'fp-ts/lib/pipeable';
import { flow } from 'fp-ts/lib/function';
+import { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl';
import { ElasticsearchClient } from '../../../elasticsearch';
import { IndexMapping } from '../../mappings';
import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization';
@@ -436,7 +437,12 @@ export const reindex = (
sourceIndex: string,
targetIndex: string,
reindexScript: Option.Option,
- requireAlias: boolean
+ requireAlias: boolean,
+ /* When reindexing we use a source query to exclude saved objects types which
+ * are no longer used. These saved objects will still be kept in the outdated
+ * index for backup purposes, but won't be availble in the upgraded index.
+ */
+ unusedTypesToExclude: Option.Option
): TaskEither.TaskEither => () => {
return client
.reindex({
@@ -450,6 +456,15 @@ export const reindex = (
index: sourceIndex,
// Set reindex batch size
size: BATCH_SIZE,
+ // Exclude saved object types
+ query: Option.fold(
+ () => undefined,
+ (types) => ({
+ bool: {
+ must_not: types.map((type) => ({ term: { type } })),
+ },
+ })
+ )(unusedTypesToExclude),
},
dest: {
index: targetIndex,
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts
index aa9a5ea92ac11..3ed3ace416990 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts
@@ -66,7 +66,8 @@ describe('migration actions', () => {
{ _source: { title: 'doc 1' } },
{ _source: { title: 'doc 2' } },
{ _source: { title: 'doc 3' } },
- { _source: { title: 'saved object 4' } },
+ { _source: { title: 'saved object 4', type: 'another_unused_type' } },
+ { _source: { title: 'f-agent-event 5', type: 'f_agent_event' } },
] as unknown) as SavedObjectsRawDoc[];
await bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', sourceDocs)();
@@ -343,7 +344,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -364,6 +366,37 @@ describe('migration actions', () => {
"doc 2",
"doc 3",
"saved object 4",
+ "f-agent-event 5",
+ ]
+ `);
+ });
+ it('resolves right and excludes all unusedTypesToExclude documents', async () => {
+ const res = (await reindex(
+ client,
+ 'existing_index_with_docs',
+ 'reindex_target_excluded_docs',
+ Option.none,
+ false,
+ Option.some(['f_agent_event', 'another_unused_type'])
+ )()) as Either.Right;
+ const task = waitForReindexTask(client, res.right.taskId, '10s');
+ await expect(task()).resolves.toMatchInlineSnapshot(`
+ Object {
+ "_tag": "Right",
+ "right": "reindex_succeeded",
+ }
+ `);
+
+ const results = ((await searchForOutdatedDocuments(client, {
+ batchSize: 1000,
+ targetIndex: 'reindex_target_excluded_docs',
+ outdatedDocumentsQuery: undefined,
+ })()) as Either.Right).right.outdatedDocuments;
+ expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(`
+ Array [
+ "doc 1",
+ "doc 2",
+ "doc 3",
]
`);
});
@@ -374,7 +407,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_2',
Option.some(`ctx._source.title = ctx._source.title + '_updated'`),
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -394,6 +428,7 @@ describe('migration actions', () => {
"doc 2_updated",
"doc 3_updated",
"saved object 4_updated",
+ "f-agent-event 5_updated",
]
`);
});
@@ -405,7 +440,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_3',
Option.some(`ctx._source.title = ctx._source.title + '_updated'`),
- false
+ false,
+ Option.none
)()) as Either.Right;
let task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -421,7 +457,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_3',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -443,6 +480,7 @@ describe('migration actions', () => {
"doc 2_updated",
"doc 3_updated",
"saved object 4_updated",
+ "f-agent-event 5_updated",
]
`);
});
@@ -469,7 +507,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_4',
Option.some(`ctx._source.title = ctx._source.title + '_updated'`),
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -491,6 +530,7 @@ describe('migration actions', () => {
"doc 2",
"doc 3_updated",
"saved object 4_updated",
+ "f-agent-event 5_updated",
]
`);
});
@@ -517,7 +557,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_5',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, reindexTaskId, '10s');
@@ -551,7 +592,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_6',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, reindexTaskId, '10s');
@@ -571,7 +613,8 @@ describe('migration actions', () => {
'no_such_index',
'reindex_target',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
await expect(task()).resolves.toMatchInlineSnapshot(`
@@ -591,7 +634,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'existing_index_with_write_block',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
@@ -612,7 +656,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'existing_index_with_write_block',
Option.none,
- true
+ true,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '10s');
@@ -633,7 +678,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
const task = waitForReindexTask(client, res.right.taskId, '0s');
@@ -659,7 +705,8 @@ describe('migration actions', () => {
'existing_index_with_docs',
'reindex_target_7',
Option.none,
- false
+ false,
+ Option.none
)()) as Either.Right;
await waitForReindexTask(client, res.right.taskId, '10s')();
@@ -714,7 +761,7 @@ describe('migration actions', () => {
targetIndex: 'existing_index_with_docs',
outdatedDocumentsQuery: undefined,
})()) as Either.Right).right.outdatedDocuments;
- expect(resultsWithoutQuery.length).toBe(4);
+ expect(resultsWithoutQuery.length).toBe(5);
});
it('resolves with _id, _source, _seq_no and _primary_term', async () => {
expect.assertions(1);
diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
index d4ce7b74baa5f..2c2cd0032abfd 100644
--- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
@@ -249,6 +249,13 @@ describe('migrationsStateActionMachine', () => {
},
},
},
+ "unusedTypesToExclude": Object {
+ "_tag": "Some",
+ "value": Array [
+ "fleet-agent-events",
+ "tsvb-validation-telemetry",
+ ],
+ },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
},
@@ -310,6 +317,13 @@ describe('migrationsStateActionMachine', () => {
},
},
},
+ "unusedTypesToExclude": Object {
+ "_tag": "Some",
+ "value": Array [
+ "fleet-agent-events",
+ "tsvb-validation-telemetry",
+ ],
+ },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
},
@@ -456,6 +470,13 @@ describe('migrationsStateActionMachine', () => {
},
},
},
+ "unusedTypesToExclude": Object {
+ "_tag": "Some",
+ "value": Array [
+ "fleet-agent-events",
+ "tsvb-validation-telemetry",
+ ],
+ },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
},
@@ -512,6 +533,13 @@ describe('migrationsStateActionMachine', () => {
},
},
},
+ "unusedTypesToExclude": Object {
+ "_tag": "Some",
+ "value": Array [
+ "fleet-agent-events",
+ "tsvb-validation-telemetry",
+ ],
+ },
"versionAlias": ".my-so-index_7.11.0",
"versionIndex": ".my-so-index_7.11.0_001",
},
diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts
index f9bf3418c0ab6..4fd9b7cbb3df4 100644
--- a/src/core/server/saved_objects/migrationsv2/model.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/model.test.ts
@@ -69,6 +69,7 @@ describe('migrations v2 model', () => {
versionAlias: '.kibana_7.11.0',
versionIndex: '.kibana_7.11.0_001',
tempIndex: '.kibana_7.11.0_reindex_temp',
+ unusedTypesToExclude: Option.some(['unused-fleet-agent-events']),
};
describe('exponential retry delays for retryable_es_client_error', () => {
@@ -1242,6 +1243,13 @@ describe('migrations v2 model', () => {
},
},
},
+ "unusedTypesToExclude": Object {
+ "_tag": "Some",
+ "value": Array [
+ "fleet-agent-events",
+ "tsvb-validation-telemetry",
+ ],
+ },
"versionAlias": ".kibana_task_manager_8.1.0",
"versionIndex": ".kibana_task_manager_8.1.0_001",
}
diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts
index e62bd108faea0..2353452a6a51b 100644
--- a/src/core/server/saved_objects/migrationsv2/model.ts
+++ b/src/core/server/saved_objects/migrationsv2/model.ts
@@ -768,6 +768,11 @@ export const createInitialState = ({
},
};
+ const unusedTypesToExclude = Option.some([
+ 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869
+ 'tsvb-validation-telemetry', // https://github.com/elastic/kibana/issues/95617
+ ]);
+
const initialState: InitState = {
controlState: 'INIT',
indexPrefix,
@@ -786,6 +791,7 @@ export const createInitialState = ({
retryAttempts: migrationsConfig.retryAttempts,
batchSize: migrationsConfig.batchSize,
logs: [],
+ unusedTypesToExclude,
};
return initialState;
};
diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts
index 5c159f4f24e22..67b2004a4b31a 100644
--- a/src/core/server/saved_objects/migrationsv2/next.ts
+++ b/src/core/server/saved_objects/migrationsv2/next.ts
@@ -61,7 +61,14 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra
CREATE_REINDEX_TEMP: (state: CreateReindexTempState) =>
Actions.createIndex(client, state.tempIndex, state.tempIndexMappings),
REINDEX_SOURCE_TO_TEMP: (state: ReindexSourceToTempState) =>
- Actions.reindex(client, state.sourceIndex.value, state.tempIndex, Option.none, false),
+ Actions.reindex(
+ client,
+ state.sourceIndex.value,
+ state.tempIndex,
+ Option.none,
+ false,
+ state.unusedTypesToExclude
+ ),
SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) =>
Actions.setWriteBlock(client, state.tempIndex),
REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK: (state: ReindexSourceToTempWaitForTaskState) =>
@@ -104,7 +111,8 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra
state.legacyIndex,
state.sourceIndex.value,
state.preMigrationScript,
- false
+ false,
+ state.unusedTypesToExclude
),
LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) =>
Actions.waitForReindexTask(client, state.legacyReindexTaskId, '60s'),
diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts
index 8d6fe3f030eb3..cc4aa18171843 100644
--- a/src/core/server/saved_objects/migrationsv2/types.ts
+++ b/src/core/server/saved_objects/migrationsv2/types.ts
@@ -89,6 +89,11 @@ export interface BaseState extends ControlState {
* prevents lost deletes e.g. `.kibana_7.11.0_reindex`.
*/
readonly tempIndex: string;
+ /* When reindexing we use a source query to exclude saved objects types which
+ * are no longer used. These saved objects will still be kept in the outdated
+ * index for backup purposes, but won't be availble in the upgraded index.
+ */
+ readonly unusedTypesToExclude: Option.Option;
}
export type InitState = BaseState & {
diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts
index 91d6ca0119d1d..700a06750d2f4 100644
--- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts
@@ -101,7 +101,7 @@ export default function (providerContext: FtrProviderContext) {
);
});
- it('should create or update the fleet_enroll user if called multiple times with forceRecreate flag', async () => {
+ it.skip('should create or update the fleet_enroll user if called multiple times with forceRecreate flag', async () => {
await supertest.post(`/api/fleet/agents/setup`).set('kbn-xsrf', 'xxxx').expect(200);
const { body: userResponseFirstTime } = await es.security.getUser({
From b645fec8b82be0ccfa6fc16378482333a2977afa Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Mon, 12 Apr 2021 12:25:03 -0400
Subject: [PATCH 20/79] [Dashboard] Move all dashboard extract/inject into
persistable state (#96095)
* Move all dashboard inject/extract to be part of embeddable persistable state
* Fixes typescript errors
* Remove comments
* Fixes test
* API Doc changes
* Fix integration tests
* Fix functional testS
* Fix unit tests
* Update Dashboard plugin API to get dashboard embeddable renderer
* Fix Types
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
...ugins-embeddable-server.embeddablestart.md | 11 +
...kibana-plugin-plugins-embeddable-server.md | 6 +
.../public/app.tsx | 4 +-
.../public/by_value/embeddable.tsx | 4 +-
.../public/plugin.tsx | 3 +-
src/plugins/dashboard/common/bwc/types.ts | 1 +
...hboard_container_persistable_state.test.ts | 158 +++++
.../dashboard_container_persistable_state.ts | 125 ++++
.../embeddable_saved_object_converters.ts | 2 +
src/plugins/dashboard/common/index.ts | 1 +
.../common/saved_dashboard_references.test.ts | 132 ++++-
.../common/saved_dashboard_references.ts | 195 ++++---
src/plugins/dashboard/common/types.ts | 15 +-
.../dashboard_container_factory.tsx | 14 +-
src/plugins/dashboard/public/plugin.tsx | 35 +-
.../dashboard_container_embeddable_factory.ts | 24 +
src/plugins/dashboard/server/plugin.ts | 20 +-
.../dashboard_migrations.test.ts | 544 ++++++++++--------
src/plugins/embeddable/server/index.ts | 4 +-
src/plugins/embeddable/server/server.api.md | 5 +
.../apis/saved_objects/export.ts | 6 +-
21 files changed, 964 insertions(+), 345 deletions(-)
create mode 100644 docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md
create mode 100644 src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts
create mode 100644 src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts
create mode 100644 src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts
diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md
new file mode 100644
index 0000000000000..c69850006e146
--- /dev/null
+++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md)
+
+## EmbeddableStart type
+
+Signature:
+
+```typescript
+export declare type EmbeddableStart = PersistableStateService;
+```
diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md
index 19ee57d677250..5b3083e039847 100644
--- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md
+++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.md
@@ -18,3 +18,9 @@
| --- | --- |
| [plugin](./kibana-plugin-plugins-embeddable-server.plugin.md) | |
+## Type Aliases
+
+| Type Alias | Description |
+| --- | --- |
+| [EmbeddableStart](./kibana-plugin-plugins-embeddable-server.embeddablestart.md) | |
+
diff --git a/examples/dashboard_embeddable_examples/public/app.tsx b/examples/dashboard_embeddable_examples/public/app.tsx
index 0e21e4421e742..8a6b5a90a22a8 100644
--- a/examples/dashboard_embeddable_examples/public/app.tsx
+++ b/examples/dashboard_embeddable_examples/public/app.tsx
@@ -55,7 +55,9 @@ const Nav = withRouter(({ history, pages }: NavProps) => {
interface Props {
basename: string;
- DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
+ DashboardContainerByValueRenderer: ReturnType<
+ DashboardStart['getDashboardContainerByValueRenderer']
+ >;
}
const DashboardEmbeddableExplorerApp = ({ basename, DashboardContainerByValueRenderer }: Props) => {
diff --git a/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx b/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx
index cba87d466176e..29297341c3016 100644
--- a/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx
+++ b/examples/dashboard_embeddable_examples/public/by_value/embeddable.tsx
@@ -96,7 +96,9 @@ const initialInput: DashboardContainerInput = {
export const DashboardEmbeddableByValue = ({
DashboardContainerByValueRenderer,
}: {
- DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
+ DashboardContainerByValueRenderer: ReturnType<
+ DashboardStart['getDashboardContainerByValueRenderer']
+ >;
}) => {
const [input, setInput] = useState(initialInput);
diff --git a/examples/dashboard_embeddable_examples/public/plugin.tsx b/examples/dashboard_embeddable_examples/public/plugin.tsx
index e57c12daaef23..57678f5a2a517 100644
--- a/examples/dashboard_embeddable_examples/public/plugin.tsx
+++ b/examples/dashboard_embeddable_examples/public/plugin.tsx
@@ -33,8 +33,7 @@ export class DashboardEmbeddableExamples implements Plugin {
+ it('should inject the extracted saved object panel', () => {
+ const inject = createInject(persistableStateService);
+ const references = [extractedSavedObjectPanelRef];
+
+ const injected = inject(
+ dashboardWithExtractedPanel,
+ references
+ ) as DashboardContainerStateWithType;
+
+ expect(injected).toEqual(unextractedDashboardState);
+ });
+
+ it('should extract the saved object panel', () => {
+ const extract = createExtract(persistableStateService);
+ const { state: extractedState, references: extractedReferences } = extract(
+ unextractedDashboardState
+ );
+
+ expect(extractedState).toEqual(dashboardWithExtractedPanel);
+ expect(extractedReferences[0]).toEqual(extractedSavedObjectPanelRef);
+ });
+});
+
+const dashboardWithExtractedByValuePanel: DashboardContainerStateWithType = {
+ id: 'id',
+ type: 'dashboard',
+ panels: {
+ panel_1: {
+ type: 'panel_type',
+ gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
+ explicitInput: {
+ id: 'panel_1',
+ extracted_reference: 'ref',
+ },
+ },
+ },
+};
+
+const extractedByValueRef = {
+ id: 'id',
+ name: 'panel_1:ref',
+ type: 'panel_type',
+};
+
+const unextractedDashboardByValueState: DashboardContainerStateWithType = {
+ id: 'id',
+ type: 'dashboard',
+ panels: {
+ panel_1: {
+ type: 'panel_type',
+ gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' },
+ explicitInput: {
+ id: 'panel_1',
+ value: 'id',
+ },
+ },
+ },
+};
+
+describe('inject/extract by value panels', () => {
+ it('should inject the extracted references', () => {
+ const inject = createInject(persistableStateService);
+
+ persistableStateService.inject.mockImplementationOnce((state, references) => {
+ const ref = references.find((r) => r.name === 'ref');
+ if (!ref) {
+ return state;
+ }
+
+ if (('extracted_reference' in state) as any) {
+ (state as any).value = ref.id;
+ delete (state as any).extracted_reference;
+ }
+
+ return state;
+ });
+
+ const injectedState = inject(dashboardWithExtractedByValuePanel, [extractedByValueRef]);
+
+ expect(injectedState).toEqual(unextractedDashboardByValueState);
+ });
+
+ it('should extract references using persistable state', () => {
+ const extract = createExtract(persistableStateService);
+
+ persistableStateService.extract.mockImplementationOnce((state) => {
+ if ((state as any).value === 'id') {
+ delete (state as any).value;
+ (state as any).extracted_reference = 'ref';
+
+ return {
+ state,
+ references: [{ id: extractedByValueRef.id, name: 'ref', type: extractedByValueRef.type }],
+ };
+ }
+
+ return { state, references: [] };
+ });
+
+ const { state: extractedState, references: extractedReferences } = extract(
+ unextractedDashboardByValueState
+ );
+
+ expect(extractedState).toEqual(dashboardWithExtractedByValuePanel);
+ expect(extractedReferences).toEqual([extractedByValueRef]);
+ });
+});
diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts
new file mode 100644
index 0000000000000..6104fcfdbe949
--- /dev/null
+++ b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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 {
+ EmbeddableInput,
+ EmbeddablePersistableStateService,
+ EmbeddableStateWithType,
+} from '../../../embeddable/common';
+import { SavedObjectReference } from '../../../../core/types';
+import { DashboardContainerStateWithType, DashboardPanelState } from '../types';
+
+const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
+
+export const createInject = (
+ persistableStateService: EmbeddablePersistableStateService
+): EmbeddablePersistableStateService['inject'] => {
+ return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
+ const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
+
+ if ('panels' in workingState) {
+ workingState.panels = { ...workingState.panels };
+
+ for (const [key, panel] of Object.entries(workingState.panels)) {
+ workingState.panels[key] = { ...panel };
+ // Find the references for this panel
+ const prefix = getPanelStatePrefix(panel);
+
+ const filteredReferences = references
+ .filter((reference) => reference.name.indexOf(prefix) === 0)
+ .map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') }));
+
+ const panelReferences = filteredReferences.length === 0 ? references : filteredReferences;
+
+ // Inject dashboard references back in
+ if (panel.panelRefName !== undefined) {
+ const matchingReference = panelReferences.find(
+ (reference) => reference.name === panel.panelRefName
+ );
+
+ if (!matchingReference) {
+ throw new Error(`Could not find reference "${panel.panelRefName}"`);
+ }
+
+ if (matchingReference !== undefined) {
+ workingState.panels[key] = {
+ ...panel,
+ type: matchingReference.type,
+ explicitInput: {
+ ...workingState.panels[key].explicitInput,
+ savedObjectId: matchingReference.id,
+ },
+ };
+
+ delete workingState.panels[key].panelRefName;
+ }
+ }
+
+ const { type, ...injectedState } = persistableStateService.inject(
+ { ...workingState.panels[key].explicitInput, type: workingState.panels[key].type },
+ panelReferences
+ );
+
+ workingState.panels[key].explicitInput = injectedState as EmbeddableInput;
+ }
+ }
+
+ return workingState as EmbeddableStateWithType;
+ };
+};
+
+export const createExtract = (
+ persistableStateService: EmbeddablePersistableStateService
+): EmbeddablePersistableStateService['extract'] => {
+ return (state: EmbeddableStateWithType) => {
+ const workingState = { ...state } as EmbeddableStateWithType | DashboardContainerStateWithType;
+
+ const references: SavedObjectReference[] = [];
+
+ if ('panels' in workingState) {
+ workingState.panels = { ...workingState.panels };
+
+ // Run every panel through the state service to get the nested references
+ for (const [key, panel] of Object.entries(workingState.panels)) {
+ const prefix = getPanelStatePrefix(panel);
+
+ // If the panel is a saved object, then we will make the reference for that saved object and change the explicit input
+ if (panel.explicitInput.savedObjectId) {
+ panel.panelRefName = `panel_${key}`;
+
+ references.push({
+ name: `${prefix}panel_${key}`,
+ type: panel.type,
+ id: panel.explicitInput.savedObjectId as string,
+ });
+
+ delete panel.explicitInput.savedObjectId;
+ delete panel.explicitInput.type;
+ }
+
+ const { state: panelState, references: panelReferences } = persistableStateService.extract({
+ ...panel.explicitInput,
+ type: panel.type,
+ });
+
+ // We're going to prefix the names of the references so that we don't end up with dupes (from visualizations for instance)
+ const prefixedReferences = panelReferences.map((reference) => ({
+ ...reference,
+ name: `${prefix}${reference.name}`,
+ }));
+
+ references.push(...prefixedReferences);
+
+ const { type, ...restOfState } = panelState;
+ workingState.panels[key].explicitInput = restOfState as EmbeddableInput;
+ }
+ }
+
+ return { state: workingState as EmbeddableStateWithType, references };
+ };
+};
diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts
index 96725d4405112..a06f248eb8125 100644
--- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts
+++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts
@@ -16,6 +16,7 @@ export function convertSavedDashboardPanelToPanelState(
return {
type: savedDashboardPanel.type,
gridData: savedDashboardPanel.gridData,
+ panelRefName: savedDashboardPanel.panelRefName,
explicitInput: {
id: savedDashboardPanel.panelIndex,
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
@@ -38,5 +39,6 @@ export function convertPanelStateToSavedDashboardPanel(
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }),
...(savedObjectId !== undefined && { id: savedObjectId }),
+ ...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
};
}
diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts
index a1d5487eeb244..017b7d804c872 100644
--- a/src/plugins/dashboard/common/index.ts
+++ b/src/plugins/dashboard/common/index.ts
@@ -14,6 +14,7 @@ export {
DashboardDocPre700,
} from './bwc/types';
export {
+ DashboardContainerStateWithType,
SavedDashboardPanelTo60,
SavedDashboardPanel610,
SavedDashboardPanel620,
diff --git a/src/plugins/dashboard/common/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts
index 584d7e5e63a92..9ab0e7b644496 100644
--- a/src/plugins/dashboard/common/saved_dashboard_references.test.ts
+++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts
@@ -12,14 +12,34 @@ import {
InjectDeps,
ExtractDeps,
} from './saved_dashboard_references';
+
+import { createExtract, createInject } from './embeddable/dashboard_container_persistable_state';
import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks';
const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock();
+const dashboardInject = createInject(embeddablePersistableStateServiceMock);
+const dashboardExtract = createExtract(embeddablePersistableStateServiceMock);
+
+embeddablePersistableStateServiceMock.extract.mockImplementation((state) => {
+ if (state.type === 'dashboard') {
+ return dashboardExtract(state);
+ }
+
+ return { state, references: [] };
+});
+
+embeddablePersistableStateServiceMock.inject.mockImplementation((state, references) => {
+ if (state.type === 'dashboard') {
+ return dashboardInject(state, references);
+ }
+
+ return state;
+});
const deps: InjectDeps & ExtractDeps = {
embeddablePersistableStateService: embeddablePersistableStateServiceMock,
};
-describe('extractReferences', () => {
+describe('legacy extract references', () => {
test('extracts references from panelsJSON', () => {
const doc = {
id: '1',
@@ -30,13 +50,13 @@ describe('extractReferences', () => {
type: 'visualization',
id: '1',
title: 'Title 1',
- version: '7.9.1',
+ version: '7.0.0',
},
{
type: 'visualization',
id: '2',
title: 'Title 2',
- version: '7.9.1',
+ version: '7.0.0',
},
]),
},
@@ -48,7 +68,7 @@ describe('extractReferences', () => {
Object {
"attributes": Object {
"foo": true,
- "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"version\\":\\"7.9.1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
+ "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"version\\":\\"7.0.0\\",\\"panelRefName\\":\\"panel_1\\"}]",
},
"references": Array [
Object {
@@ -75,7 +95,7 @@ describe('extractReferences', () => {
{
id: '1',
title: 'Title 1',
- version: '7.9.1',
+ version: '7.0.0',
},
]),
},
@@ -186,6 +206,102 @@ describe('extractReferences', () => {
});
});
+describe('extractReferences', () => {
+ test('extracts references from panelsJSON', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ panelIndex: 'panel-1',
+ type: 'visualization',
+ id: '1',
+ title: 'Title 1',
+ version: '7.9.1',
+ },
+ {
+ panelIndex: 'panel-2',
+ type: 'visualization',
+ id: '2',
+ title: 'Title 2',
+ version: '7.9.1',
+ },
+ ]),
+ },
+ references: [],
+ };
+ const updatedDoc = extractReferences(doc, deps);
+
+ expect(updatedDoc).toMatchInlineSnapshot(`
+ Object {
+ "attributes": Object {
+ "foo": true,
+ "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_panel-1\\"},{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-2\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_panel-2\\"}]",
+ },
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel-1:panel_panel-1",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel-2:panel_panel-2",
+ "type": "visualization",
+ },
+ ],
+ }
+ `);
+ });
+
+ test('fails when "type" attribute is missing from a panel', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ id: '1',
+ title: 'Title 1',
+ version: '7.9.1',
+ },
+ ]),
+ },
+ references: [],
+ };
+ expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
+ `"\\"type\\" attribute is missing from panel \\"0\\""`
+ );
+ });
+
+ test('passes when "id" attribute is missing from a panel', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ type: 'visualization',
+ title: 'Title 1',
+ version: '7.9.1',
+ },
+ ]),
+ },
+ references: [],
+ };
+ expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
+ Object {
+ "attributes": Object {
+ "foo": true,
+ "panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
+ },
+ "references": Array [],
+ }
+ `);
+ });
+});
+
describe('injectReferences', () => {
test('returns injected attributes', () => {
const attributes = {
@@ -195,10 +311,12 @@ describe('injectReferences', () => {
{
panelRefName: 'panel_0',
title: 'Title 1',
+ version: '7.9.0',
},
{
panelRefName: 'panel_1',
title: 'Title 2',
+ version: '7.9.0',
},
]),
};
@@ -219,7 +337,7 @@ describe('injectReferences', () => {
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"id": "1",
- "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
+ "panelsJSON": "[{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
"title": "test",
}
`);
@@ -280,7 +398,7 @@ describe('injectReferences', () => {
expect(newAttributes).toMatchInlineSnapshot(`
Object {
"id": "1",
- "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
+ "panelsJSON": "[{\\"version\\":\\"\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
"title": "test",
}
`);
diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts
index f1fea99057f83..16ab470ce7d6f 100644
--- a/src/plugins/dashboard/common/saved_dashboard_references.ts
+++ b/src/plugins/dashboard/common/saved_dashboard_references.ts
@@ -8,22 +8,71 @@
import semverSatisfies from 'semver/functions/satisfies';
import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types';
-import {
- extractPanelsReferences,
- injectPanelsReferences,
-} from './embeddable/embeddable_references';
-import { SavedDashboardPanel730ToLatest } from './types';
+import { DashboardContainerStateWithType, DashboardPanelState } from './types';
import { EmbeddablePersistableStateService } from '../../embeddable/common/types';
-
+import {
+ convertPanelStateToSavedDashboardPanel,
+ convertSavedDashboardPanelToPanelState,
+} from './embeddable/embeddable_saved_object_converters';
+import { SavedDashboardPanel } from './types';
export interface ExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
}
-
export interface SavedObjectAttributesAndReferences {
attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}
+const isPre730Panel = (panel: Record): boolean => {
+ return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true;
+};
+
+function dashboardAttributesToState(
+ attributes: SavedObjectAttributes
+): {
+ state: DashboardContainerStateWithType;
+ panels: SavedDashboardPanel[];
+} {
+ let inputPanels = [] as SavedDashboardPanel[];
+ if (typeof attributes.panelsJSON === 'string') {
+ inputPanels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[];
+ }
+
+ return {
+ panels: inputPanels,
+ state: {
+ id: attributes.id as string,
+ type: 'dashboard',
+ panels: inputPanels.reduce>((current, panel, index) => {
+ const panelIndex = panel.panelIndex || `${index}`;
+ current[panelIndex] = convertSavedDashboardPanelToPanelState(panel);
+ return current;
+ }, {}),
+ },
+ };
+}
+
+function panelStatesToPanels(
+ panelStates: DashboardContainerStateWithType['panels'],
+ originalPanels: SavedDashboardPanel[]
+): SavedDashboardPanel[] {
+ return Object.entries(panelStates).map(([id, panelState]) => {
+ // Find matching original panel to get the version
+ let originalPanel = originalPanels.find((p) => p.panelIndex === id);
+
+ if (!originalPanel) {
+ // Maybe original panel doesn't have a panel index and it's just straight up based on it's index
+ const numericId = parseInt(id, 10);
+ originalPanel = isNaN(numericId) ? originalPanel : originalPanels[numericId];
+ }
+
+ return convertPanelStateToSavedDashboardPanel(
+ panelState,
+ originalPanel?.version ? originalPanel.version : ''
+ );
+ });
+}
+
export function extractReferences(
{ attributes, references = [] }: SavedObjectAttributesAndReferences,
deps: ExtractDeps
@@ -31,64 +80,33 @@ export function extractReferences(
if (typeof attributes.panelsJSON !== 'string') {
return { attributes, references };
}
- const panelReferences: SavedObjectReference[] = [];
- let panels: Array> = JSON.parse(String(attributes.panelsJSON));
- const isPre730Panel = (panel: Record): boolean => {
- return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true;
- };
+ const { panels, state } = dashboardAttributesToState(attributes);
- const hasPre730Panel = panels.some(isPre730Panel);
-
- /**
- * `extractPanelsReferences` only knows how to reliably handle "latest" panels
- * It is possible that `extractReferences` is run on older dashboard SO with older panels,
- * for example, when importing a saved object using saved object UI `extractReferences` is called BEFORE any server side migrations are run.
- *
- * In this case we skip running `extractPanelsReferences` on such object.
- * We also know that there is nothing to extract
- * (First possible entity to be extracted by this mechanism is a dashboard drilldown since 7.11)
- */
- if (!hasPre730Panel) {
- const extractedReferencesResult = extractPanelsReferences(
- // it is ~safe~ to cast to `SavedDashboardPanel730ToLatest` because above we've checked that there are only >=7.3 panels
- (panels as unknown) as SavedDashboardPanel730ToLatest[],
- deps
- );
+ if (((panels as unknown) as Array>).some(isPre730Panel)) {
+ return pre730ExtractReferences({ attributes, references }, deps);
+ }
- panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array<
- Record
- >;
- extractedReferencesResult.forEach((res) => {
- panelReferences.push(...res.references);
- });
+ const missingTypeIndex = panels.findIndex((panel) => panel.type === undefined);
+ if (missingTypeIndex >= 0) {
+ throw new Error(`"type" attribute is missing from panel "${missingTypeIndex}"`);
}
- // TODO: This extraction should be done by EmbeddablePersistableStateService
- // https://github.com/elastic/kibana/issues/82830
- panels.forEach((panel, i) => {
- if (!panel.type) {
- throw new Error(`"type" attribute is missing from panel "${i}"`);
- }
- if (!panel.id) {
- // Embeddables are not required to be backed off a saved object.
- return;
- }
- panel.panelRefName = `panel_${i}`;
- panelReferences.push({
- name: `panel_${i}`,
- type: panel.type,
- id: panel.id,
- });
- delete panel.type;
- delete panel.id;
- });
+ const {
+ state: extractedState,
+ references: extractedReferences,
+ } = deps.embeddablePersistableStateService.extract(state);
+
+ const extractedPanels = panelStatesToPanels(
+ (extractedState as DashboardContainerStateWithType).panels,
+ panels
+ );
return {
- references: [...references, ...panelReferences],
+ references: [...references, ...extractedReferences],
attributes: {
...attributes,
- panelsJSON: JSON.stringify(panels),
+ panelsJSON: JSON.stringify(extractedPanels),
},
};
}
@@ -107,33 +125,60 @@ export function injectReferences(
if (typeof attributes.panelsJSON !== 'string') {
return attributes;
}
- let panels = JSON.parse(attributes.panelsJSON);
+ const parsedPanels = JSON.parse(attributes.panelsJSON);
// Same here, prevent failing saved object import if ever panels aren't an array.
- if (!Array.isArray(panels)) {
+ if (!Array.isArray(parsedPanels)) {
return attributes;
}
- // TODO: This injection should be done by EmbeddablePersistableStateService
- // https://github.com/elastic/kibana/issues/82830
- panels.forEach((panel) => {
- if (!panel.panelRefName) {
- return;
+ const { panels, state } = dashboardAttributesToState(attributes);
+
+ const injectedState = deps.embeddablePersistableStateService.inject(state, references);
+ const injectedPanels = panelStatesToPanels(
+ (injectedState as DashboardContainerStateWithType).panels,
+ panels
+ );
+
+ return {
+ ...attributes,
+ panelsJSON: JSON.stringify(injectedPanels),
+ };
+}
+
+function pre730ExtractReferences(
+ { attributes, references = [] }: SavedObjectAttributesAndReferences,
+ deps: ExtractDeps
+): SavedObjectAttributesAndReferences {
+ if (typeof attributes.panelsJSON !== 'string') {
+ return { attributes, references };
+ }
+ const panelReferences: SavedObjectReference[] = [];
+ const panels: Array> = JSON.parse(String(attributes.panelsJSON));
+
+ panels.forEach((panel, i) => {
+ if (!panel.type) {
+ throw new Error(`"type" attribute is missing from panel "${i}"`);
}
- const reference = references.find((ref) => ref.name === panel.panelRefName);
- if (!reference) {
- // Throw an error since "panelRefName" means the reference exists within
- // "references" and in this scenario we have bad data.
- throw new Error(`Could not find reference "${panel.panelRefName}"`);
+ if (!panel.id) {
+ // Embeddables are not required to be backed off a saved object.
+ return;
}
- panel.id = reference.id;
- panel.type = reference.type;
- delete panel.panelRefName;
- });
- panels = injectPanelsReferences(panels, references, deps);
+ panel.panelRefName = `panel_${i}`;
+ panelReferences.push({
+ name: `panel_${i}`,
+ type: panel.type,
+ id: panel.id,
+ });
+ delete panel.type;
+ delete panel.id;
+ });
return {
- ...attributes,
- panelsJSON: JSON.stringify(panels),
+ references: [...references, ...panelReferences],
+ attributes: {
+ ...attributes,
+ panelsJSON: JSON.stringify(panels),
+ },
};
}
diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts
index c8ef3c81662c7..9a6d185ef2ac1 100644
--- a/src/plugins/dashboard/common/types.ts
+++ b/src/plugins/dashboard/common/types.ts
@@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
-import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types';
+import {
+ EmbeddableInput,
+ EmbeddableStateWithType,
+ PanelState,
+} from '../../../../src/plugins/embeddable/common/types';
import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable';
import {
RawSavedDashboardPanelTo60,
@@ -25,6 +29,7 @@ export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState {
readonly gridData: GridData;
+ panelRefName?: string;
}
/**
@@ -80,3 +85,11 @@ export type SavedDashboardPanel730ToLatest = Pick<
readonly id?: string;
readonly type: string;
};
+
+// Making this interface because so much of the Container type from embeddable is tied up in public
+// Once that is all available from common, we should be able to move the dashboard_container type to our common as well
+export interface DashboardContainerStateWithType extends EmbeddableStateWithType {
+ panels: {
+ [panelId: string]: DashboardPanelState;
+ };
+}
diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx
index 6501f92689d17..9b93f0bbd0711 100644
--- a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx
@@ -7,6 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
import {
Container,
ErrorEmbeddable,
@@ -20,6 +21,10 @@ import {
DashboardContainerServices,
} from './dashboard_container';
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
+import {
+ createExtract,
+ createInject,
+} from '../../../common/embeddable/dashboard_container_persistable_state';
export type DashboardContainerFactory = EmbeddableFactory<
DashboardContainerInput,
@@ -32,7 +37,10 @@ export class DashboardContainerFactoryDefinition
public readonly isContainerType = true;
public readonly type = DASHBOARD_CONTAINER_TYPE;
- constructor(private readonly getStartServices: () => Promise) {}
+ constructor(
+ private readonly getStartServices: () => Promise,
+ private readonly persistableStateService: EmbeddablePersistableStateService
+ ) {}
public isEditable = async () => {
// Currently unused for dashboards
@@ -62,4 +70,8 @@ export class DashboardContainerFactoryDefinition
const services = await this.getStartServices();
return new DashboardContainer(initialInput, services, parent);
};
+
+ public inject = createInject(this.persistableStateService);
+
+ public extract = createExtract(this.persistableStateService);
}
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 5bf730996ab4f..e2f52a47455b3 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -121,9 +121,11 @@ export type DashboardSetup = void;
export interface DashboardStart {
getSavedDashboardLoader: () => SavedObjectLoader;
+ getDashboardContainerByValueRenderer: () => ReturnType<
+ typeof createDashboardContainerByValueRenderer
+ >;
dashboardUrlGenerator?: DashboardUrlGenerator;
dashboardFeatureFlagConfig: DashboardFeatureFlagConfig;
- DashboardContainerByValueRenderer: ReturnType;
}
export class DashboardPlugin
@@ -260,8 +262,16 @@ export class DashboardPlugin
},
});
- const dashboardContainerFactory = new DashboardContainerFactoryDefinition(getStartServices);
- embeddable.registerEmbeddableFactory(dashboardContainerFactory.type, dashboardContainerFactory);
+ getStartServices().then((coreStart) => {
+ const dashboardContainerFactory = new DashboardContainerFactoryDefinition(
+ getStartServices,
+ coreStart.embeddable
+ );
+ embeddable.registerEmbeddableFactory(
+ dashboardContainerFactory.type,
+ dashboardContainerFactory
+ );
+ });
const placeholderFactory = new PlaceholderEmbeddableFactory();
embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory);
@@ -403,17 +413,24 @@ export class DashboardPlugin
savedObjects: plugins.savedObjects,
embeddableStart: plugins.embeddable,
});
- const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
- DASHBOARD_CONTAINER_TYPE
- )! as DashboardContainerFactory;
return {
getSavedDashboardLoader: () => savedDashboardLoader,
+ getDashboardContainerByValueRenderer: () => {
+ const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
+ DASHBOARD_CONTAINER_TYPE
+ );
+
+ if (!dashboardContainerFactory) {
+ throw new Error(`${DASHBOARD_CONTAINER_TYPE} Embeddable Factory not found`);
+ }
+
+ return createDashboardContainerByValueRenderer({
+ factory: dashboardContainerFactory as DashboardContainerFactory,
+ });
+ },
dashboardUrlGenerator: this.dashboardUrlGenerator,
dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!,
- DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({
- factory: dashboardContainerFactory,
- }),
};
}
diff --git a/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts
new file mode 100644
index 0000000000000..995731341739a
--- /dev/null
+++ b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
+import { EmbeddableRegistryDefinition } from '../../../embeddable/server';
+import {
+ createExtract,
+ createInject,
+} from '../../common/embeddable/dashboard_container_persistable_state';
+
+export const dashboardPersistableStateServiceFactory = (
+ persistableStateService: EmbeddablePersistableStateService
+): EmbeddableRegistryDefinition => {
+ return {
+ id: 'dashboard',
+ extract: createExtract(persistableStateService),
+ inject: createInject(persistableStateService),
+ };
+};
diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts
index 020ecfeaa9239..3aeaf31c190bd 100644
--- a/src/plugins/dashboard/server/plugin.ts
+++ b/src/plugins/dashboard/server/plugin.ts
@@ -18,24 +18,29 @@ import { createDashboardSavedObjectType } from './saved_objects';
import { capabilitiesProvider } from './capabilities_provider';
import { DashboardPluginSetup, DashboardPluginStart } from './types';
-import { EmbeddableSetup } from '../../embeddable/server';
+import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/server';
import { UsageCollectionSetup } from '../../usage_collection/server';
import { registerDashboardUsageCollector } from './usage/register_collector';
+import { dashboardPersistableStateServiceFactory } from './embeddable/dashboard_container_embeddable_factory';
interface SetupDeps {
embeddable: EmbeddableSetup;
usageCollection: UsageCollectionSetup;
}
+interface StartDeps {
+ embeddable: EmbeddableStart;
+}
+
export class DashboardPlugin
- implements Plugin {
+ implements Plugin {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
- public setup(core: CoreSetup, plugins: SetupDeps) {
+ public setup(core: CoreSetup, plugins: SetupDeps) {
this.logger.debug('dashboard: Setup');
core.savedObjects.registerType(
@@ -48,6 +53,15 @@ export class DashboardPlugin
core.capabilities.registerProvider(capabilitiesProvider);
registerDashboardUsageCollector(plugins.usageCollection, plugins.embeddable);
+
+ (async () => {
+ const [, startPlugins] = await core.getStartServices();
+
+ plugins.embeddable.registerEmbeddableFactory(
+ dashboardPersistableStateServiceFactory(startPlugins.embeddable)
+ );
+ })();
+
return {};
}
diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
index e2949847bc926..9671a8d847c0a 100644
--- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
+++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
@@ -6,13 +6,39 @@
* Side Public License, v 1.
*/
-import { SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { SavedObjectReference, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks';
import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations';
import { DashboardDoc730ToLatest } from '../../common';
+import {
+ createExtract,
+ createInject,
+} from '../../common/embeddable/dashboard_container_persistable_state';
+import { EmbeddableStateWithType } from 'src/plugins/embeddable/common';
const embeddableSetupMock = createEmbeddableSetupMock();
+const extract = createExtract(embeddableSetupMock);
+const inject = createInject(embeddableSetupMock);
+const extractImplementation = (state: EmbeddableStateWithType) => {
+ if (state.type === 'dashboard') {
+ return extract(state);
+ }
+ return { state, references: [] };
+};
+const injectImplementation = (
+ state: EmbeddableStateWithType,
+ references: SavedObjectReference[]
+) => {
+ if (state.type === 'dashboard') {
+ return inject(state, references);
+ }
+
+ return state;
+};
+embeddableSetupMock.extract.mockImplementation(extractImplementation);
+embeddableSetupMock.inject.mockImplementation(injectImplementation);
+
const migrations = createDashboardSavedObjectTypeMigrations({
embeddable: embeddableSetupMock,
});
@@ -25,10 +51,10 @@ describe('dashboard', () => {
test('skips error on empty object', () => {
expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "references": Array [],
-}
-`);
+ Object {
+ "references": Array [],
+ }
+ `);
});
test('skips errors when searchSourceJSON is null', () => {
@@ -45,29 +71,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": null,
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": null,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('skips errors when searchSourceJSON is undefined', () => {
@@ -84,29 +110,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": undefined,
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": undefined,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('skips error when searchSourceJSON is not a string', () => {
@@ -122,29 +148,29 @@ Object {
},
};
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": 123,
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": 123,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('skips error when searchSourceJSON is invalid json', () => {
@@ -160,29 +186,29 @@ Object {
},
};
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{abc123}",
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{abc123}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('skips error when "index" and "filter" is missing from searchSourceJSON', () => {
@@ -199,29 +225,29 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true}",
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('extracts "index" attribute from doc', () => {
@@ -238,34 +264,34 @@ Object {
};
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "pattern*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern",
- },
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('extracts index patterns from filter', () => {
@@ -293,34 +319,34 @@ Object {
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
- },
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "my-index",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
- "type": "index-pattern",
- },
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
- "type": "dashboard",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "my-index",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('skips error when panelsJSON is not a string', () => {
@@ -331,14 +357,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": 123,
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": 123,
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
test('skips error when panelsJSON is not valid JSON', () => {
@@ -349,14 +375,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": "{123abc}",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": "{123abc}",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
test('skips panelsJSON when its not an array', () => {
@@ -367,14 +393,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": "{}",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": "{}",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
test('skips error when a panel is missing "type" attribute', () => {
@@ -385,14 +411,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": "[{\\"id\\":\\"123\\"}]",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"id\\":\\"123\\"}]",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
test('skips error when a panel is missing "id" attribute', () => {
@@ -403,14 +429,14 @@ Object {
},
} as SavedObjectUnsanitizedDoc;
expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
test('extract panel references from doc', () => {
@@ -423,25 +449,25 @@ Object {
} as SavedObjectUnsanitizedDoc;
const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "1",
- "name": "panel_0",
- "type": "visualization",
- },
- Object {
- "id": "2",
- "name": "panel_1",
- "type": "visualization",
- },
- ],
-}
-`);
+ Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ }
+ `);
});
});
@@ -475,19 +501,57 @@ Object {
test('should migrate 7.3.0 doc without embeddable state to extract', () => {
const newDoc = migration(doc, contextMock);
- expect(newDoc).toEqual(doc);
+ expect(newDoc).toMatchInlineSnapshot(`
+ Object {
+ "attributes": Object {
+ "description": "",
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"},\\"filter\\":[{\\"query\\":{\\"match_phrase\\":{\\"machine.os.keyword\\":\\"osx\\"}},\\"$state\\":{\\"store\\":\\"appState\\"},\\"meta\\":{\\"type\\":\\"phrase\\",\\"key\\":\\"machine.os.keyword\\",\\"params\\":{\\"query\\":\\"osx\\"},\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
+ },
+ "optionsJSON": "{\\"useMargins\\":true,\\"hidePanelTitles\\":false}",
+ "panelsJSON": "[{\\"version\\":\\"7.9.3\\",\\"type\\":\\"visualization\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\"},\\"panelIndex\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"embeddableConfig\\":{\\"enhancements\\":{\\"dynamicActions\\":{\\"events\\":[]}}},\\"panelRefName\\":\\"panel_82fa0882-9f9e-476a-bbb9-03555e5ced91\\"}]",
+ "timeRestore": false,
+ "title": "Dashboard A",
+ "version": 1,
+ },
+ "id": "376e6260-1f5e-11eb-91aa-7b6d5f8a61d6",
+ "references": Array [
+ Object {
+ "id": "90943e30-9a47-11e8-b64d-95841ca0b247",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "14e2e710-4258-11e8-b3aa-73fdaf54bfc9",
+ "name": "82fa0882-9f9e-476a-bbb9-03555e5ced91:panel_82fa0882-9f9e-476a-bbb9-03555e5ced91",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ }
+ `);
});
test('should migrate 7.3.0 doc and extract embeddable state', () => {
- embeddableSetupMock.extract.mockImplementationOnce((state) => ({
- state: { ...state, __extracted: true },
- references: [{ id: '__new', name: '__newRefName', type: '__newType' }],
- }));
+ embeddableSetupMock.extract.mockImplementation((state) => {
+ const stateAndReferences = extractImplementation(state);
+ const { references } = stateAndReferences;
+ let { state: newState } = stateAndReferences;
+
+ if (state.enhancements !== undefined && Object.keys(state.enhancements).length !== 0) {
+ newState = { ...state, __extracted: true } as any;
+ references.push({ id: '__new', name: '__newRefName', type: '__newType' });
+ }
+
+ return { state: newState, references };
+ });
const newDoc = migration(doc, contextMock);
expect(newDoc).not.toEqual(doc);
expect(newDoc.references).toHaveLength(doc.references.length + 1);
expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true);
+
+ embeddableSetupMock.extract.mockImplementation(extractImplementation);
});
});
});
diff --git a/src/plugins/embeddable/server/index.ts b/src/plugins/embeddable/server/index.ts
index 33eaaca9dd69b..aac081f9467b6 100644
--- a/src/plugins/embeddable/server/index.ts
+++ b/src/plugins/embeddable/server/index.ts
@@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
-import { EmbeddableServerPlugin, EmbeddableSetup } from './plugin';
+import { EmbeddableServerPlugin, EmbeddableSetup, EmbeddableStart } from './plugin';
-export { EmbeddableSetup };
+export { EmbeddableSetup, EmbeddableStart };
export { EnhancementRegistryDefinition, EmbeddableRegistryDefinition } from './types';
diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md
index d3921ab11457c..5c7efec57e93b 100644
--- a/src/plugins/embeddable/server/server.api.md
+++ b/src/plugins/embeddable/server/server.api.md
@@ -29,6 +29,11 @@ export interface EmbeddableSetup extends PersistableStateService void;
}
+// Warning: (ae-missing-release-tag) "EmbeddableStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export type EmbeddableStart = PersistableStateService;
+
// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts
index 87cdf5a8b0c46..c02ce76340da8 100644
--- a/test/api_integration/apis/saved_objects/export.ts
+++ b/test/api_integration/apis/saved_objects/export.ts
@@ -324,7 +324,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
- name: 'panel_0',
+ name: '1:panel_1',
type: 'visualization',
},
],
@@ -384,7 +384,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
- name: 'panel_0',
+ name: '1:panel_1',
type: 'visualization',
},
],
@@ -449,7 +449,7 @@ export default function ({ getService }: FtrProviderContext) {
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
- name: 'panel_0',
+ name: '1:panel_1',
type: 'visualization',
},
],
From 7e2ffc054e532fcbd47029fdc4eed78cf6650269 Mon Sep 17 00:00:00 2001
From: Larry Gregory
Date: Mon, 12 Apr 2021 12:27:56 -0400
Subject: [PATCH 21/79] RFC: Object level security, Phase 1 (#93115)
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
---
rfcs/images/ols_phase_1_auth.png | Bin 0 -> 252971 bytes
rfcs/text/0016_ols_phase_1.md | 323 +++++++++++++++++++++++++++++++
2 files changed, 323 insertions(+)
create mode 100644 rfcs/images/ols_phase_1_auth.png
create mode 100644 rfcs/text/0016_ols_phase_1.md
diff --git a/rfcs/images/ols_phase_1_auth.png b/rfcs/images/ols_phase_1_auth.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bf4b210bee9e9fd82155c006835a3076c8086f4
GIT binary patch
literal 252971
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 |