From edf16e60126427f5101e75b72f046fee7c491065 Mon Sep 17 00:00:00 2001
From: Aleh Zasypkin
Date: Mon, 4 Oct 2021 15:33:21 +0200
Subject: [PATCH 01/14] Remove `jsonwebtoken` and `base64url` dependencies.
(#113723)
---
package.json | 3 --
renovate.json5 | 7 ++--
.../fixtures/oidc/oidc_tools.ts | 41 ++++++++++++-------
yarn.lock | 11 +----
4 files changed, 31 insertions(+), 31 deletions(-)
diff --git a/package.json b/package.json
index ac30b5de6f486..f436a13a057e9 100644
--- a/package.json
+++ b/package.json
@@ -272,7 +272,6 @@
"json-stable-stringify": "^1.0.1",
"json-stringify-pretty-compact": "1.2.0",
"json-stringify-safe": "5.0.1",
- "jsonwebtoken": "^8.5.1",
"jsts": "^1.6.2",
"kea": "^2.4.2",
"load-json-file": "^6.2.0",
@@ -554,7 +553,6 @@
"@types/jsdom": "^16.2.3",
"@types/json-stable-stringify": "^1.0.32",
"@types/json5": "^0.0.30",
- "@types/jsonwebtoken": "^8.5.5",
"@types/license-checker": "15.0.0",
"@types/listr": "^0.14.0",
"@types/loader-utils": "^1.1.3",
@@ -662,7 +660,6 @@
"babel-plugin-styled-components": "^1.13.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"backport": "^5.6.6",
- "base64url": "^3.0.1",
"callsites": "^3.1.0",
"chai": "3.5.0",
"chance": "1.0.18",
diff --git a/renovate.json5 b/renovate.json5
index 12a30876291da..dea7d311bae16 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -86,10 +86,9 @@
{
groupName: 'platform security modules',
packageNames: [
- 'broadcast-channel',
- 'jsonwebtoken', '@types/jsonwebtoken',
- 'node-forge', '@types/node-forge',
- 'require-in-the-middle',
+ 'broadcast-channel',
+ 'node-forge', '@types/node-forge',
+ 'require-in-the-middle',
'tough-cookie', '@types/tough-cookie',
'xml-crypto', '@types/xml-crypto'
],
diff --git a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
index 8d078994eb0e9..3db2e2ebdce0f 100644
--- a/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
+++ b/x-pack/test/security_api_integration/fixtures/oidc/oidc_tools.ts
@@ -5,10 +5,8 @@
* 2.0.
*/
-import base64url from 'base64url';
-import { createHash } from 'crypto';
+import { createHash, createSign } from 'crypto';
import fs from 'fs';
-import jwt from 'jsonwebtoken';
import url from 'url';
export function getStateAndNonce(urlWithStateAndNonce: string) {
@@ -16,16 +14,20 @@ export function getStateAndNonce(urlWithStateAndNonce: string) {
return { state: parsedQuery.state as string, nonce: parsedQuery.nonce as string };
}
+function fromBase64(base64: string) {
+ return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
+}
+
export function createTokens(userId: string, nonce: string) {
- const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem'));
- const iat = Math.floor(Date.now() / 1000);
+ const idTokenHeader = fromBase64(
+ Buffer.from(JSON.stringify({ alg: 'RS256' })).toString('base64')
+ );
+ const iat = Math.floor(Date.now() / 1000);
const accessToken = `valid-access-token${userId}`;
const accessTokenHashBuffer = createHash('sha256').update(accessToken).digest();
-
- return {
- accessToken,
- idToken: jwt.sign(
+ const idTokenBody = fromBase64(
+ Buffer.from(
JSON.stringify({
iss: 'https://test-op.elastic.co',
sub: `user${userId}`,
@@ -34,10 +36,19 @@ export function createTokens(userId: string, nonce: string) {
exp: iat + 3600,
iat,
// See more details on `at_hash` at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
- at_hash: base64url(accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2)),
- }),
- signingKey,
- { algorithm: 'RS256' }
- ),
- };
+ at_hash: fromBase64(
+ accessTokenHashBuffer.slice(0, accessTokenHashBuffer.length / 2).toString('base64')
+ ),
+ })
+ ).toString('base64')
+ );
+
+ const idToken = `${idTokenHeader}.${idTokenBody}`;
+
+ const signingKey = fs.readFileSync(require.resolve('./jwks_private.pem'));
+ const idTokenSignature = fromBase64(
+ createSign('RSA-SHA256').update(idToken).sign(signingKey, 'base64')
+ );
+
+ return { accessToken, idToken: `${idToken}.${idTokenSignature}` };
}
diff --git a/yarn.lock b/yarn.lock
index 14cf34cae847b..83c0691646817 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6394,13 +6394,6 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818"
integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==
-"@types/jsonwebtoken@^8.5.5":
- version "8.5.5"
- resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz#da5f2f4baee88f052ef3e4db4c1a0afb46cff22c"
- integrity sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw==
- dependencies:
- "@types/node" "*"
-
"@types/keyv@*":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
@@ -9235,7 +9228,7 @@ base64-js@^1.0.2, base64-js@^1.1.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-j
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
-base64url@^3.0.0, base64url@^3.0.1:
+base64url@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
@@ -19171,7 +19164,7 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
-jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1:
+jsonwebtoken@^8.3.0:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
From 163133827b894468cb9dce4ed0a4aad493aff1f2 Mon Sep 17 00:00:00 2001
From: Kevin Lacabane
Date: Mon, 4 Oct 2021 15:57:47 +0200
Subject: [PATCH 02/14] [Stack Monitoring] React migration kibana overview
(#113604)
* Create react Kibana template
* React Kibana overview
* Add breadcrumb to kibana overview
* fix linting errors
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/public/application/index.tsx | 11 +-
.../pages/kibana/kibana_template.tsx | 30 +++++
.../application/pages/kibana/overview.tsx | 119 ++++++++++++++++++
3 files changed, 159 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/monitoring/public/application/pages/kibana/kibana_template.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index dea8d18bb65b1..acdf3b0986a64 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -23,7 +23,8 @@ import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview';
import { BeatsOverviewPage } from './pages/beats/overview';
import { BeatsInstancesPage } from './pages/beats/instances';
import { BeatsInstancePage } from './pages/beats/instance';
-import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants';
+import { KibanaOverviewPage } from './pages/kibana/overview';
+import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS, CODE_PATH_KIBANA } from '../../common/constants';
import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page';
import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page';
import { ElasticsearchNodePage } from './pages/elasticsearch/node_page';
@@ -133,6 +134,14 @@ const MonitoringApp: React.FC<{
fetchAllClusters={false}
/>
+ {/* Kibana Views */}
+
+
= ({ ...props }) => {
+ const tabs: TabMenuItem[] = [
+ {
+ id: 'overview',
+ label: i18n.translate('xpack.monitoring.kibanaNavigation.overviewLinkText', {
+ defaultMessage: 'Overview',
+ }),
+ route: '/kibana',
+ },
+ {
+ id: 'instances',
+ label: i18n.translate('xpack.monitoring.kibanaNavigation.instancesLinkText', {
+ defaultMessage: 'Instances',
+ }),
+ route: '/kibana/instances',
+ },
+ ];
+
+ return ;
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
new file mode 100644
index 0000000000000..2356011a3f77b
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useCallback, useContext, useEffect, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { find } from 'lodash';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPanel,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { KibanaTemplate } from './kibana_template';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { GlobalStateContext } from '../../global_state_context';
+import { ComponentProps } from '../../route_init';
+// @ts-ignore
+import { MonitoringTimeseriesContainer } from '../../../components/chart';
+// @ts-ignore
+import { ClusterStatus } from '../../../components/kibana/cluster_status';
+import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
+import { useCharts } from '../../hooks/use_charts';
+
+const KibanaOverview = ({ data }: { data: any }) => {
+ const { zoomInfo, onBrush } = useCharts();
+
+ if (!data) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const KibanaOverviewPage: React.FC = ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context);
+ const [data, setData] = useState();
+ const clusterUuid = globalState.cluster_uuid;
+ const cluster = find(clusters, {
+ cluster_uuid: clusterUuid,
+ }) as any;
+ const ccs = globalState.ccs;
+ const title = i18n.translate('xpack.monitoring.kibana.overview.title', {
+ defaultMessage: 'Kibana',
+ });
+ const pageTitle = i18n.translate('xpack.monitoring.kibana.overview.pageTitle', {
+ defaultMessage: 'Kibana overview',
+ });
+
+ useEffect(() => {
+ if (cluster) {
+ generateBreadcrumbs(cluster.cluster_name, {
+ inKibana: true,
+ });
+ }
+ }, [cluster, generateBreadcrumbs]);
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana`;
+
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ ccs,
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ }),
+ });
+
+ setData(response);
+ }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]);
+
+ return (
+
+
+
+ );
+};
From c868cd5c812a82f72fa21b3a4cd261160c910d39 Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Mon, 4 Oct 2021 16:00:08 +0200
Subject: [PATCH 03/14] [Discover] Extract fetch observable initialization to
separate function (#108831)
* Don't trigger autorefresh when there's no time picker
- because there's no UI for that
* Refactor and add test
* Add doc and test
* Refactor
* Remove index pattern without timefield filtering
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../apps/main/services/use_saved_search.ts | 43 ++++-----
.../apps/main/utils/get_fetch_observable.ts | 61 ++++++++++++
.../main/utils/get_fetch_observeable.test.ts | 95 +++++++++++++++++++
3 files changed, 172 insertions(+), 27 deletions(-)
create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
index 164dff8627790..26f95afba5a93 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
@@ -6,15 +6,14 @@
* Side Public License, v 1.
*/
import { useCallback, useEffect, useMemo, useRef } from 'react';
-import { BehaviorSubject, merge, Subject } from 'rxjs';
-import { debounceTime, filter, tap } from 'rxjs/operators';
+import { BehaviorSubject, Subject } from 'rxjs';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverSearchSessionManager } from './discover_search_session';
import { SearchSource } from '../../../../../../data/common';
import { GetStateReturn } from './discover_state';
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
import { RequestAdapter } from '../../../../../../inspector/public';
-import { AutoRefreshDoneFn } from '../../../../../../data/public';
+import type { AutoRefreshDoneFn } from '../../../../../../data/public';
import { validateTimeRange } from '../utils/validate_time_range';
import { Chart } from '../components/chart/point_series';
import { useSingleton } from '../utils/use_singleton';
@@ -23,6 +22,7 @@ import { FetchStatus } from '../../../types';
import { fetchAll } from '../utils/fetch_all';
import { useBehaviorSubject } from '../utils/use_behavior_subject';
import { sendResetMsg } from './use_saved_search_messages';
+import { getFetch$ } from '../utils/get_fetch_observable';
export interface SavedSearchData {
main$: DataMain$;
@@ -134,6 +134,7 @@ export const useSavedSearch = ({
*/
const refs = useRef<{
abortController?: AbortController;
+ autoRefreshDone?: AutoRefreshDoneFn;
}>({});
/**
@@ -145,29 +146,17 @@ export const useSavedSearch = ({
* handler emitted by `timefilter.getAutoRefreshFetch$()`
* to notify when data completed loading and to start a new autorefresh loop
*/
- let autoRefreshDoneCb: AutoRefreshDoneFn | undefined;
- const fetch$ = merge(
+ const setAutoRefreshDone = (fn: AutoRefreshDoneFn | undefined) => {
+ refs.current.autoRefreshDone = fn;
+ };
+ const fetch$ = getFetch$({
+ setAutoRefreshDone,
+ data,
+ main$,
refetch$,
- filterManager.getFetches$(),
- timefilter.getFetch$(),
- timefilter.getAutoRefreshFetch$().pipe(
- tap((done) => {
- autoRefreshDoneCb = done;
- }),
- filter(() => {
- /**
- * filter to prevent auto-refresh triggered fetch when
- * loading is still ongoing
- */
- const currentFetchStatus = main$.getValue().fetchStatus;
- return (
- currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL
- );
- })
- ),
- data.query.queryString.getUpdates$(),
- searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
- ).pipe(debounceTime(100));
+ searchSessionManager,
+ searchSource,
+ });
const subscription = fetch$.subscribe((val) => {
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
@@ -190,8 +179,8 @@ export const useSavedSearch = ({
}).subscribe({
complete: () => {
// if this function was set and is executed, another refresh fetch can be triggered
- autoRefreshDoneCb?.();
- autoRefreshDoneCb = undefined;
+ refs.current.autoRefreshDone?.();
+ refs.current.autoRefreshDone = undefined;
},
});
} catch (error) {
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
new file mode 100644
index 0000000000000..aac6196e64f6f
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { merge } from 'rxjs';
+import { debounceTime, filter, tap } from 'rxjs/operators';
+
+import { FetchStatus } from '../../../types';
+import type {
+ AutoRefreshDoneFn,
+ DataPublicPluginStart,
+ SearchSource,
+} from '../../../../../../data/public';
+import { DataMain$, DataRefetch$ } from '../services/use_saved_search';
+import { DiscoverSearchSessionManager } from '../services/discover_search_session';
+
+/**
+ * This function returns an observable that's used to trigger data fetching
+ */
+export function getFetch$({
+ setAutoRefreshDone,
+ data,
+ main$,
+ refetch$,
+ searchSessionManager,
+}: {
+ setAutoRefreshDone: (val: AutoRefreshDoneFn | undefined) => void;
+ data: DataPublicPluginStart;
+ main$: DataMain$;
+ refetch$: DataRefetch$;
+ searchSessionManager: DiscoverSearchSessionManager;
+ searchSource: SearchSource;
+}) {
+ const { timefilter } = data.query.timefilter;
+ const { filterManager } = data.query;
+ return merge(
+ refetch$,
+ filterManager.getFetches$(),
+ timefilter.getFetch$(),
+ timefilter.getAutoRefreshFetch$().pipe(
+ tap((done) => {
+ setAutoRefreshDone(done);
+ }),
+ filter(() => {
+ const currentFetchStatus = main$.getValue().fetchStatus;
+ return (
+ /**
+ * filter to prevent auto-refresh triggered fetch when
+ * loading is still ongoing
+ */
+ currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL
+ );
+ })
+ ),
+ data.query.queryString.getUpdates$(),
+ searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
+ ).pipe(debounceTime(100));
+}
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
new file mode 100644
index 0000000000000..5f728b115b2e9
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { getFetch$ } from './get_fetch_observable';
+import { FetchStatus } from '../../../types';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { DataPublicPluginStart } from '../../../../../../data/public';
+import { createSearchSessionMock } from '../../../../__mocks__/search_session';
+import { DataRefetch$ } from '../services/use_saved_search';
+import { savedSearchMock, savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search';
+
+function createDataMock(
+ queryString$: Subject,
+ filterManager$: Subject,
+ timefilterFetch$: Subject,
+ autoRefreshFetch$: Subject
+) {
+ return {
+ query: {
+ queryString: {
+ getUpdates$: () => {
+ return queryString$;
+ },
+ },
+ filterManager: {
+ getFetches$: () => {
+ return filterManager$;
+ },
+ },
+ timefilter: {
+ timefilter: {
+ getFetch$: () => {
+ return timefilterFetch$;
+ },
+ getAutoRefreshFetch$: () => {
+ return autoRefreshFetch$;
+ },
+ },
+ },
+ },
+ } as unknown as DataPublicPluginStart;
+}
+
+describe('getFetchObservable', () => {
+ test('refetch$.next should trigger fetch$.next', async (done) => {
+ const searchSessionManagerMock = createSearchSessionMock();
+
+ const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED });
+ const refetch$: DataRefetch$ = new Subject();
+ const fetch$ = getFetch$({
+ setAutoRefreshDone: jest.fn(),
+ main$,
+ refetch$,
+ data: createDataMock(new Subject(), new Subject(), new Subject(), new Subject()),
+ searchSessionManager: searchSessionManagerMock.searchSessionManager,
+ searchSource: savedSearchMock.searchSource,
+ });
+
+ fetch$.subscribe(() => {
+ done();
+ });
+ refetch$.next();
+ });
+ test('getAutoRefreshFetch$ should trigger fetch$.next', async () => {
+ jest.useFakeTimers();
+ const searchSessionManagerMock = createSearchSessionMock();
+ const autoRefreshFetch$ = new Subject();
+
+ const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED });
+ const refetch$: DataRefetch$ = new Subject();
+ const dataMock = createDataMock(new Subject(), new Subject(), new Subject(), autoRefreshFetch$);
+ const setAutoRefreshDone = jest.fn();
+ const fetch$ = getFetch$({
+ setAutoRefreshDone,
+ main$,
+ refetch$,
+ data: dataMock,
+ searchSessionManager: searchSessionManagerMock.searchSessionManager,
+ searchSource: savedSearchMockWithTimeField.searchSource,
+ });
+
+ const fetchfnMock = jest.fn();
+ fetch$.subscribe(() => {
+ fetchfnMock();
+ });
+ autoRefreshFetch$.next(jest.fn());
+ jest.runAllTimers();
+ expect(fetchfnMock).toHaveBeenCalledTimes(1);
+ expect(setAutoRefreshDone).toHaveBeenCalled();
+ });
+});
From 1ff02e1da684a5bbd3d1179db9567b5dd2fe4a97 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Mon, 4 Oct 2021 10:05:01 -0400
Subject: [PATCH 04/14] [Observability] [Exploratory View] Add exploratory view
multi series (#113464)
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* [Observability] [Exploratory View] Create multi series feature branch (#108079)
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* Revert "[Observability][Exploratory View] revert exploratory view multi-series (#107647)"
This reverts commit 1649661ffdc79d00f9d23451790335e5d25da25f.
* update types
* update tests
* [Observability] exploratory view design issues (#111028)
* remove custom y axis labels for better clarity
* move add series button to the bottom
* disable auto apply
* fix missing test
* When series count changes, collapse other series. (#110894)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* Feature/observability exploratory view multi series panels (#111555)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* [Exploratory View] Fix date range picker on secondary series (#111700)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* [Exploratory View] Collapse series only on add, not delete (#111790)
* [Exploratory view] Remove preview panel (#111884)
* [Exploratory view] implement popovers for data type and metric type (#112370)
* implement popovers for data type and metric type
* adjust types
* add IncompleteBadge
* make report metric dismissable
* show date-picker even if metric is undefined
* adjust styles of expanded series row
* add truncation to series name
* move incomplete badge and add edit pencil
* add tooltip to data type badge
* adjust content
* lint
* delete extra file
* move filters row
* adjust name editing behavior
* adjust filter styles
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* move cases button to top
* fix types
* more types :(
Co-authored-by: Justin Kambic
Co-authored-by: shahzad31
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Shahzad
---
packages/kbn-test/src/jest/utils/get_url.ts | 9 +-
test/functional/page_objects/common_page.ts | 5 +-
.../app/RumDashboard/ActionMenu/index.tsx | 22 +-
.../PageLoadDistribution/index.tsx | 19 +-
.../app/RumDashboard/PageViewsTrend/index.tsx | 19 +-
.../analyze_data_button.test.tsx | 8 +-
.../analyze_data_button.tsx | 37 +-
.../apm/server/lib/rum_client/has_rum_data.ts | 5 +-
.../add_data_buttons/mobile_add_data.tsx | 32 ++
.../add_data_buttons/synthetics_add_data.tsx | 32 ++
.../shared/add_data_buttons/ux_add_data.tsx | 32 ++
.../action_menu/action_menu.test.tsx | 61 ++++
.../components/action_menu/action_menu.tsx | 98 ++++++
.../components/action_menu/index.tsx | 26 ++
.../date_range_picker.tsx | 63 ++--
.../components/empty_view.tsx | 17 +-
.../components/filter_label.test.tsx | 14 +-
.../components/filter_label.tsx | 11 +-
.../components/series_color_picker.tsx | 65 ++++
.../series_date_picker/index.tsx | 35 +-
.../series_date_picker.test.tsx | 59 ++--
.../configurations/constants/constants.ts | 13 +
.../configurations/constants/url_constants.ts | 4 +-
.../configurations/default_configs.ts | 17 +-
.../configurations/lens_attributes.test.ts | 23 +-
.../configurations/lens_attributes.ts | 45 +--
.../mobile/device_distribution_config.ts | 8 +-
.../mobile/distribution_config.ts | 4 +-
.../mobile/kpi_over_time_config.ts | 10 +-
.../rum/core_web_vitals_config.test.ts | 9 +-
.../rum/core_web_vitals_config.ts | 4 +-
.../rum/data_distribution_config.ts | 9 +-
.../rum/kpi_over_time_config.ts | 10 +-
.../synthetics/data_distribution_config.ts | 9 +-
.../synthetics/kpi_over_time_config.ts | 4 +-
.../test_data/sample_attribute.ts | 89 +++--
.../test_data/sample_attribute_cwv.ts | 4 +-
.../test_data/sample_attribute_kpi.ts | 75 ++--
.../exploratory_view/configurations/utils.ts | 27 +-
.../exploratory_view.test.tsx | 34 +-
.../exploratory_view/exploratory_view.tsx | 174 +++++++---
.../header/add_to_case_action.test.tsx | 7 +-
.../header/add_to_case_action.tsx | 3 +-
.../exploratory_view/header/header.test.tsx | 47 +--
.../shared/exploratory_view/header/header.tsx | 87 ++---
.../last_updated.tsx | 21 +-
.../exploratory_view/hooks/use_add_to_case.ts | 2 +-
.../hooks/use_app_index_pattern.tsx | 2 +-
.../hooks/use_discover_link.tsx | 92 +++++
.../hooks/use_lens_attributes.ts | 57 ++-
.../hooks/use_series_filters.ts | 43 ++-
.../hooks/use_series_storage.test.tsx | 91 ++---
.../hooks/use_series_storage.tsx | 123 ++++---
.../shared/exploratory_view/index.tsx | 4 +-
.../exploratory_view/lens_embeddable.tsx | 77 +++-
.../shared/exploratory_view/rtl_helpers.tsx | 61 ++--
.../columns/data_types_col.test.tsx | 62 ----
.../series_builder/columns/data_types_col.tsx | 74 ----
.../columns/date_picker_col.tsx | 39 ---
.../columns/report_breakdowns.test.tsx | 74 ----
.../columns/report_breakdowns.tsx | 26 --
.../columns/report_definition_col.tsx | 101 ------
.../columns/report_filters.test.tsx | 28 --
.../series_builder/columns/report_filters.tsx | 29 --
.../columns/report_types_col.test.tsx | 79 -----
.../columns/report_types_col.tsx | 108 ------
.../series_builder/report_metric_options.tsx | 46 ---
.../series_builder/series_builder.tsx | 303 ----------------
.../series_editor/chart_edit_options.tsx | 30 --
.../series_editor/columns/breakdowns.test.tsx | 22 +-
.../series_editor/columns/breakdowns.tsx | 49 +--
.../series_editor/columns/chart_options.tsx | 35 --
.../columns/chart_type_select.tsx | 73 ++++
.../columns/chart_types.test.tsx | 6 +-
.../columns/chart_types.tsx | 52 ++-
.../columns/data_type_select.test.tsx | 45 +++
.../columns/data_type_select.tsx | 144 ++++++++
.../series_editor/columns/date_picker_col.tsx | 78 ++++-
.../columns/filter_expanded.test.tsx | 48 +--
.../series_editor/columns/filter_expanded.tsx | 139 ++++----
.../columns/filter_value_btn.test.tsx | 145 ++++----
.../columns/filter_value_btn.tsx | 16 +-
.../columns/incomplete_badge.tsx | 63 ++++
.../columns/operation_type_select.test.tsx | 36 +-
.../columns/operation_type_select.tsx | 13 +-
.../series_editor/columns/remove_series.tsx | 41 ++-
.../columns/report_definition_col.test.tsx | 46 +--
.../columns/report_definition_col.tsx | 59 ++++
.../columns/report_definition_field.tsx | 50 ++-
.../columns/report_type_select.tsx | 63 ++++
.../{ => columns}/selected_filters.test.tsx | 22 +-
.../columns/selected_filters.tsx | 101 ++++++
.../series_editor/columns/series_actions.tsx | 139 ++++----
.../series_editor/columns/series_filter.tsx | 145 ++------
.../series_editor/columns/series_info.tsx | 37 ++
.../columns/series_name.test.tsx | 47 +++
.../series_editor/columns/series_name.tsx | 105 ++++++
.../series_editor/expanded_series_row.tsx | 95 +++++
.../series_editor/report_metric_options.tsx | 139 ++++++++
.../series_editor/selected_filters.tsx | 101 ------
.../exploratory_view/series_editor/series.tsx | 93 +++++
.../series_editor/series_editor.tsx | 328 +++++++++++-------
.../shared/exploratory_view/types.ts | 17 +-
.../views/add_series_button.test.tsx | 106 ++++++
.../views/add_series_button.tsx | 80 +++++
.../exploratory_view/views/series_views.tsx | 26 ++
.../exploratory_view/views/view_actions.tsx | 30 ++
.../field_value_combobox.tsx | 61 ++--
.../field_value_selection.tsx | 4 +-
.../field_value_suggestions/index.test.tsx | 2 +
.../shared/field_value_suggestions/index.tsx | 8 +-
.../shared/field_value_suggestions/types.ts | 3 +-
.../filter_value_label/filter_value_label.tsx | 18 +-
.../public/components/shared/index.tsx | 3 +-
.../public/hooks/use_quick_time_ranges.tsx | 2 +-
x-pack/plugins/observability/public/plugin.ts | 2 +
.../observability/public/routes/index.tsx | 16 +-
.../translations/translations/ja-JP.json | 20 --
.../translations/translations/zh-CN.json | 20 --
.../common/charts/ping_histogram.tsx | 25 +-
.../common/header/action_menu_content.tsx | 29 +-
.../monitor_duration_container.tsx | 21 +-
.../apps/observability/exploratory_view.ts | 82 +++++
.../apps/observability/index.ts | 3 +-
124 files changed, 3602 insertions(+), 2508 deletions(-)
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_date_picker => components}/date_range_picker.tsx (58%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_color_picker.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{ => components}/series_date_picker/index.tsx (54%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{ => components}/series_date_picker/series_date_picker.test.tsx (51%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => header}/last_updated.tsx (55%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/chart_types.test.tsx (85%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/chart_types.tsx (77%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/operation_type_select.test.tsx (69%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/operation_type_select.tsx (91%)
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/report_definition_col.test.tsx (65%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_builder => series_editor}/columns/report_definition_field.tsx (69%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
rename x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/{ => columns}/selected_filters.test.tsx (59%)
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
create mode 100644 x-pack/test/observability_functional/apps/observability/exploratory_view.ts
diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts
index 734e26c5199d7..e08695b334e1b 100644
--- a/packages/kbn-test/src/jest/utils/get_url.ts
+++ b/packages/kbn-test/src/jest/utils/get_url.ts
@@ -22,11 +22,6 @@ interface UrlParam {
username?: string;
}
-interface App {
- pathname?: string;
- hash?: string;
-}
-
/**
* Converts a config and a pathname to a url
* @param {object} config A url config
@@ -46,11 +41,11 @@ interface App {
* @return {string}
*/
-function getUrl(config: UrlParam, app: App) {
+function getUrl(config: UrlParam, app: UrlParam) {
return url.format(_.assign({}, config, app));
}
-getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) {
+getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) {
config = _.pickBy(config, function (val, param) {
return param !== 'auth';
});
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index 853a926f4f6e8..8fe2e4139e6ca 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -217,8 +217,9 @@ export class CommonPageObject extends FtrService {
{
basePath = '',
shouldLoginIfPrompted = true,
- disableWelcomePrompt = true,
hash = '',
+ search = '',
+ disableWelcomePrompt = true,
insertTimestamp = true,
} = {}
) {
@@ -229,11 +230,13 @@ export class CommonPageObject extends FtrService {
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
pathname: `${basePath}${appConfig.pathname}`,
hash: hash || appConfig.hash,
+ search,
});
} else {
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
pathname: `${basePath}/app/${appName}`,
hash,
+ search,
});
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
index 170e3a2fdad1e..593de7c3a6f70 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
@@ -11,12 +11,12 @@ import { i18n } from '@kbn/i18n';
import {
createExploratoryViewUrl,
HeaderMenuPortal,
- SeriesUrl,
} from '../../../../../../observability/public';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { AppMountParameters } from '../../../../../../../../src/core/public';
import { InspectorHeaderLink } from '../../../shared/apm_header_action_menu/inspector_header_link';
+import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames';
const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', {
defaultMessage: 'Analyze data',
@@ -39,15 +39,22 @@ export function UXActionMenu({
services: { http },
} = useKibana();
const { urlParams } = useUrlParams();
- const { rangeTo, rangeFrom } = urlParams;
+ const { rangeTo, rangeFrom, serviceName } = urlParams;
const uxExploratoryViewLink = createExploratoryViewUrl(
{
- 'ux-series': {
- dataType: 'ux',
- isNew: true,
- time: { from: rangeFrom, to: rangeTo },
- } as unknown as SeriesUrl,
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ dataType: 'ux',
+ name: `${serviceName}-page-views`,
+ time: { from: rangeFrom!, to: rangeTo! },
+ reportDefinitions: {
+ [SERVICE_NAME]: serviceName ? [serviceName] : [],
+ },
+ selectedMetricField: 'Records',
+ },
+ ],
},
http?.basePath.get()
);
@@ -61,6 +68,7 @@ export function UXActionMenu({
{ANALYZE_MESSAGE}
}>
{
render();
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => {
render();
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => {
render();
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
@@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => {
render();
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
- 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ 'http://localhost/app/observability/exploratory-view/#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
);
});
});
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
index 068d7bb1c242f..a4fc964a444c9 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
@@ -9,10 +9,7 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
-import {
- createExploratoryViewUrl,
- SeriesUrl,
-} from '../../../../../../observability/public';
+import { createExploratoryViewUrl } from '../../../../../../observability/public';
import { ALL_VALUES_SELECTED } from '../../../../../../observability/public';
import {
isIosAgentName,
@@ -21,6 +18,7 @@ import {
import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
+ TRANSACTION_DURATION,
} from '../../../../../common/elasticsearch_fieldnames';
import {
ENVIRONMENT_ALL,
@@ -29,13 +27,11 @@ import {
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
-function getEnvironmentDefinition(environment?: string) {
+function getEnvironmentDefinition(environment: string) {
switch (environment) {
case ENVIRONMENT_ALL.value:
return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] };
case ENVIRONMENT_NOT_DEFINED.value:
- case undefined:
- return {};
default:
return { [SERVICE_ENVIRONMENT]: [environment] };
}
@@ -54,21 +50,26 @@ export function AnalyzeDataButton() {
if (
(isRumAgentName(agentName) || isIosAgentName(agentName)) &&
- canShowDashboard
+ rangeFrom &&
+ canShowDashboard &&
+ rangeTo
) {
const href = createExploratoryViewUrl(
{
- 'apm-series': {
- dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
- time: { from: rangeFrom, to: rangeTo },
- reportType: 'kpi-over-time',
- reportDefinitions: {
- [SERVICE_NAME]: [serviceName],
- ...getEnvironmentDefinition(environment),
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${serviceName}-response-latency`,
+ selectedMetricField: TRANSACTION_DURATION,
+ dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
+ time: { from: rangeFrom, to: rangeTo },
+ reportDefinitions: {
+ [SERVICE_NAME]: [serviceName],
+ ...(environment ? getEnvironmentDefinition(environment) : {}),
+ },
+ operationType: 'average',
},
- operationType: 'average',
- isNew: true,
- } as SeriesUrl,
+ ],
},
basepath
);
diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
index ebb5c7655806a..9409e94fa9ba9 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import moment from 'moment';
import { SetupUX } from '../../routes/rum_client';
import {
SERVICE_NAME,
@@ -16,8 +17,8 @@ import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
export async function hasRumData({
setup,
- start,
- end,
+ start = moment().subtract(24, 'h').valueOf(),
+ end = moment().valueOf(),
}: {
setup: SetupUX;
start?: number;
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
new file mode 100644
index 0000000000000..0e17c6277618b
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function MobileAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', {
+ defaultMessage: 'Add Mobile data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
new file mode 100644
index 0000000000000..af91624769e6b
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function SyntheticsAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', {
+ defaultMessage: 'Add synthetics data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
new file mode 100644
index 0000000000000..c6aa0742466f1
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiHeaderLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../utils/kibana_react';
+
+export function UXAddData() {
+ const kibana = useKibana();
+
+ return (
+
+ {ADD_DATA_LABEL}
+
+ );
+}
+
+const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', {
+ defaultMessage: 'Add UX data',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
new file mode 100644
index 0000000000000..2b59628c3e8d3
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { render } from '../../rtl_helpers';
+import { fireEvent, screen } from '@testing-library/dom';
+import React from 'react';
+import { sampleAttribute } from '../../configurations/test_data/sample_attribute';
+import * as pluginHook from '../../../../../hooks/use_plugin_context';
+import { TypedLensByValueInput } from '../../../../../../../lens/public';
+import { ExpViewActionMenuContent } from './action_menu';
+
+jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
+ appMountParameters: {
+ setHeaderActionMenu: jest.fn(),
+ },
+} as any);
+
+describe('Action Menu', function () {
+ it('should be able to click open in lens', async function () {
+ const { findByText, core } = render(
+
+ );
+
+ expect(await screen.findByText('Open in Lens')).toBeInTheDocument();
+
+ fireEvent.click(await findByText('Open in Lens'));
+
+ expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
+ expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
+ {
+ id: '',
+ attributes: sampleAttribute,
+ timeRange: { to: 'now', from: 'now-10m' },
+ },
+ {
+ openInNewTab: true,
+ }
+ );
+ });
+
+ it('should be able to click save', async function () {
+ const { findByText } = render(
+
+ );
+
+ expect(await screen.findByText('Save')).toBeInTheDocument();
+
+ fireEvent.click(await findByText('Save'));
+
+ expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
new file mode 100644
index 0000000000000..08b4a3b948c57
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public';
+import { ObservabilityAppServices } from '../../../../../application/types';
+import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { AddToCaseAction } from '../../header/add_to_case_action';
+
+export function ExpViewActionMenuContent({
+ timeRange,
+ lensAttributes,
+}: {
+ timeRange?: { from: string; to: string };
+ lensAttributes: TypedLensByValueInput['attributes'] | null;
+}) {
+ const kServices = useKibana().services;
+
+ const { lens } = kServices;
+
+ const [isSaveOpen, setIsSaveOpen] = useState(false);
+
+ const LensSaveModalComponent = lens.SaveModalComponent;
+
+ return (
+ <>
+
+
+
+
+
+ {
+ if (lensAttributes) {
+ lens.navigateToPrefilledEditor(
+ {
+ id: '',
+ timeRange,
+ attributes: lensAttributes,
+ },
+ {
+ openInNewTab: true,
+ }
+ );
+ }
+ }}
+ >
+ {i18n.translate('xpack.observability.expView.heading.openInLens', {
+ defaultMessage: 'Open in Lens',
+ })}
+
+
+
+ {
+ if (lensAttributes) {
+ setIsSaveOpen(true);
+ }
+ }}
+ size="s"
+ >
+ {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
+ defaultMessage: 'Save',
+ })}
+
+
+
+
+ {isSaveOpen && lensAttributes && (
+ setIsSaveOpen(false)}
+ // if we want to do anything after the viz is saved
+ // right now there is no action, so an empty function
+ onSave={() => {}}
+ />
+ )}
+ >
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
new file mode 100644
index 0000000000000..23500b63e900a
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { ExpViewActionMenuContent } from './action_menu';
+import HeaderMenuPortal from '../../../header_menu_portal';
+import { usePluginContext } from '../../../../../hooks/use_plugin_context';
+import { TypedLensByValueInput } from '../../../../../../../lens/public';
+
+interface Props {
+ timeRange?: { from: string; to: string };
+ lensAttributes: TypedLensByValueInput['attributes'] | null;
+}
+export function ExpViewActionMenu(props: Props) {
+ const { appMountParameters } = usePluginContext();
+
+ return (
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
similarity index 58%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
index c30863585b3b0..aabde404aa7b4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx
@@ -6,48 +6,48 @@
*/
import React from 'react';
-import { i18n } from '@kbn/i18n';
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
-import DateMath from '@elastic/datemath';
import { Moment } from 'moment';
+import DateMath from '@elastic/datemath';
+import { i18n } from '@kbn/i18n';
import { useSeriesStorage } from '../hooks/use_series_storage';
import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
+import { SeriesUrl } from '../types';
+import { ReportTypes } from '../configurations/constants';
export const parseAbsoluteDate = (date: string, options = {}) => {
return DateMath.parse(date, options)!;
};
-export function DateRangePicker({ seriesId }: { seriesId: string }) {
- const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
+export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
+ const { firstSeries, setSeries, reportType } = useSeriesStorage();
const dateFormat = useUiSetting('dateFormat');
- const {
- time: { from, to },
- reportType,
- } = getSeries(firstSeriesId);
+ const seriesFrom = series.time?.from;
+ const seriesTo = series.time?.to;
- const series = getSeries(seriesId);
+ const { from: mainFrom, to: mainTo } = firstSeries!.time;
- const {
- time: { from: seriesFrom, to: seriesTo },
- } = series;
+ const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!;
+ const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!;
- const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
- const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
+ const getTotalDuration = () => {
+ const mainStartDate = parseAbsoluteDate(mainFrom)!;
+ const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!;
+ return mainEndDate.diff(mainStartDate, 'millisecond');
+ };
- const onStartChange = (newDate: Moment) => {
- if (reportType === 'kpi-over-time') {
- const mainStartDate = parseAbsoluteDate(from)!;
- const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
- const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
- const newFrom = newDate.toISOString();
- const newTo = newDate.add(totalDuration, 'millisecond').toISOString();
+ const onStartChange = (newStartDate: Moment) => {
+ if (reportType === ReportTypes.KPI) {
+ const totalDuration = getTotalDuration();
+ const newFrom = newStartDate.toISOString();
+ const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
- const newFrom = newDate.toISOString();
+ const newFrom = newStartDate.toISOString();
setSeries(seriesId, {
...series,
@@ -55,20 +55,19 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
});
}
};
- const onEndChange = (newDate: Moment) => {
- if (reportType === 'kpi-over-time') {
- const mainStartDate = parseAbsoluteDate(from)!;
- const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
- const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
- const newTo = newDate.toISOString();
- const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString();
+
+ const onEndChange = (newEndDate: Moment) => {
+ if (reportType === ReportTypes.KPI) {
+ const totalDuration = getTotalDuration();
+ const newTo = newEndDate.toISOString();
+ const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
- const newTo = newDate.toISOString();
+ const newTo = newEndDate.toISOString();
setSeries(seriesId, {
...series,
@@ -90,7 +89,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
defaultMessage: 'Start date',
})}
- dateFormat={dateFormat}
+ dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
/>
}
@@ -104,7 +103,7 @@ export function DateRangePicker({ seriesId }: { seriesId: string }) {
aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
defaultMessage: 'End date',
})}
- dateFormat={dateFormat}
+ dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
/>
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
index 3566835b1701c..d17e451ef702c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
@@ -10,19 +10,19 @@ import { isEmpty } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
-import { LOADING_VIEW } from '../series_builder/series_builder';
-import { SeriesUrl } from '../types';
+import { LOADING_VIEW } from '../series_editor/series_editor';
+import { ReportViewType, SeriesUrl } from '../types';
export function EmptyView({
loading,
- height,
series,
+ reportType,
}: {
loading: boolean;
- height: string;
- series: SeriesUrl;
+ series?: SeriesUrl;
+ reportType: ReportViewType;
}) {
- const { dataType, reportType, reportDefinitions } = series ?? {};
+ const { dataType, reportDefinitions } = series ?? {};
let emptyMessage = EMPTY_LABEL;
@@ -45,7 +45,7 @@ export function EmptyView({
}
return (
-
+
{loading && (
`
+const Wrapper = styled.div`
text-align: center;
- height: ${(props) => props.height};
position: relative;
`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
index fe2953edd36d6..03fd23631f755 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
+import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers';
import { FilterLabel } from './filter_label';
import * as useSeriesHook from '../hooks/use_series_filters';
import { buildFilterLabel } from '../../filter_value_label/filter_value_label';
@@ -27,9 +27,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={jest.fn()}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -51,9 +52,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={removeFilter}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -74,9 +76,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={false}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={removeFilter}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
@@ -100,9 +103,10 @@ describe('FilterLabel', function () {
value={'elastic-co'}
label={'Web Application'}
negate={true}
- seriesId={'kpi-over-time'}
+ seriesId={0}
removeFilter={jest.fn()}
indexPattern={mockIndexPattern}
+ series={mockUxSeries}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
index a08e777c5ea71..c6254a85de9ac 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
@@ -9,21 +9,24 @@ import React from 'react';
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { useSeriesFilters } from '../hooks/use_series_filters';
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
+import { SeriesUrl } from '../types';
interface Props {
field: string;
label: string;
- value: string;
- seriesId: string;
+ value: string | string[];
+ seriesId: number;
+ series: SeriesUrl;
negate: boolean;
definitionFilter?: boolean;
indexPattern: IndexPattern;
- removeFilter: (field: string, value: string, notVal: boolean) => void;
+ removeFilter: (field: string, value: string | string[], notVal: boolean) => void;
}
export function FilterLabel({
label,
seriesId,
+ series,
field,
value,
negate,
@@ -31,7 +34,7 @@ export function FilterLabel({
removeFilter,
definitionFilter,
}: Props) {
- const { invertFilter } = useSeriesFilters({ seriesId });
+ const { invertFilter } = useSeriesFilters({ seriesId, series });
return indexPattern ? (
{
+ setSeries(seriesId, { ...series, color: colorN });
+ };
+
+ const color =
+ series.color ?? (theme.eui as unknown as Record)[`euiColorVis${seriesId}`];
+
+ const button = (
+
+ setIsOpen((prevState) => !prevState)} flush="both">
+
+
+
+ );
+
+ return (
+ setIsOpen(false)}>
+
+
+
+
+ );
+}
+
+const PICK_A_COLOR_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.pickColor',
+ {
+ defaultMessage: 'Pick a color',
+ }
+);
+
+const EDIT_SERIES_COLOR_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.editSeriesColor',
+ {
+ defaultMessage: 'Edit color for series',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
similarity index 54%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
index e21da424b58c8..e02f11dfc4954 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx
@@ -6,11 +6,13 @@
*/
import { EuiSuperDatePicker } from '@elastic/eui';
-import React, { useEffect } from 'react';
-import { useHasData } from '../../../../hooks/use_has_data';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
-import { DEFAULT_TIME } from '../configurations/constants';
+import React from 'react';
+
+import { useHasData } from '../../../../../hooks/use_has_data';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges';
+import { SeriesUrl } from '../../types';
+import { ReportTypes } from '../../configurations/constants';
export interface TimePickerTime {
from: string;
@@ -22,28 +24,27 @@ export interface TimePickerQuickRange extends TimePickerTime {
}
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
}
-export function SeriesDatePicker({ seriesId }: Props) {
+export function SeriesDatePicker({ series, seriesId }: Props) {
const { onRefreshTimeRange } = useHasData();
const commonlyUsedRanges = useQuickTimeRanges();
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries, reportType, allSeries } = useSeriesStorage();
function onTimeChange({ start, end }: { start: string; end: string }) {
onRefreshTimeRange();
- setSeries(seriesId, { ...series, time: { from: start, to: end } });
- }
-
- useEffect(() => {
- if (!series || !series.time) {
- setSeries(seriesId, { ...series, time: DEFAULT_TIME });
+ if (reportType === ReportTypes.KPI) {
+ allSeries.forEach((currSeries, seriesIndex) => {
+ setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } });
+ });
+ } else {
+ setSeries(seriesId, { ...series, time: { from: start, to: end } });
}
- }, [series, seriesId, setSeries]);
+ }
return (
, { initSeries });
+ const { getByText } = render(, {
+ initSeries,
+ });
getByText('Last 30 minutes');
});
- it('should set defaults', async function () {
- const initSeries = {
- data: {
- 'uptime-pings-histogram': {
- reportType: 'kpi-over-time' as const,
- dataType: 'synthetics' as const,
- breakdown: 'monitor.status',
- },
- },
- };
- const { setSeries: setSeries1 } = render(
- ,
- { initSeries: initSeries as any }
- );
- expect(setSeries1).toHaveBeenCalledTimes(1);
- expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
- breakdown: 'monitor.status',
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- time: DEFAULT_TIME,
- });
- });
-
it('should set series data', async function () {
const initSeries = {
- data: {
- 'uptime-pings-histogram': {
+ data: [
+ {
+ name: 'uptime-pings-histogram',
dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
- },
+ ],
};
const { onRefreshTimeRange } = mockUseHasData();
- const { getByTestId, setSeries } = render(, {
- initSeries,
- });
+ const { getByTestId, setSeries } = render(
+ ,
+ {
+ initSeries,
+ }
+ );
await waitFor(function () {
fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton'));
@@ -76,10 +57,10 @@ describe('SeriesDatePicker', function () {
expect(onRefreshTimeRange).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
+ name: 'uptime-pings-histogram',
breakdown: 'monitor.status',
dataType: 'synthetics',
- reportType: 'kpi-over-time',
time: { from: 'now/d', to: 'now/d' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index ba1f2214223e3..bf5feb7d5863c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -94,6 +94,19 @@ export const DataViewLabels: Record = {
'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
};
+export enum ReportTypes {
+ KPI = 'kpi-over-time',
+ DISTRIBUTION = 'data-distribution',
+ CORE_WEB_VITAL = 'core-web-vitals',
+ DEVICE_DISTRIBUTION = 'device-data-distribution',
+}
+
+export enum DataTypes {
+ SYNTHETICS = 'synthetics',
+ UX = 'ux',
+ MOBILE = 'mobile',
+}
+
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
export const FILTER_RECORDS = 'FILTER_RECORDS';
export const TERMS_COLUMN = 'TERMS_COLUMN';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
index 6f990015fbc62..55ac75b47c056 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts
@@ -8,10 +8,12 @@
export enum URL_KEYS {
DATA_TYPE = 'dt',
OPERATION_TYPE = 'op',
- REPORT_TYPE = 'rt',
SERIES_TYPE = 'st',
BREAK_DOWN = 'bd',
FILTERS = 'ft',
REPORT_DEFINITIONS = 'rdf',
SELECTED_METRIC = 'mt',
+ HIDDEN = 'h',
+ NAME = 'n',
+ COLOR = 'c',
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 574a9f6a2bc10..3f6551986527c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -15,6 +15,7 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config';
import { getMobileKPIConfig } from './mobile/kpi_over_time_config';
import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
+import { DataTypes, ReportTypes } from './constants';
interface Props {
reportType: ReportViewType;
@@ -24,24 +25,24 @@ interface Props {
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
switch (dataType) {
- case 'ux':
- if (reportType === 'data-distribution') {
+ case DataTypes.UX:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getRumDistributionConfig({ indexPattern });
}
- if (reportType === 'core-web-vitals') {
+ if (reportType === ReportTypes.CORE_WEB_VITAL) {
return getCoreWebVitalsConfig({ indexPattern });
}
return getKPITrendsLensConfig({ indexPattern });
- case 'synthetics':
- if (reportType === 'data-distribution') {
+ case DataTypes.SYNTHETICS:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getSyntheticsDistributionConfig({ indexPattern });
}
return getSyntheticsKPIConfig({ indexPattern });
- case 'mobile':
- if (reportType === 'data-distribution') {
+ case DataTypes.MOBILE:
+ if (reportType === ReportTypes.DISTRIBUTION) {
return getMobileKPIDistributionConfig({ indexPattern });
}
- if (reportType === 'device-data-distribution') {
+ if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
return getMobileDeviceDistributionConfig({ indexPattern });
}
return getMobileKPIConfig({ indexPattern });
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index 706c58609b7cb..9e7c5254b511f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -16,7 +16,7 @@ import {
} from './constants/elasticsearch_fieldnames';
import { buildExistsFilter, buildPhrasesFilter } from './utils';
import { sampleAttributeKpi } from './test_data/sample_attribute_kpi';
-import { REPORT_METRIC_FIELD } from './constants';
+import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants';
describe('Lens Attribute', () => {
mockAppIndexPattern();
@@ -38,6 +38,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
beforeEach(() => {
@@ -50,7 +53,7 @@ describe('Lens Attribute', () => {
it('should return expected json for kpi report type', function () {
const seriesConfigKpi = getDefaultConfigs({
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
dataType: 'ux',
indexPattern: mockIndexPattern,
});
@@ -63,6 +66,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: RECORDS_FIELD,
},
]);
@@ -135,6 +141,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1]);
@@ -383,7 +392,7 @@ describe('Lens Attribute', () => {
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
},
],
legend: { isVisible: true, position: 'right' },
@@ -403,6 +412,9 @@ describe('Lens Attribute', () => {
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
breakdown: USER_AGENT_NAME,
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1]);
@@ -423,7 +435,7 @@ describe('Lens Attribute', () => {
seriesType: 'line',
splitAccessor: 'breakdown-column-layer0',
xAccessor: 'x-axis-column-layer0',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
},
]);
@@ -589,6 +601,9 @@ describe('Lens Attribute', () => {
indexPattern: mockIndexPattern,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
+ color: 'green',
+ name: 'test-series',
+ selectedMetricField: TRANSACTION_DURATION,
};
const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 2778edc94838e..ec2e6b5066c87 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -37,10 +37,11 @@ import {
REPORT_METRIC_FIELD,
RECORDS_FIELD,
RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
} from './constants';
import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types';
import { PersistableFilter } from '../../../../../../lens/common';
-import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
+import { parseAbsoluteDate } from '../components/date_range_picker';
import { getDistributionInPercentageColumn } from './lens_columns/overall_column';
function getLayerReferenceName(layerId: string) {
@@ -74,14 +75,6 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF
timeScale = currField?.timeScale;
columnLabel = currField?.label;
}
- } else if (metricOptions?.[0].field || metricOptions?.[0].id) {
- const firstMetricOption = metricOptions?.[0];
-
- selectedMetricField = firstMetricOption.field || firstMetricOption.id;
- columnType = firstMetricOption.columnType;
- columnFilters = firstMetricOption.columnFilters;
- timeScale = firstMetricOption.timeScale;
- columnLabel = firstMetricOption.label;
}
return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel };
@@ -96,7 +89,9 @@ export interface LayerConfig {
reportDefinitions: URLReportDefinition;
time: { to: string; from: string };
indexPattern: IndexPattern;
- selectedMetricField?: string;
+ selectedMetricField: string;
+ color: string;
+ name: string;
}
export class LensAttributes {
@@ -467,14 +462,15 @@ export class LensAttributes {
getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
const {
filters,
- time: { from, to },
+ time,
seriesConfig: { baseFilters: layerFilters, reportType },
} = layerConfig;
let baseFilters = '';
- if (reportType !== 'kpi-over-time' && totalLayers > 1) {
+
+ if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) {
// for kpi over time, we don't need to add time range filters
// since those are essentially plotted along the x-axis
- baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`;
+ baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`;
}
layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
@@ -530,7 +526,11 @@ export class LensAttributes {
}
getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
- if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') {
+ if (
+ index === 0 ||
+ mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI ||
+ !layerConfig.time
+ ) {
return null;
}
@@ -542,11 +542,14 @@ export class LensAttributes {
time: { from },
} = layerConfig;
- const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
+ const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'));
if (inDays > 1) {
return inDays + 'd';
}
- const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
+ const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'));
+ if (inHours === 0) {
+ return null;
+ }
return inHours + 'h';
}
@@ -564,6 +567,8 @@ export class LensAttributes {
const { sourceField } = seriesConfig.xAxisColumn;
+ const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label;
+
layers[layerId] = {
columnOrder: [
`x-axis-column-${layerId}`,
@@ -577,7 +582,7 @@ export class LensAttributes {
[`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
[`y-axis-column-${layerId}`]: {
...mainYAxis,
- label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
+ label,
filter: { query: columnFilter, language: 'kuery' },
...(timeShift ? { timeShift } : {}),
},
@@ -621,7 +626,7 @@ export class LensAttributes {
seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType,
palette: layerConfig.seriesConfig.palette,
yConfig: layerConfig.seriesConfig.yConfig || [
- { forAccessor: `y-axis-column-layer${index}` },
+ { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color },
],
xAccessor: `x-axis-column-layer${index}`,
...(layerConfig.breakdown &&
@@ -635,7 +640,7 @@ export class LensAttributes {
};
}
- getJSON(): TypedLensByValueInput['attributes'] {
+ getJSON(refresh?: number): TypedLensByValueInput['attributes'] {
const uniqueIndexPatternsIds = Array.from(
new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
);
@@ -644,7 +649,7 @@ export class LensAttributes {
return {
title: 'Prefilled from exploratory view app',
- description: '',
+ description: String(refresh),
visualizationType: 'lnsXY',
references: [
...uniqueIndexPatternsIds.map((patternId) => ({
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
index d1612a08f5551..4e178bba7e02a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants';
+import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants';
import { buildPhraseFilter } from '../utils';
import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels';
@@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'device-data-distribution',
+ reportType: ReportTypes.DEVICE_DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['bar', 'bar_horizontal'],
xAxisColumn: {
@@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps)
...MobileFields,
[SERVICE_NAME]: MOBILE_APP,
},
+ definitionFields: [SERVICE_NAME],
metricOptions: [
{
- id: 'labels.device_id',
field: 'labels.device_id',
+ id: 'labels.device_id',
label: NUMBER_OF_DEVICES,
},
],
- definitionFields: [SERVICE_NAME],
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
index 9b1c4c8da3e9b..1da27be4fcc95 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
@@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
index 945a631078a33..3ee5b3125fcda 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts
@@ -6,7 +6,13 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import {
+ FieldLabels,
+ OPERATION_COLUMN,
+ RECORDS_FIELD,
+ REPORT_METRIC_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
@@ -26,7 +32,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar', 'area'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
index 07bb13f957e45..35e094996f6f2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts
@@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers';
import { getDefaultConfigs } from '../default_configs';
import { LayerConfig, LensAttributes } from '../lens_attributes';
import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv';
-import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
+import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
describe('Core web vital config test', function () {
mockAppIndexPattern();
@@ -24,10 +24,13 @@ describe('Core web vital config test', function () {
const layerConfig: LayerConfig = {
seriesConfig,
+ color: 'green',
+ name: 'test-series',
+ breakdown: USER_AGENT_OS,
indexPattern: mockIndexPattern,
- reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
- breakdown: USER_AGENT_OS,
+ reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
+ selectedMetricField: LCP_FIELD,
};
beforeEach(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
index 62455df248085..e8d620388a89e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts
@@ -11,6 +11,7 @@ import {
FieldLabels,
FILTER_RECORDS,
REPORT_METRIC_FIELD,
+ ReportTypes,
USE_BREAK_DOWN_COLUMN,
} from '../constants';
import { buildPhraseFilter } from '../utils';
@@ -38,7 +39,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
return {
defaultSeriesType: 'bar_horizontal_percentage_stacked',
- reportType: 'core-web-vitals',
+ reportType: ReportTypes.CORE_WEB_VITAL,
seriesTypes: ['bar_horizontal_percentage_stacked'],
xAxisColumn: {
sourceField: USE_BREAK_DOWN_COLUMN,
@@ -153,5 +154,6 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
{ color: statusPallete[1], forAccessor: 'y-axis-column-1' },
{ color: statusPallete[2], forAccessor: 'y-axis-column-2' },
],
+ query: { query: 'transaction.type: "page-load"', language: 'kuery' },
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
index f34c8db6c197d..de6f2c67b2aeb 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts
@@ -6,7 +6,12 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
+import {
+ FieldLabels,
+ REPORT_METRIC_FIELD,
+ RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
@@ -41,7 +46,7 @@ import {
export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
index 5899b16d12b4f..9112778eadaa7 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts
@@ -6,7 +6,13 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
+import {
+ FieldLabels,
+ OPERATION_COLUMN,
+ RECORDS_FIELD,
+ REPORT_METRIC_FIELD,
+ ReportTypes,
+} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
@@ -43,7 +49,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon
return {
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
xAxisColumn: {
sourceField: '@timestamp',
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
index 730e742f9d8c5..da90f45d15201 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
@@ -6,7 +6,12 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
+import {
+ FieldLabels,
+ REPORT_METRIC_FIELD,
+ RECORDS_PERCENTAGE_FIELD,
+ ReportTypes,
+} from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
@@ -30,7 +35,7 @@ export function getSyntheticsDistributionConfig({
indexPattern,
}: ConfigProps): SeriesConfig {
return {
- reportType: 'data-distribution',
+ reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: series?.seriesType || 'line',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
index 4ee22181d4334..65b43a83a8fb5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
@@ -6,7 +6,7 @@
*/
import { ConfigProps, SeriesConfig } from '../../types';
-import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants';
+import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
@@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down';
export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
return {
- reportType: 'kpi-over-time',
+ reportType: ReportTypes.KPI,
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
index 596e7af4378ec..7e0ea1e575481 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
@@ -5,12 +5,18 @@
* 2.0.
*/
export const sampleAttribute = {
- title: 'Prefilled from exploratory view app',
- description: '',
- visualizationType: 'lnsXY',
+ description: 'undefined',
references: [
- { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-current-indexpattern',
+ type: 'index-pattern',
+ },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-layer-layer0',
+ type: 'index-pattern',
+ },
],
state: {
datasourceStates: {
@@ -28,17 +34,23 @@ export const sampleAttribute = {
],
columns: {
'x-axis-column-layer0': {
- sourceField: 'transaction.duration.us',
- label: 'Page load time',
dataType: 'number',
- operationType: 'range',
isBucketed: true,
- scale: 'interval',
+ label: 'Page load time',
+ operationType: 'range',
params: {
- type: 'histogram',
- ranges: [{ from: 0, to: 1000, label: '' }],
maxBars: 'auto',
+ ranges: [
+ {
+ from: 0,
+ label: '',
+ to: 1000,
+ },
+ ],
+ type: 'histogram',
},
+ scale: 'interval',
+ sourceField: 'transaction.duration.us',
},
'y-axis-column-layer0': {
dataType: 'number',
@@ -81,16 +93,16 @@ export const sampleAttribute = {
'y-axis-column-layer0X1': {
customLabel: true,
dataType: 'number',
- isBucketed: false,
- label: 'Part of count() / overall_sum(count())',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
+ isBucketed: false,
+ label: 'Part of count() / overall_sum(count())',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
},
'y-axis-column-layer0X2': {
customLabel: true,
@@ -140,27 +152,52 @@ export const sampleAttribute = {
},
},
},
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: 'transaction.duration.us < 60000000',
+ },
visualization: {
- legend: { isVisible: true, position: 'right' },
- valueLabels: 'hide',
- fittingFunction: 'Linear',
+ axisTitlesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
curveType: 'CURVE_MONOTONE_X',
- axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
- gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- preferredSeriesType: 'line',
+ fittingFunction: 'Linear',
+ gridlinesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
layers: [
{
accessors: ['y-axis-column-layer0'],
layerId: 'layer0',
layerType: 'data',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
xAccessor: 'x-axis-column-layer0',
+ yConfig: [
+ {
+ color: 'green',
+ forAccessor: 'y-axis-column-layer0',
+ },
+ ],
},
],
+ legend: {
+ isVisible: true,
+ position: 'right',
+ },
+ preferredSeriesType: 'line',
+ tickLabelsVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
+ valueLabels: 'hide',
},
- query: { query: 'transaction.duration.us < 60000000', language: 'kuery' },
- filters: [],
},
+ title: 'Prefilled from exploratory view app',
+ visualizationType: 'lnsXY',
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
index 56ceba8fc52de..dff3d6b3ad5ef 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
export const sampleAttributeCoreWebVital = {
- description: '',
+ description: 'undefined',
references: [
{
id: 'apm-*',
@@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = {
filters: [],
query: {
language: 'kuery',
- query: '',
+ query: 'transaction.type: "page-load"',
},
visualization: {
axisTitlesVisibilitySettings: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
index 72933573c410b..6ed9b4face6e3 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
@@ -5,12 +5,18 @@
* 2.0.
*/
export const sampleAttributeKpi = {
- title: 'Prefilled from exploratory view app',
- description: '',
- visualizationType: 'lnsXY',
+ description: 'undefined',
references: [
- { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-current-indexpattern',
+ type: 'index-pattern',
+ },
+ {
+ id: 'apm-*',
+ name: 'indexpattern-datasource-layer-layer0',
+ type: 'index-pattern',
+ },
],
state: {
datasourceStates: {
@@ -20,25 +26,27 @@ export const sampleAttributeKpi = {
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
columns: {
'x-axis-column-layer0': {
- sourceField: '@timestamp',
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
- params: { interval: 'auto' },
+ params: {
+ interval: 'auto',
+ },
scale: 'interval',
+ sourceField: '@timestamp',
},
'y-axis-column-layer0': {
dataType: 'number',
+ filter: {
+ language: 'kuery',
+ query: 'transaction.type: page-load and processor.event: transaction',
+ },
isBucketed: false,
label: 'Page views',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
- filter: {
- query: 'transaction.type: page-load and processor.event: transaction',
- language: 'kuery',
- },
},
},
incompleteColumns: {},
@@ -46,27 +54,52 @@ export const sampleAttributeKpi = {
},
},
},
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: '',
+ },
visualization: {
- legend: { isVisible: true, position: 'right' },
- valueLabels: 'hide',
- fittingFunction: 'Linear',
+ axisTitlesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
curveType: 'CURVE_MONOTONE_X',
- axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
- gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- preferredSeriesType: 'line',
+ fittingFunction: 'Linear',
+ gridlinesVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
layers: [
{
accessors: ['y-axis-column-layer0'],
layerId: 'layer0',
layerType: 'data',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
xAccessor: 'x-axis-column-layer0',
+ yConfig: [
+ {
+ color: 'green',
+ forAccessor: 'y-axis-column-layer0',
+ },
+ ],
},
],
+ legend: {
+ isVisible: true,
+ position: 'right',
+ },
+ preferredSeriesType: 'line',
+ tickLabelsVisibilitySettings: {
+ x: true,
+ yLeft: true,
+ yRight: true,
+ },
+ valueLabels: 'hide',
},
- query: { query: '', language: 'kuery' },
- filters: [],
},
+ title: 'Prefilled from exploratory view app',
+ visualizationType: 'lnsXY',
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
index c7d2d21581e7a..56e6cb5210356 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
import rison, { RisonValue } from 'rison-node';
-import type { SeriesUrl, UrlFilter } from '../types';
+import type { ReportViewType, SeriesUrl, UrlFilter } from '../types';
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
@@ -16,40 +16,43 @@ export function convertToShortUrl(series: SeriesUrl) {
const {
operationType,
seriesType,
- reportType,
breakdown,
filters,
reportDefinitions,
dataType,
selectedMetricField,
+ hidden,
+ name,
+ color,
...restSeries
} = series;
return {
[URL_KEYS.OPERATION_TYPE]: operationType,
- [URL_KEYS.REPORT_TYPE]: reportType,
[URL_KEYS.SERIES_TYPE]: seriesType,
[URL_KEYS.BREAK_DOWN]: breakdown,
[URL_KEYS.FILTERS]: filters,
[URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions,
[URL_KEYS.DATA_TYPE]: dataType,
[URL_KEYS.SELECTED_METRIC]: selectedMetricField,
+ [URL_KEYS.HIDDEN]: hidden,
+ [URL_KEYS.NAME]: name,
+ [URL_KEYS.COLOR]: color,
...restSeries,
};
}
-export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
- const allSeriesIds = Object.keys(allSeries);
-
- const allShortSeries: AllShortSeries = {};
-
- allSeriesIds.forEach((seriesKey) => {
- allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]);
- });
+export function createExploratoryViewUrl(
+ { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries },
+ baseHref = ''
+) {
+ const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series));
return (
baseHref +
- `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}`
+ `/app/observability/exploratory-view/#?reportType=${reportType}&sr=${rison.encode(
+ allShortSeries as unknown as RisonValue
+ )}`
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index a3b5130e9830b..8f061fcbfbf26 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -6,12 +6,18 @@
*/
import React from 'react';
-import { screen, waitFor } from '@testing-library/dom';
+import { screen } from '@testing-library/dom';
import { render, mockAppIndexPattern } from './rtl_helpers';
import { ExploratoryView } from './exploratory_view';
import * as obsvInd from './utils/observability_index_patterns';
+import * as pluginHook from '../../../hooks/use_plugin_context';
import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs';
+jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
+ appMountParameters: {
+ setHeaderActionMenu: jest.fn(),
+ },
+} as any);
describe('ExploratoryView', () => {
mockAppIndexPattern();
@@ -40,36 +46,22 @@ describe('ExploratoryView', () => {
});
it('renders exploratory view', async () => {
- render();
+ render(, { initSeries: { data: [] } });
- expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
+ expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument();
+ expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument();
+ expect(await screen.findByText(/Refresh/i)).toBeInTheDocument();
expect(
await screen.findByRole('heading', { name: /Performance Distribution/i })
).toBeInTheDocument();
});
it('renders lens component when there is series', async () => {
- const initSeries = {
- data: {
- 'ux-series': {
- isNew: true,
- dataType: 'ux' as const,
- reportType: 'data-distribution' as const,
- breakdown: 'user_agent .name',
- reportDefinitions: { 'service.name': ['elastic-co'] },
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- render(, { initSeries });
+ render();
- expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument();
expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument();
- await waitFor(() => {
- screen.getByRole('table', { name: /this table contains 1 rows\./i });
- });
+ expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index af04108c56790..faf064868dec5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -4,11 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { i18n } from '@kbn/i18n';
import React, { useEffect, useRef, useState } from 'react';
-import { EuiPanel, EuiTitle } from '@elastic/eui';
+import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
-import { isEmpty } from 'lodash';
+import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
@@ -16,40 +17,15 @@ import { useSeriesStorage } from './hooks/use_series_storage';
import { useLensAttributes } from './hooks/use_lens_attributes';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
-import { SeriesBuilder } from './series_builder/series_builder';
-import { SeriesUrl } from './types';
+import { SeriesViews } from './views/series_views';
import { LensEmbeddable } from './lens_embeddable';
import { EmptyView } from './components/empty_view';
-export const combineTimeRanges = (
- allSeries: Record,
- firstSeries?: SeriesUrl
-) => {
- let to: string = '';
- let from: string = '';
- if (firstSeries?.reportType === 'kpi-over-time') {
- return firstSeries.time;
- }
- Object.values(allSeries ?? {}).forEach((series) => {
- if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) {
- const seriesTo = new Date(series.time.to);
- const seriesFrom = new Date(series.time.from);
- if (!to || seriesTo > new Date(to)) {
- to = series.time.to;
- }
- if (!from || seriesFrom < new Date(from)) {
- from = series.time.from;
- }
- }
- });
- return { to, from };
-};
+export type PanelId = 'seriesPanel' | 'chartPanel';
export function ExploratoryView({
saveAttributes,
- multiSeries,
}: {
- multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
const {
@@ -69,20 +45,19 @@ export function ExploratoryView({
const { loadIndexPattern, loading } = useAppIndexPatternContext();
- const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage();
+ const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage();
const lensAttributesT = useLensAttributes();
const setHeightOffset = () => {
if (seriesBuilderRef?.current && wrapperRef.current) {
const headerOffset = wrapperRef.current.getBoundingClientRect().top;
- const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height;
- setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`);
+ setHeight(`calc(100vh - ${headerOffset + 40}px)`);
}
};
useEffect(() => {
- Object.values(allSeries).forEach((seriesT) => {
+ allSeries.forEach((seriesT) => {
loadIndexPattern({
dataType: seriesT.dataType,
});
@@ -96,38 +71,102 @@ export function ExploratoryView({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(lensAttributesT ?? {})]);
+ }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]);
useEffect(() => {
setHeightOffset();
});
+ const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>();
+
+ const [hiddenPanel, setHiddenPanel] = useState('');
+
+ const onCollapse = (panelId: string) => {
+ setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId));
+ };
+
+ const onChange = (panelId: PanelId) => {
+ onCollapse(panelId);
+ if (collapseFn.current) {
+ collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left');
+ }
+ };
+
return (
{lens ? (
<>
-
+
- {lensAttributes ? (
-
- ) : (
-
+
+ {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
+ collapseFn.current = (id, direction) => togglePanel?.(id, { direction });
+
+ return (
+ <>
+
+ {lensAttributes ? (
+
+ ) : (
+
+ )}
+
+
+
+ {hiddenPanel === 'chartPanel' ? (
+ onChange('chartPanel')} iconType="arrowDown">
+ {SHOW_CHART_LABEL}
+
+ ) : (
+ onChange('chartPanel')}
+ iconType="arrowUp"
+ color="text"
+ >
+ {HIDE_CHART_LABEL}
+
+ )}
+
+
+ >
+ );
+ }}
+
+ {hiddenPanel === 'seriesPanel' && (
+ onChange('seriesPanel')} iconType="arrowUp">
+ {PREVIEW_LABEL}
+
)}
-
>
) : (
-
- {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', {
- defaultMessage:
- 'Lens app is not available, please enable Lens to use exploratory view.',
- })}
-
+ {LENS_NOT_AVAILABLE}
)}
@@ -147,4 +186,39 @@ const Wrapper = styled(EuiPanel)`
margin: 0 auto;
width: 100%;
overflow-x: auto;
+ position: relative;
+`;
+
+const ShowPreview = styled(EuiButtonEmpty)`
+ position: absolute;
+ bottom: 34px;
+`;
+const HideChart = styled(EuiButtonEmpty)`
+ position: absolute;
+ top: -35px;
+ right: 50px;
`;
+const ShowChart = styled(EuiButtonEmpty)`
+ position: absolute;
+ top: -10px;
+ right: 50px;
+`;
+
+const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', {
+ defaultMessage: 'Hide chart',
+});
+
+const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', {
+ defaultMessage: 'Show chart',
+});
+
+const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', {
+ defaultMessage: 'Preview',
+});
+
+const LENS_NOT_AVAILABLE = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.lensDisabled',
+ {
+ defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
index 619ea0d21ae15..b8f16f3e5effb 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx
@@ -23,14 +23,15 @@ describe('AddToCaseAction', function () {
it('should be able to click add to case button', async function () {
const initSeries = {
- data: {
- 'uptime-pings-histogram': {
+ data: [
+ {
+ name: 'test-series',
dataType: 'synthetics' as const,
reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
const { findByText, core } = render(
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
index 4fa8deb2700d0..bc813a4980e78 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx
@@ -17,7 +17,7 @@ import { Case, SubCase } from '../../../../../../cases/common';
import { observabilityFeatureId } from '../../../../../common';
export interface AddToCaseProps {
- timeRange: { from: string; to: string };
+ timeRange?: { from: string; to: string };
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
@@ -54,6 +54,7 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) {
return (
<>
);
- getByText('Open in Lens');
- });
-
- it('should be able to click open in lens', function () {
- const initSeries = {
- data: {
- 'uptime-pings-histogram': {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- const { getByText, core } = render(
- ,
- { initSeries }
- );
- fireEvent.click(getByText('Open in Lens'));
-
- expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
- expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
- {
- attributes: { title: 'Performance distribution' },
- id: '',
- timeRange: {
- from: 'now-15m',
- to: 'now',
- },
- },
- { openInNewTab: true }
- );
+ getByText('Refresh');
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 7adef4779ea94..bec8673f88b4e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -5,44 +5,37 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public';
-import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { TypedLensByValueInput } from '../../../../../../lens/public';
import { DataViewLabels } from '../configurations/constants';
-import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
-import { combineTimeRanges } from '../exploratory_view';
-import { AddToCaseAction } from './add_to_case_action';
+import { LastUpdated } from './last_updated';
+import { combineTimeRanges } from '../lens_embeddable';
+import { ExpViewActionMenu } from '../components/action_menu';
interface Props {
- seriesId: string;
+ seriesId?: number;
+ lastUpdated?: number;
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
-export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
- const kServices = useKibana().services;
+export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) {
+ const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage();
- const { lens } = kServices;
+ const series = seriesId ? getSeries(seriesId) : undefined;
- const { getSeries, allSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const [isSaveOpen, setIsSaveOpen] = useState(false);
-
- const LensSaveModalComponent = lens.SaveModalComponent;
-
- const timeRange = combineTimeRanges(allSeries, series);
+ const timeRange = combineTimeRanges(reportType, allSeries, series);
return (
<>
+
- {DataViewLabels[series.reportType] ??
+ {DataViewLabels[reportType] ??
i18n.translate('xpack.observability.expView.heading.label', {
defaultMessage: 'Analyze data',
})}{' '}
@@ -58,58 +51,18 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
-
+
- {
- if (lensAttributes) {
- lens.navigateToPrefilledEditor(
- {
- id: '',
- timeRange,
- attributes: lensAttributes,
- },
- {
- openInNewTab: true,
- }
- );
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.openInLens', {
- defaultMessage: 'Open in Lens',
- })}
-
-
-
- {
- if (lensAttributes) {
- setIsSaveOpen(true);
- }
- }}
- >
- {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
- defaultMessage: 'Save',
- })}
+ setLastRefresh(Date.now())}>
+ {REFRESH_LABEL}
-
- {isSaveOpen && lensAttributes && (
- setIsSaveOpen(false)}
- onSave={() => {}}
- />
- )}
>
);
}
+
+const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', {
+ defaultMessage: 'Refresh',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
similarity index 55%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
index 874171de123d2..c352ec0423dd8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx
@@ -8,6 +8,7 @@
import React, { useEffect, useState } from 'react';
import { EuiIcon, EuiText } from '@elastic/eui';
import moment from 'moment';
+import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
lastUpdated?: number;
@@ -18,20 +19,34 @@ export function LastUpdated({ lastUpdated }: Props) {
useEffect(() => {
const interVal = setInterval(() => {
setRefresh(Date.now());
- }, 1000);
+ }, 5000);
return () => {
clearInterval(interVal);
};
}, []);
+ useEffect(() => {
+ setRefresh(Date.now());
+ }, [lastUpdated]);
+
if (!lastUpdated) {
return null;
}
+ const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5;
+ const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10;
+
return (
-
- Last Updated: {moment(lastUpdated).from(refresh)}
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
index 5ec9e1d4ab4b5..d1e15aa916eed 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts
@@ -25,7 +25,7 @@ async function addToCase(
http: HttpSetup,
theCase: Case | SubCase,
attributes: TypedLensByValueInput['attributes'],
- timeRange: { from: string; to: string }
+ timeRange?: { from: string; to: string }
) {
const apiPath = `/api/cases/${theCase?.id}/comments`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
index 88818665bbe2a..83a7ac1ae17dc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
@@ -27,7 +27,7 @@ interface ProviderProps {
}
type HasAppDataState = Record;
-type IndexPatternState = Record;
+export type IndexPatternState = Record;
type LoadingState = Record;
export function IndexPatternContextProvider({ children }: ProviderProps) {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
new file mode 100644
index 0000000000000..4f19a8131f669
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useEffect, useState } from 'react';
+import { useKibana } from '../../../../utils/kibana_react';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { useAppIndexPatternContext } from './use_app_index_pattern';
+import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils';
+import { getFiltersFromDefs } from './use_lens_attributes';
+import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
+
+interface UseDiscoverLink {
+ seriesConfig?: SeriesConfig;
+ series: SeriesUrl;
+}
+
+export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => {
+ const kServices = useKibana().services;
+ const {
+ application: { navigateToUrl },
+ } = kServices;
+
+ const { indexPatterns } = useAppIndexPatternContext();
+
+ const urlGenerator = kServices.discover?.urlGenerator;
+ const [discoverUrl, setDiscoverUrl] = useState('');
+
+ useEffect(() => {
+ const indexPattern = indexPatterns?.[series.dataType];
+
+ const definitions = series.reportDefinitions ?? {};
+ const filters = [...(seriesConfig?.baseFilters ?? [])];
+
+ const definitionFilters = getFiltersFromDefs(definitions);
+
+ definitionFilters.forEach(({ field, values = [] }) => {
+ if (values.length > 1) {
+ filters.push(buildPhrasesFilter(field, values, indexPattern)[0]);
+ } else {
+ filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]);
+ }
+ });
+
+ const selectedMetricField = series.selectedMetricField;
+
+ if (
+ selectedMetricField &&
+ selectedMetricField !== RECORDS_FIELD &&
+ selectedMetricField !== RECORDS_PERCENTAGE_FIELD
+ ) {
+ filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]);
+ }
+
+ const getDiscoverUrl = async () => {
+ if (!urlGenerator?.createUrl) return;
+
+ const newUrl = await urlGenerator.createUrl({
+ filters,
+ indexPatternId: indexPattern?.id,
+ });
+ setDiscoverUrl(newUrl);
+ };
+ getDiscoverUrl();
+ }, [
+ indexPatterns,
+ series.dataType,
+ series.reportDefinitions,
+ series.selectedMetricField,
+ seriesConfig?.baseFilters,
+ urlGenerator,
+ ]);
+
+ const onClick = useCallback(
+ (event: React.MouseEvent) => {
+ if (discoverUrl) {
+ event.preventDefault();
+
+ return navigateToUrl(discoverUrl);
+ }
+ },
+ [discoverUrl, navigateToUrl]
+ );
+
+ return {
+ href: discoverUrl,
+ onClick,
+ };
+};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 8bb265b4f6d89..ef974d54e6cdc 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -9,12 +9,18 @@ import { useMemo } from 'react';
import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../../lens/public';
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
-import { useSeriesStorage } from './use_series_storage';
+import {
+ AllSeries,
+ allSeriesKey,
+ convertAllShortSeries,
+ useSeriesStorage,
+} from './use_series_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { SeriesUrl, UrlFilter } from '../types';
import { useAppIndexPatternContext } from './use_app_index_pattern';
import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox';
+import { useTheme } from '../../../../hooks/use_theme';
export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => {
return Object.entries(reportDefinitions ?? {})
@@ -28,41 +34,54 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio
};
export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
- const { allSeriesIds, allSeries } = useSeriesStorage();
+ const { storage, allSeries, lastRefresh, reportType } = useSeriesStorage();
const { indexPatterns } = useAppIndexPatternContext();
+ const theme = useTheme();
+
return useMemo(() => {
- if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
+ if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) {
return null;
}
+ const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []);
+
const layerConfigs: LayerConfig[] = [];
- allSeriesIds.forEach((seriesIdT) => {
- const seriesT = allSeries[seriesIdT];
- const indexPattern = indexPatterns?.[seriesT?.dataType];
- if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
+ allSeriesT.forEach((series, seriesIndex) => {
+ const indexPattern = indexPatterns?.[series?.dataType];
+
+ if (
+ indexPattern &&
+ !isEmpty(series.reportDefinitions) &&
+ !series.hidden &&
+ series.selectedMetricField
+ ) {
const seriesConfig = getDefaultConfigs({
- reportType: seriesT.reportType,
- dataType: seriesT.dataType,
+ reportType,
indexPattern,
+ dataType: series.dataType,
});
- const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
- getFiltersFromDefs(seriesT.reportDefinitions)
+ const filters: UrlFilter[] = (series.filters ?? []).concat(
+ getFiltersFromDefs(series.reportDefinitions)
);
+ const color = `euiColorVis${seriesIndex}`;
+
layerConfigs.push({
filters,
indexPattern,
seriesConfig,
- time: seriesT.time,
- breakdown: seriesT.breakdown,
- seriesType: seriesT.seriesType,
- operationType: seriesT.operationType,
- reportDefinitions: seriesT.reportDefinitions ?? {},
- selectedMetricField: seriesT.selectedMetricField,
+ time: series.time,
+ name: series.name,
+ breakdown: series.breakdown,
+ seriesType: series.seriesType,
+ operationType: series.operationType,
+ reportDefinitions: series.reportDefinitions ?? {},
+ selectedMetricField: series.selectedMetricField,
+ color: series.color ?? (theme.eui as unknown as Record)[color],
});
}
});
@@ -73,6 +92,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null
const lensAttributes = new LensAttributes(layerConfigs);
- return lensAttributes.getJSON();
- }, [indexPatterns, allSeriesIds, allSeries]);
+ return lensAttributes.getJSON(lastRefresh);
+ }, [indexPatterns, allSeries, reportType, storage, theme, lastRefresh]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
index 2d2618bc46152..f2a6130cdc59d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts
@@ -6,18 +6,16 @@
*/
import { useSeriesStorage } from './use_series_storage';
-import { UrlFilter } from '../types';
+import { SeriesUrl, UrlFilter } from '../types';
export interface UpdateFilter {
field: string;
- value: string;
+ value: string | string[];
negate?: boolean;
}
-export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => {
+ const { setSeries } = useSeriesStorage();
const filters = series.filters ?? [];
@@ -26,10 +24,14 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
.map((filter) => {
if (filter.field === field) {
if (negate) {
- const notValuesN = filter.notValues?.filter((val) => val !== value);
+ const notValuesN = filter.notValues?.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
return { ...filter, notValues: notValuesN };
} else {
- const valuesN = filter.values?.filter((val) => val !== value);
+ const valuesN = filter.values?.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
return { ...filter, values: valuesN };
}
}
@@ -43,9 +45,9 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
const addFilter = ({ field, value, negate }: UpdateFilter) => {
const currFilter: UrlFilter = { field };
if (negate) {
- currFilter.notValues = [value];
+ currFilter.notValues = value instanceof Array ? value : [value];
} else {
- currFilter.values = [value];
+ currFilter.values = value instanceof Array ? value : [value];
}
if (filters.length === 0) {
setSeries(seriesId, { ...series, filters: [currFilter] });
@@ -65,13 +67,26 @@ export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
const currNotValues = currFilter.notValues ?? [];
const currValues = currFilter.values ?? [];
- const notValues = currNotValues.filter((val) => val !== value);
- const values = currValues.filter((val) => val !== value);
+ const notValues = currNotValues.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
+
+ const values = currValues.filter((val) =>
+ value instanceof Array ? !value.includes(val) : val !== value
+ );
if (negate) {
- notValues.push(value);
+ if (value instanceof Array) {
+ notValues.push(...value);
+ } else {
+ notValues.push(value);
+ }
} else {
- values.push(value);
+ if (value instanceof Array) {
+ values.push(...value);
+ } else {
+ values.push(value);
+ }
}
currFilter.notValues = notValues.length > 0 ? notValues : undefined;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
index c32acc47abd1b..ce6d7bd94d8e4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx
@@ -6,37 +6,39 @@
*/
import React, { useEffect } from 'react';
-
-import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage';
+import { Route, Router } from 'react-router-dom';
import { render } from '@testing-library/react';
+import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage';
+import { getHistoryFromUrl } from '../rtl_helpers';
-const mockSingleSeries = {
- 'performance-distribution': {
- reportType: 'data-distribution',
+const mockSingleSeries = [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
-};
+];
-const mockMultipleSeries = {
- 'performance-distribution': {
- reportType: 'data-distribution',
+const mockMultipleSeries = [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- 'kpi-over-time': {
- reportType: 'kpi-over-time',
+ {
+ name: 'kpi-over-time',
dataType: 'synthetics',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
-};
+];
-describe('userSeries', function () {
+describe('userSeriesStorage', function () {
function setupTestComponent(seriesData: any) {
const setData = jest.fn();
+
function TestComponent() {
const data = useSeriesStorage();
@@ -48,11 +50,20 @@ describe('userSeries', function () {
}
render(
-
-
-
+
+
+ (key === 'sr' ? seriesData : null)),
+ set: jest.fn(),
+ }}
+ >
+
+
+
+
);
return setData;
@@ -63,22 +74,20 @@ describe('userSeries', function () {
expect(setData).toHaveBeenCalledTimes(2);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {
- 'performance-distribution': {
- breakdown: 'user_agent.name',
+ allSeries: [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- },
- allSeriesIds: ['performance-distribution'],
+ ],
firstSeries: {
- breakdown: 'user_agent.name',
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- firstSeriesId: 'performance-distribution',
})
);
});
@@ -89,42 +98,38 @@ describe('userSeries', function () {
expect(setData).toHaveBeenCalledTimes(2);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {
- 'performance-distribution': {
- breakdown: 'user_agent.name',
+ allSeries: [
+ {
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- 'kpi-over-time': {
- reportType: 'kpi-over-time',
+ {
+ name: 'kpi-over-time',
dataType: 'synthetics',
breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- },
- allSeriesIds: ['performance-distribution', 'kpi-over-time'],
+ ],
firstSeries: {
- breakdown: 'user_agent.name',
+ name: 'performance-distribution',
dataType: 'ux',
- reportType: 'data-distribution',
+ breakdown: 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
},
- firstSeriesId: 'performance-distribution',
})
);
});
it('should return expected result when there are no series', function () {
- const setData = setupTestComponent({});
+ const setData = setupTestComponent([]);
- expect(setData).toHaveBeenCalledTimes(2);
+ expect(setData).toHaveBeenCalledTimes(1);
expect(setData).toHaveBeenLastCalledWith(
expect.objectContaining({
- allSeries: {},
- allSeriesIds: [],
+ allSeries: [],
firstSeries: undefined,
- firstSeriesId: undefined,
})
);
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
index a47a124d14b4d..d9a5adc822140 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
@@ -22,13 +22,17 @@ import { OperationType, SeriesType } from '../../../../../../lens/public';
import { URL_KEYS } from '../configurations/constants/url_constants';
export interface SeriesContextValue {
- firstSeries: SeriesUrl;
- firstSeriesId: string;
- allSeriesIds: string[];
+ firstSeries?: SeriesUrl;
+ lastRefresh: number;
+ setLastRefresh: (val: number) => void;
+ applyChanges: () => void;
allSeries: AllSeries;
- setSeries: (seriesIdN: string, newValue: SeriesUrl) => void;
- getSeries: (seriesId: string) => SeriesUrl;
- removeSeries: (seriesId: string) => void;
+ setSeries: (seriesIndex: number, newValue: SeriesUrl) => void;
+ getSeries: (seriesIndex: number) => SeriesUrl | undefined;
+ removeSeries: (seriesIndex: number) => void;
+ setReportType: (reportType: string) => void;
+ storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
+ reportType: ReportViewType;
}
export const UrlStorageContext = createContext({} as SeriesContextValue);
@@ -36,72 +40,87 @@ interface ProviderProps {
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
}
-function convertAllShortSeries(allShortSeries: AllShortSeries) {
- const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = {};
- allSeriesIds.forEach((seriesKey) => {
- allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
- });
-
- return allSeriesN;
+export function convertAllShortSeries(allShortSeries: AllShortSeries) {
+ return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries));
}
+export const allSeriesKey = 'sr';
+const reportTypeKey = 'reportType';
+
export function UrlStorageContextProvider({
children,
storage,
}: ProviderProps & { children: JSX.Element }) {
- const allSeriesKey = 'sr';
-
- const [allShortSeries, setAllShortSeries] = useState(
- () => storage.get(allSeriesKey) ?? {}
- );
const [allSeries, setAllSeries] = useState(() =>
- convertAllShortSeries(storage.get(allSeriesKey) ?? {})
+ convertAllShortSeries(storage.get(allSeriesKey) ?? [])
);
- const [firstSeriesId, setFirstSeriesId] = useState('');
+
+ const [lastRefresh, setLastRefresh] = useState(() => Date.now());
+
+ const [reportType, setReportType] = useState(
+ () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? ''
+ );
+
const [firstSeries, setFirstSeries] = useState();
useEffect(() => {
- const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
+ const firstSeriesT = allSeries?.[0];
- setAllSeries(allSeriesN);
- setFirstSeriesId(allSeriesIds?.[0]);
- setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]);
- (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
- }, [allShortSeries, storage]);
+ setFirstSeries(firstSeriesT);
+ }, [allSeries, storage]);
- const setSeries = (seriesIdN: string, newValue: SeriesUrl) => {
- setAllShortSeries((prevState) => {
- prevState[seriesIdN] = convertToShortUrl(newValue);
- return { ...prevState };
- });
- };
+ const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => {
+ setAllSeries((prevAllSeries) => {
+ const newStateRest = prevAllSeries.map((series, index) => {
+ if (index === seriesIndex) {
+ return newValue;
+ }
+ return series;
+ });
+
+ if (prevAllSeries.length === seriesIndex) {
+ return [...newStateRest, newValue];
+ }
- const removeSeries = (seriesIdN: string) => {
- setAllShortSeries((prevState) => {
- delete prevState[seriesIdN];
- return { ...prevState };
+ return [...newStateRest];
});
- };
+ }, []);
- const allSeriesIds = Object.keys(allShortSeries);
+ useEffect(() => {
+ (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType);
+ }, [reportType, storage]);
+
+ const removeSeries = useCallback((seriesIndex: number) => {
+ setAllSeries((prevAllSeries) =>
+ prevAllSeries.filter((seriesT, index) => index !== seriesIndex)
+ );
+ }, []);
const getSeries = useCallback(
- (seriesId?: string) => {
- return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl);
+ (seriesIndex: number) => {
+ return allSeries[seriesIndex];
},
[allSeries]
);
+ const applyChanges = useCallback(() => {
+ const allShortSeries = allSeries.map((series) => convertToShortUrl(series));
+
+ (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
+ setLastRefresh(Date.now());
+ }, [allSeries, storage]);
+
const value = {
+ applyChanges,
storage,
getSeries,
setSeries,
removeSeries,
- firstSeriesId,
allSeries,
- allSeriesIds,
+ lastRefresh,
+ setLastRefresh,
+ setReportType,
+ reportType: storage.get(reportTypeKey) as ReportViewType,
firstSeries: firstSeries!,
};
return {children};
@@ -112,10 +131,9 @@ export function useSeriesStorage() {
}
function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
- const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue;
+ const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue;
return {
operationType: op,
- reportType: rt!,
seriesType: st,
breakdown: bd,
filters: ft!,
@@ -123,26 +141,31 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
reportDefinitions: rdf,
dataType: dt!,
selectedMetricField: mt,
+ hidden: h,
+ name: n,
+ color: c,
...restSeries,
};
}
interface ShortUrlSeries {
[URL_KEYS.OPERATION_TYPE]?: OperationType;
- [URL_KEYS.REPORT_TYPE]?: ReportViewType;
[URL_KEYS.DATA_TYPE]?: AppDataType;
[URL_KEYS.SERIES_TYPE]?: SeriesType;
[URL_KEYS.BREAK_DOWN]?: string;
[URL_KEYS.FILTERS]?: UrlFilter[];
[URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition;
[URL_KEYS.SELECTED_METRIC]?: string;
+ [URL_KEYS.HIDDEN]?: boolean;
+ [URL_KEYS.NAME]: string;
+ [URL_KEYS.COLOR]?: string;
time?: {
to: string;
from: string;
};
}
-export type AllShortSeries = Record;
-export type AllSeries = Record;
+export type AllShortSeries = ShortUrlSeries[];
+export type AllSeries = SeriesUrl[];
-export const NEW_SERIES_KEY = 'new-series-key';
+export const NEW_SERIES_KEY = 'new-series';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
index e55752ceb62ba..3de29b02853e8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
@@ -25,11 +25,9 @@ import { TypedLensByValueInput } from '../../../../../lens/public';
export function ExploratoryViewPage({
saveAttributes,
- multiSeries = false,
useSessionStorage = false,
}: {
useSessionStorage?: boolean;
- multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' });
@@ -61,7 +59,7 @@ export function ExploratoryViewPage({
-
+
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
index 4cb586fe94ceb..9e4d9486dc155 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx
@@ -7,16 +7,51 @@
import { i18n } from '@kbn/i18n';
import React, { Dispatch, SetStateAction, useCallback } from 'react';
-import { combineTimeRanges } from './exploratory_view';
+import styled from 'styled-components';
+import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useSeriesStorage } from './hooks/use_series_storage';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { ReportViewType, SeriesUrl } from './types';
+import { ReportTypes } from './configurations/constants';
interface Props {
lensAttributes: TypedLensByValueInput['attributes'];
setLastUpdated: Dispatch>;
}
+export const combineTimeRanges = (
+ reportType: ReportViewType,
+ allSeries: SeriesUrl[],
+ firstSeries?: SeriesUrl
+) => {
+ let to: string = '';
+ let from: string = '';
+
+ if (reportType === ReportTypes.KPI) {
+ return firstSeries?.time;
+ }
+
+ allSeries.forEach((series) => {
+ if (
+ series.dataType &&
+ series.selectedMetricField &&
+ !isEmpty(series.reportDefinitions) &&
+ series.time
+ ) {
+ const seriesTo = new Date(series.time.to);
+ const seriesFrom = new Date(series.time.from);
+ if (!to || seriesTo > new Date(to)) {
+ to = series.time.to;
+ }
+ if (!from || seriesFrom < new Date(from)) {
+ from = series.time.from;
+ }
+ }
+ });
+
+ return { to, from };
+};
export function LensEmbeddable(props: Props) {
const { lensAttributes, setLastUpdated } = props;
@@ -27,9 +62,11 @@ export function LensEmbeddable(props: Props) {
const LensComponent = lens?.EmbeddableComponent;
- const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage();
+ const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage();
- const timeRange = combineTimeRanges(allSeries, series);
+ const firstSeriesId = 0;
+
+ const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null;
const onLensLoad = useCallback(() => {
setLastUpdated(Date.now());
@@ -37,9 +74,9 @@ export function LensEmbeddable(props: Props) {
const onBrushEnd = useCallback(
({ range }: { range: number[] }) => {
- if (series?.reportType !== 'data-distribution') {
+ if (reportType !== 'data-distribution' && firstSeries) {
setSeries(firstSeriesId, {
- ...series,
+ ...firstSeries,
time: {
from: new Date(range[0]).toISOString(),
to: new Date(range[1]).toISOString(),
@@ -53,16 +90,30 @@ export function LensEmbeddable(props: Props) {
);
}
},
- [notifications?.toasts, series, firstSeriesId, setSeries]
+ [reportType, setSeries, firstSeries, notifications?.toasts]
);
+ if (timeRange === null || !firstSeries) {
+ return null;
+ }
+
return (
-
+
+
+
);
}
+
+const LensWrapper = styled.div`
+ height: 100%;
+
+ &&& > div {
+ height: 100%;
+ }
+`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index a577a8df3e3d9..48a22f91eb7f6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -10,7 +10,7 @@ import React, { ReactElement } from 'react';
import { stringify } from 'query-string';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render as reactTestLibRender, RenderOptions } from '@testing-library/react';
-import { Router } from 'react-router-dom';
+import { Route, Router } from 'react-router-dom';
import { createMemoryHistory, History } from 'history';
import { CoreStart } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react';
@@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/
import { lensPluginMock } from '../../../../../lens/public/mocks';
import * as useAppIndexPatternHook from './hooks/use_app_index_pattern';
import { IndexPatternContextProvider } from './hooks/use_app_index_pattern';
-import { AllSeries, UrlStorageContext } from './hooks/use_series_storage';
+import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage';
import * as fetcherHook from '../../../hooks/use_fetcher';
import * as useSeriesFilterHook from './hooks/use_series_filters';
@@ -35,10 +35,12 @@ import indexPatternData from './configurations/test_data/test_index_pattern.json
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
+
+import { AppDataType, SeriesUrl, UrlFilter } from './types';
import { createStubIndexPattern } from '../../../../../../../src/plugins/data/common/stubs';
-import { AppDataType, UrlFilter } from './types';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { ListItem } from '../../../hooks/use_values_list';
+import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames';
import { casesPluginMock } from '../../../../../cases/public/mocks';
interface KibanaProps {
@@ -157,9 +159,11 @@ export function MockRouter({
}: MockRouterProps) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
@@ -172,7 +176,7 @@ export function render(
core: customCore,
kibanaProps,
renderOptions,
- url,
+ url = '/app/observability/exploratory-view/',
initSeries = {},
}: RenderRouterOptions = {}
) {
@@ -202,7 +206,7 @@ export function render(
};
}
-const getHistoryFromUrl = (url: Url) => {
+export const getHistoryFromUrl = (url: Url) => {
if (typeof url === 'string') {
return createMemoryHistory({
initialEntries: [url],
@@ -251,6 +255,15 @@ export const mockUseValuesList = (values?: ListItem[]) => {
return { spy, onRefreshTimeRange };
};
+export const mockUxSeries = {
+ name: 'performance-distribution',
+ dataType: 'ux',
+ breakdown: 'user_agent.name',
+ time: { from: 'now-15m', to: 'now' },
+ reportDefinitions: { 'service.name': ['elastic-co'] },
+ selectedMetricField: TRANSACTION_DURATION,
+} as SeriesUrl;
+
function mockSeriesStorageContext({
data,
filters,
@@ -260,34 +273,34 @@ function mockSeriesStorageContext({
filters?: UrlFilter[];
breakdown?: string;
}) {
- const mockDataSeries = data || {
- 'performance-distribution': {
- reportType: 'data-distribution',
- dataType: 'ux',
- breakdown: breakdown || 'user_agent.name',
- time: { from: 'now-15m', to: 'now' },
- ...(filters ? { filters } : {}),
- },
+ const testSeries = {
+ ...mockUxSeries,
+ breakdown: breakdown || 'user_agent.name',
+ ...(filters ? { filters } : {}),
};
- const allSeriesIds = Object.keys(mockDataSeries);
- const firstSeriesId = allSeriesIds?.[0];
- const series = mockDataSeries[firstSeriesId];
+ const mockDataSeries = data || [testSeries];
const removeSeries = jest.fn();
const setSeries = jest.fn();
- const getSeries = jest.fn().mockReturnValue(series);
+ const getSeries = jest.fn().mockReturnValue(testSeries);
return {
- firstSeriesId,
- allSeriesIds,
removeSeries,
setSeries,
getSeries,
- firstSeries: mockDataSeries[firstSeriesId],
+ autoApply: true,
+ reportType: 'data-distribution',
+ lastRefresh: Date.now(),
+ setLastRefresh: jest.fn(),
+ setAutoApply: jest.fn(),
+ applyChanges: jest.fn(),
+ firstSeries: mockDataSeries[0],
allSeries: mockDataSeries,
- };
+ setReportType: jest.fn(),
+ storage: { get: jest.fn().mockReturnValue(mockDataSeries) } as any,
+ } as SeriesContextValue;
}
export function mockUseSeriesFilter() {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
deleted file mode 100644
index b10702ebded57..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { fireEvent, screen } from '@testing-library/react';
-import { mockAppIndexPattern, render } from '../../rtl_helpers';
-import { dataTypes, DataTypesCol } from './data_types_col';
-
-describe('DataTypesCol', function () {
- const seriesId = 'test-series-id';
-
- mockAppIndexPattern();
-
- it('should render properly', function () {
- const { getByText } = render();
-
- dataTypes.forEach(({ label }) => {
- getByText(label);
- });
- });
-
- it('should set series on change', function () {
- const { setSeries } = render();
-
- fireEvent.click(screen.getByText(/user experience \(rum\)/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'ux',
- isNew: true,
- time: {
- from: 'now-15m',
- to: 'now',
- },
- });
- });
-
- it('should set series on change on already selected', function () {
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- },
- },
- };
-
- render(, { initSeries });
-
- const button = screen.getByRole('button', {
- name: /Synthetic Monitoring/i,
- });
-
- expect(button.classList).toContain('euiButton--fill');
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
deleted file mode 100644
index f386f62d9ed73..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import styled from 'styled-components';
-import { AppDataType } from '../../types';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-
-export const dataTypes: Array<{ id: AppDataType; label: string }> = [
- { id: 'synthetics', label: 'Synthetic Monitoring' },
- { id: 'ux', label: 'User Experience (RUM)' },
- { id: 'mobile', label: 'Mobile Experience' },
- // { id: 'infra_logs', label: 'Logs' },
- // { id: 'infra_metrics', label: 'Metrics' },
- // { id: 'apm', label: 'APM' },
-];
-
-export function DataTypesCol({ seriesId }: { seriesId: string }) {
- const { getSeries, setSeries, removeSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
- const { loading } = useAppIndexPatternContext();
-
- const onDataTypeChange = (dataType?: AppDataType) => {
- if (!dataType) {
- removeSeries(seriesId);
- } else {
- setSeries(seriesId || `${dataType}-series`, {
- dataType,
- isNew: true,
- time: series.time,
- } as any);
- }
- };
-
- const selectedDataType = series.dataType;
-
- return (
-
- {dataTypes.map(({ id: dataTypeId, label }) => (
-
-
-
- ))}
-
- );
-}
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
-
-const Button = styled(EuiButton)`
- will-change: transform;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
deleted file mode 100644
index 6be78084ae195..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import styled from 'styled-components';
-import { SeriesDatePicker } from '../../series_date_picker';
-import { DateRangePicker } from '../../series_date_picker/date_range_picker';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-
-interface Props {
- seriesId: string;
-}
-export function DatePickerCol({ seriesId }: Props) {
- const { firstSeriesId, getSeries } = useSeriesStorage();
- const { reportType } = getSeries(firstSeriesId);
-
- return (
-
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
-
- ) : (
-
- )}
-
- );
-}
-
-const Wrapper = styled.div`
- .euiSuperDatePicker__flexWrapper {
- width: 100%;
- > .euiFlexItem {
- margin-right: 0px;
- }
- }
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
deleted file mode 100644
index a5e5ad3900ded..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { fireEvent, screen } from '@testing-library/react';
-import { getDefaultConfigs } from '../../configurations/default_configs';
-import { mockIndexPattern, render } from '../../rtl_helpers';
-import { ReportBreakdowns } from './report_breakdowns';
-import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
-
-describe('Series Builder ReportBreakdowns', function () {
- const seriesId = 'test-series-id';
- const dataViewSeries = getDefaultConfigs({
- reportType: 'data-distribution',
- dataType: 'ux',
- indexPattern: mockIndexPattern,
- });
-
- it('should render properly', function () {
- render();
-
- screen.getByText('Select an option: , is selected');
- screen.getAllByText('Browser family');
- });
-
- it('should set new series breakdown on change', function () {
- const { setSeries } = render(
-
- );
-
- const btn = screen.getByRole('button', {
- name: /select an option: Browser family , is selected/i,
- hidden: true,
- });
-
- fireEvent.click(btn);
-
- fireEvent.click(screen.getByText(/operating system/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- breakdown: USER_AGENT_OS,
- dataType: 'ux',
- reportType: 'data-distribution',
- time: { from: 'now-15m', to: 'now' },
- });
- });
- it('should set undefined on new series on no select breakdown', function () {
- const { setSeries } = render(
-
- );
-
- const btn = screen.getByRole('button', {
- name: /select an option: Browser family , is selected/i,
- hidden: true,
- });
-
- fireEvent.click(btn);
-
- fireEvent.click(screen.getByText(/no breakdown/i));
-
- expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- breakdown: undefined,
- dataType: 'ux',
- reportType: 'data-distribution',
- time: { from: 'now-15m', to: 'now' },
- });
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
deleted file mode 100644
index fa2d01691ce1d..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { Breakdowns } from '../../series_editor/columns/breakdowns';
-import { SeriesConfig } from '../../types';
-
-export function ReportBreakdowns({
- seriesId,
- seriesConfig,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- return (
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
deleted file mode 100644
index 7962bf2b924f7..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
-import styled from 'styled-components';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { ReportMetricOptions } from '../report_metric_options';
-import { SeriesConfig } from '../../types';
-import { SeriesChartTypesSelect } from './chart_types';
-import { OperationTypeSelect } from './operation_type_select';
-import { DatePickerCol } from './date_picker_col';
-import { parseCustomFieldName } from '../../configurations/lens_attributes';
-import { ReportDefinitionField } from './report_definition_field';
-
-function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
- const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
-
- return columnType;
-}
-
-export function ReportDefinitionCol({
- seriesConfig,
- seriesId,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {};
-
- const { definitionFields, defaultSeriesType, hasOperationType, yAxisColumns, metricOptions } =
- seriesConfig;
-
- const onChange = (field: string, value?: string[]) => {
- if (!value?.[0]) {
- delete selectedReportDefinitions[field];
- setSeries(seriesId, {
- ...series,
- reportDefinitions: { ...selectedReportDefinitions },
- });
- } else {
- setSeries(seriesId, {
- ...series,
- reportDefinitions: { ...selectedReportDefinitions, [field]: value },
- });
- }
- };
-
- const columnType = getColumnType(seriesConfig, selectedMetricField);
-
- return (
-
-
-
-
-
- {definitionFields.map((field) => (
-
-
-
- ))}
- {metricOptions && (
-
-
-
- )}
- {(hasOperationType || columnType === 'operation') && (
-
-
-
- )}
-
-
-
-
- );
-}
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
deleted file mode 100644
index 0b183b5f20c03..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { screen } from '@testing-library/react';
-import { ReportFilters } from './report_filters';
-import { getDefaultConfigs } from '../../configurations/default_configs';
-import { mockIndexPattern, render } from '../../rtl_helpers';
-
-describe('Series Builder ReportFilters', function () {
- const seriesId = 'test-series-id';
-
- const dataViewSeries = getDefaultConfigs({
- reportType: 'data-distribution',
- indexPattern: mockIndexPattern,
- dataType: 'ux',
- });
-
- it('should render properly', function () {
- render();
-
- screen.getByText('Add filter');
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
deleted file mode 100644
index d5938c5387e8f..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { SeriesFilter } from '../../series_editor/columns/series_filter';
-import { SeriesConfig } from '../../types';
-
-export function ReportFilters({
- seriesConfig,
- seriesId,
-}: {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}) {
- return (
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
deleted file mode 100644
index 12ae8560453c9..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { fireEvent, screen } from '@testing-library/react';
-import { mockAppIndexPattern, render } from '../../rtl_helpers';
-import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
-import { ReportTypes } from '../series_builder';
-import { DEFAULT_TIME } from '../../configurations/constants';
-
-describe('ReportTypesCol', function () {
- const seriesId = 'performance-distribution';
-
- mockAppIndexPattern();
-
- it('should render properly', function () {
- render();
- screen.getByText('Performance distribution');
- screen.getByText('KPI over time');
- });
-
- it('should display empty message', function () {
- render();
- screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT);
- });
-
- it('should set series on change', function () {
- const { setSeries } = render(
-
- );
-
- fireEvent.click(screen.getByText(/KPI over time/i));
-
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'ux',
- selectedMetricField: undefined,
- reportType: 'kpi-over-time',
- time: { from: 'now-15m', to: 'now' },
- });
- expect(setSeries).toHaveBeenCalledTimes(1);
- });
-
- it('should set selected as filled', function () {
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'synthetics' as const,
- reportType: 'kpi-over-time' as const,
- breakdown: 'monitor.status',
- time: { from: 'now-15m', to: 'now' },
- isNew: true,
- },
- },
- };
-
- const { setSeries } = render(
- ,
- { initSeries }
- );
-
- const button = screen.getByRole('button', {
- name: /KPI over time/i,
- });
-
- expect(button.classList).toContain('euiButton--fill');
- fireEvent.click(button);
-
- // undefined on click selected
- expect(setSeries).toHaveBeenCalledWith(seriesId, {
- dataType: 'synthetics',
- time: DEFAULT_TIME,
- isNew: true,
- });
- });
-});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
deleted file mode 100644
index c4eebbfaca3eb..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { map } from 'lodash';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import styled from 'styled-components';
-import { ReportViewType, SeriesUrl } from '../../types';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { DEFAULT_TIME } from '../../configurations/constants';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { ReportTypeItem } from '../series_builder';
-
-interface Props {
- seriesId: string;
- reportTypes: ReportTypeItem[];
-}
-
-export function ReportTypesCol({ seriesId, reportTypes }: Props) {
- const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
-
- const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
-
- const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
-
- if (!restSeries.dataType) {
- return (
-
- );
- }
-
- if (!loading && !hasData) {
- return (
-
- );
- }
-
- const disabledReportTypes: ReportViewType[] = map(
- reportTypes.filter(
- ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType
- ),
- 'reportType'
- );
-
- return reportTypes?.length > 0 ? (
-
- {reportTypes.map(({ reportType, label }) => (
-
-
-
- ))}
-
- ) : (
- {SELECTED_DATA_TYPE_FOR_REPORT}
- );
-}
-
-export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate(
- 'xpack.observability.expView.reportType.noDataType',
- { defaultMessage: 'No data type selected.' }
-);
-
-const FlexGroup = styled(EuiFlexGroup)`
- width: 100%;
-`;
-
-const Button = styled(EuiButton)`
- will-change: transform;
-`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
deleted file mode 100644
index a2a3e34c21834..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiSuperSelect } from '@elastic/eui';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { SeriesConfig } from '../types';
-
-interface Props {
- seriesId: string;
- defaultValue?: string;
- options: SeriesConfig['metricOptions'];
-}
-
-export function ReportMetricOptions({ seriesId, options: opts }: Props) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const onChange = (value: string) => {
- setSeries(seriesId, {
- ...series,
- selectedMetricField: value,
- });
- };
-
- const options = opts ?? [];
-
- return (
- ({
- value: fd || id,
- inputDisplay: label,
- }))}
- valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id}
- onChange={(value) => onChange(value)}
- />
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
deleted file mode 100644
index 684cf3a210a51..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { RefObject, useEffect, useState } from 'react';
-import { isEmpty } from 'lodash';
-import { i18n } from '@kbn/i18n';
-import {
- EuiBasicTable,
- EuiButton,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiSwitch,
-} from '@elastic/eui';
-import { rgba } from 'polished';
-import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types';
-import { DataTypesCol } from './columns/data_types_col';
-import { ReportTypesCol } from './columns/report_types_col';
-import { ReportDefinitionCol } from './columns/report_definition_col';
-import { ReportFilters } from './columns/report_filters';
-import { ReportBreakdowns } from './columns/report_breakdowns';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { getDefaultConfigs } from '../configurations/default_configs';
-import { SeriesEditor } from '../series_editor/series_editor';
-import { SeriesActions } from '../series_editor/columns/series_actions';
-import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
-import { LastUpdated } from './last_updated';
-import {
- CORE_WEB_VITALS_LABEL,
- DEVICE_DISTRIBUTION_LABEL,
- KPI_OVER_TIME_LABEL,
- PERF_DIST_LABEL,
-} from '../configurations/constants/labels';
-
-export interface ReportTypeItem {
- id: string;
- reportType: ReportViewType;
- label: string;
-}
-
-export const ReportTypes: Record = {
- synthetics: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- ],
- ux: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
- ],
- mobile: [
- { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
- { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
- ],
- apm: [],
- infra_logs: [],
- infra_metrics: [],
-};
-
-interface BuilderItem {
- id: string;
- series: SeriesUrl;
- seriesConfig?: SeriesConfig;
-}
-
-export function SeriesBuilder({
- seriesBuilderRef,
- lastUpdated,
- multiSeries,
-}: {
- seriesBuilderRef: RefObject;
- lastUpdated?: number;
- multiSeries?: boolean;
-}) {
- const [editorItems, setEditorItems] = useState([]);
- const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
-
- const { loading, indexPatterns } = useAppIndexPatternContext();
-
- useEffect(() => {
- const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
- if (indexPatterns?.[dataType]) {
- return getDefaultConfigs({
- dataType,
- indexPattern: indexPatterns[dataType],
- reportType: reportType!,
- });
- }
- };
-
- const seriesToEdit: BuilderItem[] =
- allSeriesIds
- .filter((sId) => {
- return allSeries?.[sId]?.isNew;
- })
- .map((sId) => {
- const series = getSeries(sId);
- const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
-
- return { id: sId, series, seriesConfig };
- }) ?? [];
- const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }];
- setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries);
- }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]);
-
- const columns = [
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
- defaultMessage: 'Data Type',
- }),
- field: 'id',
- width: '15%',
- render: (seriesId: string) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
- defaultMessage: 'Report',
- }),
- width: '15%',
- field: 'id',
- render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', {
- defaultMessage: 'Definition',
- }),
- width: '30%',
- field: 'id',
- render: (
- seriesId: string,
- { series: { dataType, reportType }, seriesConfig }: BuilderItem
- ) => {
- if (dataType && seriesConfig) {
- return loading ? (
- LOADING_VIEW
- ) : reportType ? (
-
- ) : (
- SELECT_REPORT_TYPE
- );
- }
-
- return null;
- },
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', {
- defaultMessage: 'Filters',
- }),
- width: '20%',
- field: 'id',
- render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
- reportType && seriesConfig ? (
-
- ) : null,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', {
- defaultMessage: 'Breakdowns',
- }),
- width: '20%',
- field: 'id',
- render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
- reportType && seriesConfig ? (
-
- ) : null,
- },
- ...(multiSeries
- ? [
- {
- name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (seriesId: string, item: BuilderItem) => (
-
- ),
- },
- ]
- : []),
- ];
-
- const applySeries = () => {
- editorItems.forEach(({ series, id: seriesId }) => {
- const { reportType, reportDefinitions, isNew, ...restSeries } = series;
-
- if (reportType && !isEmpty(reportDefinitions)) {
- const reportDefId = Object.values(reportDefinitions ?? {})[0];
- const newSeriesId = `${reportDefId}-${reportType}`;
-
- const newSeriesN: SeriesUrl = {
- ...restSeries,
- reportType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(seriesId);
- }
- });
- };
-
- const addSeries = () => {
- const prevSeries = allSeries?.[allSeriesIds?.[0]];
- setSeries(
- `${NEW_SERIES_KEY}-${editorItems.length + 1}`,
- prevSeries
- ? ({ isNew: true, time: prevSeries.time } as SeriesUrl)
- : ({ isNew: true } as SeriesUrl)
- );
- };
-
- return (
-
- {multiSeries && (
-
-
-
-
-
- {}}
- compressed
- />
-
-
- applySeries()} isDisabled={true} size="s">
- {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
- defaultMessage: 'Apply changes',
- })}
-
-
-
- addSeries()} size="s">
- {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
- defaultMessage: 'Add Series',
- })}
-
-
-
- )}
-
- {multiSeries && }
- {editorItems.length > 0 && (
-
- )}
-
-
-
- );
-}
-
-const Wrapper = euiStyled.div`
- max-height: 50vh;
- overflow-y: scroll;
- overflow-x: clip;
- &::-webkit-scrollbar {
- height: ${({ theme }) => theme.eui.euiScrollBar};
- width: ${({ theme }) => theme.eui.euiScrollBar};
- }
- &::-webkit-scrollbar-thumb {
- background-clip: content-box;
- background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
- border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
- }
- &::-webkit-scrollbar-corner,
- &::-webkit-scrollbar-track {
- background-color: transparent;
- }
-`;
-
-export const LOADING_VIEW = i18n.translate(
- 'xpack.observability.expView.seriesBuilder.loadingView',
- {
- defaultMessage: 'Loading view ...',
- }
-);
-
-export const SELECT_REPORT_TYPE = i18n.translate(
- 'xpack.observability.expView.seriesBuilder.selectReportType',
- {
- defaultMessage: 'No report type selected',
- }
-);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
deleted file mode 100644
index 207a53e13f1ad..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { Breakdowns } from './columns/breakdowns';
-import { SeriesConfig } from '../types';
-import { ChartOptions } from './columns/chart_options';
-
-interface Props {
- seriesConfig: SeriesConfig;
- seriesId: string;
- breakdownFields: string[];
-}
-export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) {
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
index 84568e1c5068a..21b766227a562 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { Breakdowns } from './breakdowns';
-import { mockIndexPattern, render } from '../../rtl_helpers';
+import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
import { getDefaultConfigs } from '../../configurations/default_configs';
import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
@@ -20,13 +20,7 @@ describe('Breakdowns', function () {
});
it('should render properly', async function () {
- render(
-
- );
+ render();
screen.getAllByText('Browser family');
});
@@ -36,9 +30,9 @@ describe('Breakdowns', function () {
const { setSeries } = render(
,
{ initSeries }
);
@@ -49,10 +43,14 @@ describe('Breakdowns', function () {
fireEvent.click(screen.getByText('Browser family'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
breakdown: 'user_agent.name',
dataType: 'ux',
- reportType: 'data-distribution',
+ name: 'performance-distribution',
+ reportDefinitions: {
+ 'service.name': ['elastic-co'],
+ },
+ selectedMetricField: 'transaction.duration.us',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
index 2237935d466ad..6003ddbf0290f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx
@@ -10,18 +10,16 @@ import { EuiSuperSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
interface Props {
- seriesId: string;
- breakdowns: string[];
- seriesConfig: SeriesConfig;
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
}
-export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
- const { setSeries, getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
+ const { setSeries } = useSeriesStorage();
const selectedBreakdown = series.breakdown;
const NO_BREAKDOWN = 'no_breakdown';
@@ -40,9 +38,13 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
}
};
+ if (!seriesConfig) {
+ return null;
+ }
+
const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN;
- const items = breakdowns.map((breakdown) => ({
+ const items = seriesConfig.breakdownFields.map((breakdown) => ({
id: breakdown,
label: seriesConfig.labels[breakdown],
}));
@@ -50,14 +52,12 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
if (!hasUseBreakdownColumn) {
items.push({
id: NO_BREAKDOWN,
- label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', {
- defaultMessage: 'No breakdown',
- }),
+ label: NO_BREAK_DOWN_LABEL,
});
}
const options = items.map(({ id, label }) => ({
- inputDisplay: id === NO_BREAKDOWN ? label : {label},
+ inputDisplay: label,
value: id,
dropdownDisplay: label,
}));
@@ -66,15 +66,18 @@ export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN);
return (
-
- onOptionChange(value)}
- data-test-subj={'seriesBreakdown'}
- />
-
+ onOptionChange(value)}
+ data-test-subj={'seriesBreakdown'}
+ />
);
}
+
+export const NO_BREAK_DOWN_LABEL = i18n.translate(
+ 'xpack.observability.exp.breakDownFilter.noBreakdown',
+ {
+ defaultMessage: 'No breakdown',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
deleted file mode 100644
index f2a6377fd9b71..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { SeriesConfig } from '../../types';
-import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select';
-import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types';
-
-interface Props {
- seriesConfig: SeriesConfig;
- seriesId: string;
-}
-
-export function ChartOptions({ seriesConfig, seriesId }: Props) {
- return (
-
-
-
-
- {seriesConfig.hasOperationType && (
-
-
-
- )}
-
- );
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
new file mode 100644
index 0000000000000..6f88de5cc2afc
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_type_select.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiPopover, EuiToolTip, EuiButtonEmpty, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
+import { SeriesUrl, useFetcher } from '../../../../../index';
+import { SeriesConfig } from '../../types';
+import { SeriesChartTypesSelect } from './chart_types';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}
+
+export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) {
+ const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType;
+
+ const {
+ services: { lens },
+ } = useKibana();
+
+ const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]);
+
+ const icon = (data ?? []).find(({ id }) => id === seriesType)?.icon;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ return (
+ setIsPopoverOpen(false)}
+ button={
+
+ setIsPopoverOpen((prevState) => !prevState)}
+ flush="both"
+ >
+ {icon && (
+ id === seriesType)?.icon!} size="l" />
+ )}
+
+
+ }
+ >
+
+
+ );
+}
+
+const EDIT_CHART_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.editChartSeriesLabel',
+ {
+ defaultMessage: 'Edit chart type for series',
+ }
+);
+
+const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', {
+ defaultMessage: 'Chart type',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
similarity index 85%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
index c054853d9c877..8f196b8a05dda 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx
@@ -7,12 +7,12 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { render } from '../../rtl_helpers';
+import { mockUxSeries, render } from '../../rtl_helpers';
import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types';
describe.skip('SeriesChartTypesSelect', function () {
it('should render properly', async function () {
- render();
+ render();
await waitFor(() => {
screen.getByText(/chart type/i);
@@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () {
it('should call set series on change', async function () {
const { setSeries } = render(
-
+
);
await waitFor(() => {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
similarity index 77%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
index 50c2f91e6067d..27d846502dbe6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx
@@ -6,11 +6,11 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
-import { useFetcher } from '../../../../..';
+import { SeriesUrl, useFetcher } from '../../../../..';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { SeriesType } from '../../../../../../../lens/public';
@@ -20,16 +20,14 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.
export function SeriesChartTypesSelect({
seriesId,
- seriesTypes,
+ series,
defaultChartType,
}: {
- seriesId: string;
- seriesTypes?: SeriesType[];
+ seriesId: number;
+ series: SeriesUrl;
defaultChartType: SeriesType;
}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries } = useSeriesStorage();
const seriesType = series?.seriesType ?? defaultChartType;
@@ -42,17 +40,15 @@ export function SeriesChartTypesSelect({
onChange={onChange}
value={seriesType}
excludeChartTypes={['bar_percentage_stacked']}
- includeChartTypes={
- seriesTypes || [
- 'bar',
- 'bar_horizontal',
- 'line',
- 'area',
- 'bar_stacked',
- 'area_stacked',
- 'bar_horizontal_percentage_stacked',
- ]
- }
+ includeChartTypes={[
+ 'bar',
+ 'bar_horizontal',
+ 'line',
+ 'area',
+ 'bar_stacked',
+ 'area_stacked',
+ 'bar_horizontal_percentage_stacked',
+ ]}
label={CHART_TYPE_LABEL}
/>
);
@@ -105,14 +101,14 @@ export function XYChartTypesSelect({
});
return (
-
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
new file mode 100644
index 0000000000000..fc96ad0741ec5
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, screen } from '@testing-library/react';
+import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
+import { DataTypesLabels, DataTypesSelect } from './data_type_select';
+import { DataTypes } from '../../configurations/constants';
+
+describe('DataTypeSelect', function () {
+ const seriesId = 0;
+
+ mockAppIndexPattern();
+
+ it('should render properly', function () {
+ render();
+ });
+
+ it('should set series on change', async function () {
+ const seriesWithoutDataType = {
+ ...mockUxSeries,
+ dataType: undefined,
+ };
+ const { setSeries } = render(
+
+ );
+
+ fireEvent.click(await screen.findByText('Select data type'));
+ fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS]));
+
+ expect(setSeries).toHaveBeenCalledTimes(1);
+ expect(setSeries).toHaveBeenCalledWith(seriesId, {
+ dataType: 'synthetics',
+ name: 'synthetics-series-1',
+ time: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
new file mode 100644
index 0000000000000..71fd147e8e264
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiButton,
+ EuiPopover,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiBadge,
+ EuiToolTip,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { AppDataType, SeriesUrl } from '../../types';
+import { DataTypes, ReportTypes } from '../../configurations/constants';
+
+interface Props {
+ seriesId: number;
+ series: Omit & {
+ dataType?: SeriesUrl['dataType'];
+ };
+}
+
+export const DataTypesLabels = {
+ [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', {
+ defaultMessage: 'User experience (RUM)',
+ }),
+
+ [DataTypes.SYNTHETICS]: i18n.translate(
+ 'xpack.observability.overview.exploratoryView.syntheticsLabel',
+ {
+ defaultMessage: 'Synthetics monitoring',
+ }
+ ),
+
+ [DataTypes.MOBILE]: i18n.translate(
+ 'xpack.observability.overview.exploratoryView.mobileExperienceLabel',
+ {
+ defaultMessage: 'Mobile experience',
+ }
+ ),
+};
+
+export const dataTypes: Array<{ id: AppDataType; label: string }> = [
+ {
+ id: DataTypes.SYNTHETICS,
+ label: DataTypesLabels[DataTypes.SYNTHETICS],
+ },
+ {
+ id: DataTypes.UX,
+ label: DataTypesLabels[DataTypes.UX],
+ },
+ {
+ id: DataTypes.MOBILE,
+ label: DataTypesLabels[DataTypes.MOBILE],
+ },
+];
+
+const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE';
+
+export function DataTypesSelect({ seriesId, series }: Props) {
+ const { setSeries, reportType } = useSeriesStorage();
+ const [showOptions, setShowOptions] = useState(false);
+
+ const onDataTypeChange = (dataType: AppDataType) => {
+ if (String(dataType) !== SELECT_DATA_TYPE) {
+ setSeries(seriesId, {
+ dataType,
+ time: series.time,
+ name: `${dataType}-series-${seriesId + 1}`,
+ });
+ }
+ };
+
+ const options = dataTypes
+ .filter(({ id }) => {
+ if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
+ return id === DataTypes.MOBILE;
+ }
+ if (reportType === ReportTypes.CORE_WEB_VITAL) {
+ return id === DataTypes.UX;
+ }
+ return true;
+ })
+ .map(({ id, label }) => ({
+ value: id,
+ inputDisplay: label,
+ }));
+
+ return (
+ <>
+ {!series.dataType && (
+ setShowOptions((prevState) => !prevState)}
+ fill
+ size="s"
+ >
+ {SELECT_DATA_TYPE_LABEL}
+
+ }
+ isOpen={showOptions}
+ closePopover={() => setShowOptions((prevState) => !prevState)}
+ >
+
+ {options.map((option) => (
+ onDataTypeChange(option.value)}
+ label={option.inputDisplay}
+ />
+ ))}
+
+
+ )}
+ {series.dataType && (
+
+ {DataTypesLabels[series.dataType as DataTypes]}
+
+ )}
+ >
+ );
+}
+
+const SELECT_DATA_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.selectDataType',
+ {
+ defaultMessage: 'Select data type',
+ }
+);
+
+const SELECT_DATA_TYPE_TOOLTIP = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.selectDataTypeTooltip',
+ {
+ defaultMessage: 'Data type cannot be edited.',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
index 41e83f407af2b..b01010e4b81f9 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx
@@ -6,24 +6,80 @@
*/
import React from 'react';
-import { SeriesDatePicker } from '../../series_date_picker';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { DateRangePicker } from '../../series_date_picker/date_range_picker';
+import { DateRangePicker } from '../../components/date_range_picker';
+import { SeriesDatePicker } from '../../components/series_date_picker';
+import { AppDataType, SeriesUrl } from '../../types';
+import { ReportTypes } from '../../configurations/constants';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data';
+import { MobileAddData } from '../../../add_data_buttons/mobile_add_data';
+import { UXAddData } from '../../../add_data_buttons/ux_add_data';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
}
-export function DatePickerCol({ seriesId }: Props) {
- const { firstSeriesId, getSeries } = useSeriesStorage();
- const { reportType } = getSeries(firstSeriesId);
+
+const AddDataComponents: Record = {
+ mobile: MobileAddData,
+ ux: UXAddData,
+ synthetics: SyntheticsAddData,
+ apm: null,
+ infra_logs: null,
+ infra_metrics: null,
+};
+
+export function DatePickerCol({ seriesId, series }: Props) {
+ const { reportType } = useSeriesStorage();
+
+ const { hasAppData } = useAppIndexPatternContext();
+
+ if (!series.dataType) {
+ return null;
+ }
+
+ const AddDataButton = AddDataComponents[series.dataType];
+ if (hasAppData[series.dataType] === false && AddDataButton !== null) {
+ return (
+
+
+
+ {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', {
+ defaultMessage: 'No {dataType} data available.',
+ values: {
+ dataType: series.dataType,
+ },
+ })}
+
+
+
+
+
+
+ );
+ }
return (
-
- {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
-
+
+ {seriesId === 0 || reportType !== ReportTypes.KPI ? (
+
) : (
-
+
)}
-
+
);
}
+
+const Wrapper = styled.div`
+ width: 100%;
+ .euiSuperDatePicker__flexWrapper {
+ width: 100%;
+ > .euiFlexItem {
+ margin-right: 0;
+ }
+ }
+`;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
index 90a039f6b44d0..a88e2eadd10c9 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx
@@ -8,20 +8,24 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { FilterExpanded } from './filter_expanded';
-import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
+import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('FilterExpanded', function () {
- it('should render properly', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
+
+ const mockSeries = { ...mockUxSeries, filters };
+
+ it('render', async () => {
+ const initSeries = { filters };
mockAppIndexPattern();
render(
,
{ initSeries }
@@ -33,15 +37,14 @@ describe('FilterExpanded', function () {
});
it('should call go back on click', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
- const goBack = jest.fn();
+ const initSeries = { filters };
render(
,
{ initSeries }
@@ -49,28 +52,23 @@ describe('FilterExpanded', function () {
await waitFor(() => {
fireEvent.click(screen.getByText('Browser Family'));
-
- expect(goBack).toHaveBeenCalledTimes(1);
- expect(goBack).toHaveBeenCalledWith();
});
});
- it('should call useValuesList on load', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ it('calls useValuesList on load', async () => {
+ const initSeries = { filters };
const { spy } = mockUseValuesList([
{ label: 'Chrome', count: 10 },
{ label: 'Firefox', count: 5 },
]);
- const goBack = jest.fn();
-
render(
,
{ initSeries }
@@ -87,8 +85,8 @@ describe('FilterExpanded', function () {
});
});
- it('should filter display values', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ it('filters display values', async () => {
+ const initSeries = { filters };
mockUseValuesList([
{ label: 'Chrome', count: 10 },
@@ -97,18 +95,20 @@ describe('FilterExpanded', function () {
render(
,
{ initSeries }
);
- expect(screen.getByText('Firefox')).toBeTruthy();
-
await waitFor(() => {
+ fireEvent.click(screen.getByText('Browser Family'));
+
+ expect(screen.queryByText('Firefox')).toBeTruthy();
+
fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } });
expect(screen.queryByText('Firefox')).toBeFalsy();
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
index 84c326f62f89d..693b79c6dc831 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
@@ -6,7 +6,14 @@
*/
import React, { useState, Fragment } from 'react';
-import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui';
+import {
+ EuiFieldSearch,
+ EuiSpacer,
+ EuiFilterGroup,
+ EuiText,
+ EuiPopover,
+ EuiFilterButton,
+} from '@elastic/eui';
import styled from 'styled-components';
import { rgba } from 'polished';
import { i18n } from '@kbn/i18n';
@@ -14,8 +21,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
import { map } from 'lodash';
import { ExistsFilter, isExistsFilter } from '@kbn/es-query';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { SeriesConfig, UrlFilter } from '../../types';
+import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
import { FilterValueButton } from './filter_value_btn';
import { useValuesList } from '../../../../../hooks/use_values_list';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
@@ -23,31 +29,33 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc
import { PersistableFilter } from '../../../../../../../lens/common';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
label: string;
field: string;
isNegated?: boolean;
- goBack: () => void;
nestedField?: string;
filters: SeriesConfig['baseFilters'];
}
+export interface NestedFilterOpen {
+ value: string;
+ negate: boolean;
+}
+
export function FilterExpanded({
seriesId,
+ series,
field,
label,
- goBack,
nestedField,
isNegated,
filters: defaultFilters,
}: Props) {
const [value, setValue] = useState('');
- const [isOpen, setIsOpen] = useState({ value: '', negate: false });
-
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false });
const queryFilters: ESFilter[] = [];
@@ -80,62 +88,71 @@ export function FilterExpanded({
);
return (
-
- goBack()}>
- {label}
-
- {
- setValue(evt.target.value);
- }}
- placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
- defaultMessage: 'Search for {label}',
- values: { label },
- })}
- />
-
-
- {displayValues.length === 0 && !loading && (
-
- {i18n.translate('xpack.observability.filters.expanded.noFilter', {
- defaultMessage: 'No filters found.',
- })}
-
- )}
- {displayValues.map((opt) => (
-
-
- {isNegated !== false && (
+ setIsOpen((prevState) => !prevState)} iconType="arrowDown">
+ {label}
+
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ >
+
+ {
+ setValue(evt.target.value);
+ }}
+ placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
+ defaultMessage: 'Search for {label}',
+ values: { label },
+ })}
+ />
+
+
+ {displayValues.length === 0 && !loading && (
+
+ {i18n.translate('xpack.observability.filters.expanded.noFilter', {
+ defaultMessage: 'No filters found.',
+ })}
+
+ )}
+ {displayValues.map((opt) => (
+
+
+ {isNegated !== false && (
+
+ )}
- )}
-
-
-
-
- ))}
-
-
+
+
+
+ ))}
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
index a9609abc70d69..764a27fd663f5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { FilterValueButton } from './filter_value_btn';
-import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
+import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
import {
USER_AGENT_NAME,
USER_AGENT_VERSION,
@@ -19,84 +19,98 @@ describe('FilterValueButton', function () {
render(
);
- screen.getByText('Chrome');
+ await waitFor(() => {
+ expect(screen.getByText('Chrome')).toBeInTheDocument();
+ });
});
- it('should render display negate state', async function () {
- render(
-
- );
+ describe('when negate is true', () => {
+ it('displays negate stats', async () => {
+ render(
+
+ );
- await waitFor(() => {
- screen.getByText('Not Chrome');
- screen.getByTitle('Not Chrome');
- const btn = screen.getByRole('button');
- expect(btn.classList).toContain('euiButtonEmpty--danger');
+ await waitFor(() => {
+ expect(screen.getByText('Not Chrome')).toBeInTheDocument();
+ expect(screen.getByTitle('Not Chrome')).toBeInTheDocument();
+ const btn = screen.getByRole('button');
+ expect(btn.classList).toContain('euiButtonEmpty--danger');
+ });
});
- });
- it('should call set filter on click', async function () {
- const { setFilter, removeFilter } = mockUseSeriesFilter();
+ it('calls setFilter on click', async () => {
+ const { setFilter, removeFilter } = mockUseSeriesFilter();
- render(
-
- );
+ render(
+
+ );
- await waitFor(() => {
fireEvent.click(screen.getByText('Not Chrome'));
- expect(removeFilter).toHaveBeenCalledTimes(0);
- expect(setFilter).toHaveBeenCalledTimes(1);
- expect(setFilter).toHaveBeenCalledWith({
- field: 'user_agent.name',
- negate: true,
- value: 'Chrome',
+
+ await waitFor(() => {
+ expect(removeFilter).toHaveBeenCalledTimes(0);
+ expect(setFilter).toHaveBeenCalledTimes(1);
+
+ expect(setFilter).toHaveBeenCalledWith({
+ field: 'user_agent.name',
+ negate: true,
+ value: 'Chrome',
+ });
});
});
});
- it('should remove filter on click if already selected', async function () {
- const { removeFilter } = mockUseSeriesFilter();
+ describe('when selected', () => {
+ it('removes the filter on click', async () => {
+ const { removeFilter } = mockUseSeriesFilter();
+
+ render(
+
+ );
- render(
-
- );
- await waitFor(() => {
fireEvent.click(screen.getByText('Chrome'));
- expect(removeFilter).toHaveBeenCalledWith({
- field: 'user_agent.name',
- negate: false,
- value: 'Chrome',
+
+ await waitFor(() => {
+ expect(removeFilter).toHaveBeenCalledWith({
+ field: 'user_agent.name',
+ negate: false,
+ value: 'Chrome',
+ });
});
});
});
@@ -107,12 +121,13 @@ describe('FilterValueButton', function () {
render(
);
@@ -134,13 +149,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -167,13 +183,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -203,13 +220,14 @@ describe('FilterValueButton', function () {
render(
);
@@ -229,13 +247,14 @@ describe('FilterValueButton', function () {
render(
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
index bf4ca6eb83d94..11f29c0233ef5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
@@ -5,13 +5,15 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
+
import React, { useMemo } from 'react';
import { EuiFilterButton, hexToRgb } from '@elastic/eui';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
import { useSeriesFilters } from '../../hooks/use_series_filters';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import FieldValueSuggestions from '../../../field_value_suggestions';
+import { SeriesUrl } from '../../types';
+import { NestedFilterOpen } from './filter_expanded';
interface Props {
value: string;
@@ -19,12 +21,13 @@ interface Props {
allSelectedValues?: string[];
negate: boolean;
nestedField?: string;
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
isNestedOpen: {
value: string;
negate: boolean;
};
- setIsNestedOpen: (val: { value: string; negate: boolean }) => void;
+ setIsNestedOpen: (val: NestedFilterOpen) => void;
}
export function FilterValueButton({
@@ -34,16 +37,13 @@ export function FilterValueButton({
field,
negate,
seriesId,
+ series,
nestedField,
allSelectedValues,
}: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
const { indexPatterns } = useAppIndexPatternContext(series.dataType);
- const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
+ const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series });
const hasActiveFilters = (allSelectedValues ?? []).includes(value);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
new file mode 100644
index 0000000000000..4e1c385921908
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { isEmpty } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { EuiBadge } from '@elastic/eui';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { SeriesConfig, SeriesUrl } from '../../types';
+
+interface Props {
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+
+export function IncompleteBadge({ seriesConfig, series }: Props) {
+ const { loading } = useAppIndexPatternContext();
+
+ if (!seriesConfig) {
+ return null;
+ }
+ const { dataType, reportDefinitions, selectedMetricField } = series;
+ const { definitionFields, labels } = seriesConfig;
+ const isIncomplete =
+ (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading;
+
+ const incompleteDefinition = isEmpty(reportDefinitions)
+ ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', {
+ defaultMessage: 'Missing {reportDefinition}',
+ values: { reportDefinition: labels?.[definitionFields[0]] },
+ })
+ : '';
+
+ let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition;
+
+ if (!dataType) {
+ incompleteMessage = MISSING_DATA_TYPE_LABEL;
+ }
+
+ if (!isIncomplete) {
+ return null;
+ }
+
+ return {incompleteMessage};
+}
+
+const MISSING_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.missingReportMetric',
+ {
+ defaultMessage: 'Missing report metric',
+ }
+);
+
+const MISSING_DATA_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.overview.exploratoryView.missingDataType',
+ {
+ defaultMessage: 'Missing data type',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
similarity index 69%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
index 516f04e3812ba..ced4d3af057ff 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx
@@ -7,62 +7,66 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
-import { render } from '../../rtl_helpers';
+import { mockUxSeries, render } from '../../rtl_helpers';
import { OperationTypeSelect } from './operation_type_select';
describe('OperationTypeSelect', function () {
it('should render properly', function () {
- render();
+ render();
screen.getByText('Select an option: , is selected');
});
it('should display selected value', function () {
const initSeries = {
- data: {
- 'performance-distribution': {
+ data: [
+ {
+ name: 'performance-distribution',
dataType: 'ux' as const,
- reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
- render(, { initSeries });
+ render(, {
+ initSeries,
+ });
screen.getByText('Median');
});
it('should call set series on change', function () {
const initSeries = {
- data: {
- 'series-id': {
+ data: [
+ {
+ name: 'performance-distribution',
dataType: 'ux' as const,
- reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
- },
+ ],
};
- const { setSeries } = render(, { initSeries });
+ const { setSeries } = render(, {
+ initSeries,
+ });
fireEvent.click(screen.getByTestId('operationTypeSelect'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
operationType: 'median',
dataType: 'ux',
- reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
+ name: 'performance-distribution',
});
fireEvent.click(screen.getByText('95th Percentile'));
- expect(setSeries).toHaveBeenCalledWith('series-id', {
+ expect(setSeries).toHaveBeenCalledWith(0, {
operationType: '95th',
dataType: 'ux',
- reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
+ name: 'performance-distribution',
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
similarity index 91%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
index fce1383f30f34..4c10c9311704d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx
@@ -11,17 +11,18 @@ import { EuiSuperSelect } from '@elastic/eui';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { OperationType } from '../../../../../../../lens/public';
+import { SeriesUrl } from '../../types';
export function OperationTypeSelect({
seriesId,
+ series,
defaultOperationType,
}: {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
defaultOperationType?: OperationType;
}) {
- const { getSeries, setSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
+ const { setSeries } = useSeriesStorage();
const operationType = series?.operationType;
@@ -83,11 +84,7 @@ export function OperationTypeSelect({
return (
{
removeSeries(seriesId);
};
+
+ const isDisabled = seriesId === 0 && allSeries.length > 1;
+
return (
-
+
+
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
similarity index 65%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
index 3d156e0ee9c2b..544a294e021e2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx
@@ -12,14 +12,14 @@ import {
mockAppIndexPattern,
mockIndexPattern,
mockUseValuesList,
+ mockUxSeries,
render,
} from '../../rtl_helpers';
import { ReportDefinitionCol } from './report_definition_col';
-import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('Series Builder ReportDefinitionCol', function () {
mockAppIndexPattern();
- const seriesId = 'test-series-id';
+ const seriesId = 0;
const seriesConfig = getDefaultConfigs({
reportType: 'data-distribution',
@@ -27,36 +27,24 @@ describe('Series Builder ReportDefinitionCol', function () {
dataType: 'ux',
});
- const initSeries = {
- data: {
- [seriesId]: {
- dataType: 'ux' as const,
- reportType: 'data-distribution' as const,
- time: { from: 'now-30d', to: 'now' },
- reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
- },
- },
- };
-
mockUseValuesList([{ label: 'elastic-co', count: 10 }]);
- it('should render properly', async function () {
- render(, {
- initSeries,
- });
+ it('renders', async () => {
+ render(
+
+ );
await waitFor(() => {
- screen.getByText('Web Application');
- screen.getByText('Environment');
- screen.getByText('Select an option: Page load time, is selected');
- screen.getByText('Page load time');
+ expect(screen.getByText('Web Application')).toBeInTheDocument();
+ expect(screen.getByText('Environment')).toBeInTheDocument();
+ expect(screen.getByText('Search Environment')).toBeInTheDocument();
});
});
it('should render selected report definitions', async function () {
- render(, {
- initSeries,
- });
+ render(
+
+ );
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
@@ -65,8 +53,7 @@ describe('Series Builder ReportDefinitionCol', function () {
it('should be able to remove selected definition', async function () {
const { setSeries } = render(
- ,
- { initSeries }
+
);
expect(
@@ -80,11 +67,14 @@ describe('Series Builder ReportDefinitionCol', function () {
fireEvent.click(removeBtn);
expect(setSeries).toHaveBeenCalledTimes(1);
+
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'ux',
+ name: 'performance-distribution',
+ breakdown: 'user_agent.name',
reportDefinitions: {},
- reportType: 'data-distribution',
- time: { from: 'now-30d', to: 'now' },
+ selectedMetricField: 'transaction.duration.us',
+ time: { from: 'now-15m', to: 'now' },
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
new file mode 100644
index 0000000000000..fbd7c34303d94
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { ReportDefinitionField } from './report_definition_field';
+
+export function ReportDefinitionCol({
+ seriesId,
+ series,
+ seriesConfig,
+}: {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}) {
+ const { setSeries } = useSeriesStorage();
+
+ const { reportDefinitions: selectedReportDefinitions = {} } = series;
+
+ const { definitionFields } = seriesConfig;
+
+ const onChange = (field: string, value?: string[]) => {
+ if (!value?.[0]) {
+ delete selectedReportDefinitions[field];
+ setSeries(seriesId, {
+ ...series,
+ reportDefinitions: { ...selectedReportDefinitions },
+ });
+ } else {
+ setSeries(seriesId, {
+ ...series,
+ reportDefinitions: { ...selectedReportDefinitions, [field]: value },
+ });
+ }
+ };
+
+ return (
+
+ {definitionFields.map((field) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
similarity index 69%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
index 8a83b5c2a8cb0..3651b4b7f075b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx
@@ -6,30 +6,25 @@
*/
import React, { useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { ExistsFilter } from '@kbn/es-query';
import FieldValueSuggestions from '../../../field_value_suggestions';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch';
import { PersistableFilter } from '../../../../../../../lens/common';
import { buildPhrasesFilter } from '../../configurations/utils';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox';
interface Props {
- seriesId: string;
+ seriesId: number;
+ series: SeriesUrl;
field: string;
seriesConfig: SeriesConfig;
onChange: (field: string, value?: string[]) => void;
}
-export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
+export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) {
const { indexPattern } = useAppIndexPatternContext(series.dataType);
const { reportDefinitions: selectedReportDefinitions = {} } = series;
@@ -64,23 +59,26 @@ export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]);
+ if (!indexPattern) {
+ return null;
+ }
+
return (
-
-
- {indexPattern && (
- onChange(field, val)}
- filters={queryFilters}
- time={series.time}
- fullWidth={true}
- allowAllValuesSelection={true}
- />
- )}
-
-
+ onChange(field, val)}
+ filters={queryFilters}
+ time={series.time}
+ fullWidth={true}
+ asCombobox={true}
+ allowExclusions={false}
+ allowAllValuesSelection={true}
+ usePrependLabel={false}
+ compressed={false}
+ required={isEmpty(selectedReportDefinitions)}
+ />
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
new file mode 100644
index 0000000000000..31a8c7cb7bfae
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiSuperSelect } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { ReportViewType } from '../../types';
+import {
+ CORE_WEB_VITALS_LABEL,
+ DEVICE_DISTRIBUTION_LABEL,
+ KPI_OVER_TIME_LABEL,
+ PERF_DIST_LABEL,
+} from '../../configurations/constants/labels';
+
+const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE';
+
+export const reportTypesList: Array<{
+ reportType: ReportViewType | typeof SELECT_REPORT_TYPE;
+ label: string;
+}> = [
+ {
+ reportType: SELECT_REPORT_TYPE,
+ label: i18n.translate('xpack.observability.expView.reportType.selectLabel', {
+ defaultMessage: 'Select report type',
+ }),
+ },
+ { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
+ { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
+];
+
+export function ReportTypesSelect() {
+ const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage();
+
+ const onReportTypeChange = (reportType: ReportViewType) => {
+ setReportType(reportType);
+ };
+
+ const options = reportTypesList
+ .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true))
+ .map(({ reportType, label }) => ({
+ value: reportType,
+ inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label},
+ dropdownDisplay: label,
+ }));
+
+ return (
+ onReportTypeChange(value as ReportViewType)}
+ style={{ minWidth: 200 }}
+ isInvalid={!selectedReportType && allSeries.length > 0}
+ disabled={allSeries.length > 0}
+ />
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
similarity index 59%
rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
index eb76772a66c7e..64291f84f7662 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx
@@ -7,10 +7,10 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
-import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
+import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
import { SelectedFilters } from './selected_filters';
-import { getDefaultConfigs } from '../configurations/default_configs';
-import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
+import { getDefaultConfigs } from '../../configurations/default_configs';
+import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
describe('SelectedFilters', function () {
mockAppIndexPattern();
@@ -22,11 +22,19 @@ describe('SelectedFilters', function () {
});
it('should render properly', async function () {
- const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
+ const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
+ const initSeries = { filters };
- render(, {
- initSeries,
- });
+ render(
+ ,
+ {
+ initSeries,
+ }
+ );
await waitFor(() => {
screen.getByText('Chrome');
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx
new file mode 100644
index 0000000000000..3327ecf1fc9b6
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx
@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Fragment } from 'react';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FilterLabel } from '../../components/filter_label';
+import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
+import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { useSeriesFilters } from '../../hooks/use_series_filters';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig: SeriesConfig;
+}
+export function SelectedFilters({ seriesId, series, seriesConfig }: Props) {
+ const { setSeries } = useSeriesStorage();
+
+ const { labels } = seriesConfig;
+
+ const filters: UrlFilter[] = series.filters ?? [];
+
+ const { removeFilter } = useSeriesFilters({ seriesId, series });
+
+ const { indexPattern } = useAppIndexPatternContext(series.dataType);
+
+ if (filters.length === 0 || !indexPattern) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {filters.map(({ field, values, notValues }) => (
+
+ {(values ?? []).length > 0 && (
+
+ {
+ values?.forEach((val) => {
+ removeFilter({ field, value: val, negate: false });
+ });
+ }}
+ negate={false}
+ indexPattern={indexPattern}
+ />
+
+ )}
+ {(notValues ?? []).length > 0 && (
+
+ {
+ values?.forEach((val) => {
+ removeFilter({ field, value: val, negate: false });
+ });
+ }}
+ indexPattern={indexPattern}
+ />
+
+ )}
+
+ ))}
+
+ {(series.filters ?? []).length > 0 && (
+
+ {
+ setSeries(seriesId, { ...series, filters: undefined });
+ }}
+ size="xs"
+ >
+ {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
+ defaultMessage: 'Clear filters',
+ })}
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
index 51ebe6c6bd9d5..37b5b1571f84d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
@@ -6,98 +6,113 @@
*/
import React from 'react';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { isEmpty } from 'lodash';
import { RemoveSeries } from './remove_series';
import { useSeriesStorage } from '../../hooks/use_series_storage';
-import { SeriesUrl } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { useDiscoverLink } from '../../hooks/use_discover_link';
interface Props {
- seriesId: string;
- editorMode?: boolean;
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+ onEditClick?: () => void;
}
-export function SeriesActions({ seriesId, editorMode = false }: Props) {
- const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
- const series = getSeries(seriesId);
- const onEdit = () => {
- setSeries(seriesId, { ...series, isNew: true });
- };
+export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: Props) {
+ const { setSeries, allSeries } = useSeriesStorage();
+
+ const { href: discoverHref } = useDiscoverLink({ series, seriesConfig });
const copySeries = () => {
- let copySeriesId: string = `${seriesId}-copy`;
- if (allSeriesIds.includes(copySeriesId)) {
- copySeriesId = copySeriesId + allSeriesIds.length;
+ let copySeriesId: string = `${series.name}-copy`;
+ if (allSeries.find(({ name }) => name === copySeriesId)) {
+ copySeriesId = copySeriesId + allSeries.length;
}
- setSeries(copySeriesId, series);
+ setSeries(allSeries.length, { ...series, name: copySeriesId });
};
- const { reportType, reportDefinitions, isNew, ...restSeries } = series;
- const isSaveAble = reportType && !isEmpty(reportDefinitions);
-
- const saveSeries = () => {
- if (isSaveAble) {
- const reportDefId = Object.values(reportDefinitions ?? {})[0];
- let newSeriesId = `${reportDefId}-${reportType}`;
-
- if (allSeriesIds.includes(newSeriesId)) {
- newSeriesId = `${newSeriesId}-${allSeriesIds.length}`;
- }
- const newSeriesN: SeriesUrl = {
- ...restSeries,
- reportType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(seriesId);
+ const toggleSeries = () => {
+ if (series.hidden) {
+ setSeries(seriesId, { ...series, hidden: undefined });
+ } else {
+ setSeries(seriesId, { ...series, hidden: true });
}
};
return (
-
- {!editorMode && (
-
+
+
+
+
+
+
+
+
-
- )}
- {editorMode && (
-
+
+
+
+
+
-
- )}
- {editorMode && (
-
+
+
+
+
+
-
- )}
+
+
);
}
+
+const EDIT_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.edit', {
+ defaultMessage: 'Edit series',
+});
+
+const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide', {
+ defaultMessage: 'Hide series',
+});
+
+const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', {
+ defaultMessage: 'Copy series',
+});
+
+const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate(
+ 'xpack.observability.seriesEditor.sampleDocuments',
+ {
+ defaultMessage: 'View sample documents in new tab',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
index 02144c6929b38..5b576d9da0172 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx
@@ -5,29 +5,17 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-import React, { useState, Fragment } from 'react';
-import {
- EuiButton,
- EuiPopover,
- EuiSpacer,
- EuiButtonEmpty,
- EuiFlexItem,
- EuiFlexGroup,
-} from '@elastic/eui';
+import React from 'react';
+import { EuiFilterGroup, EuiSpacer } from '@elastic/eui';
import { FilterExpanded } from './filter_expanded';
-import { SeriesConfig } from '../../types';
+import { SeriesConfig, SeriesUrl } from '../../types';
import { FieldLabels } from '../../configurations/constants/constants';
-import { SelectedFilters } from '../selected_filters';
-import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SelectedFilters } from './selected_filters';
interface Props {
- seriesId: string;
- filterFields: SeriesConfig['filterFields'];
- baseFilters: SeriesConfig['baseFilters'];
+ seriesId: number;
seriesConfig: SeriesConfig;
- isNew?: boolean;
- labels?: Record;
+ series: SeriesUrl;
}
export interface Field {
@@ -37,119 +25,38 @@ export interface Field {
isNegated?: boolean;
}
-export function SeriesFilter({
- seriesConfig,
- isNew,
- seriesId,
- filterFields = [],
- baseFilters,
- labels,
-}: Props) {
- const [isPopoverVisible, setIsPopoverVisible] = useState(false);
-
- const [selectedField, setSelectedField] = useState();
-
- const options: Field[] = filterFields.map((field) => {
+export function SeriesFilter({ series, seriesConfig, seriesId }: Props) {
+ const options: Field[] = seriesConfig.filterFields.map((field) => {
if (typeof field === 'string') {
- return { label: labels?.[field] ?? FieldLabels[field], field };
+ return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field };
}
return {
field: field.field,
nested: field.nested,
isNegated: field.isNegated,
- label: labels?.[field.field] ?? FieldLabels[field.field],
+ label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field],
};
});
- const { setSeries, getSeries } = useSeriesStorage();
- const urlSeries = getSeries(seriesId);
-
- const button = (
- {
- setIsPopoverVisible((prevState) => !prevState);
- }}
- size="s"
- >
- {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', {
- defaultMessage: 'Add filter',
- })}
-
- );
-
- const mainPanel = (
+ return (
<>
+
+ {options.map((opt) => (
+
+ ))}
+
- {options.map((opt) => (
-
- {
- setSelectedField(opt);
- }}
- >
- {opt.label}
-
-
-
- ))}
+
>
);
-
- const childPanel = selectedField ? (
- {
- setSelectedField(undefined);
- }}
- filters={baseFilters}
- />
- ) : null;
-
- const closePopover = () => {
- setIsPopoverVisible(false);
- setSelectedField(undefined);
- };
-
- return (
-
-
-
-
- {!selectedField ? mainPanel : childPanel}
-
-
- {(urlSeries.filters ?? []).length > 0 && (
-
- {
- setSeries(seriesId, { ...urlSeries, filters: undefined });
- }}
- size="s"
- >
- {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
- defaultMessage: 'Clear filters',
- })}
-
-
- )}
-
- );
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
new file mode 100644
index 0000000000000..4c2e57e780550
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_info.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { SeriesConfig, SeriesUrl } from '../../types';
+import { SeriesColorPicker } from '../../components/series_color_picker';
+import { SeriesChartTypes } from './chart_type_select';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+
+export function SeriesInfo({ seriesId, series, seriesConfig }: Props) {
+ if (!seriesConfig) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+
+ return null;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
new file mode 100644
index 0000000000000..ccad461209313
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { mockUxSeries, render } from '../../rtl_helpers';
+import { SeriesName } from './series_name';
+
+describe.skip('SeriesChartTypesSelect', function () {
+ it('should render properly', async function () {
+ render();
+
+ expect(screen.getByText(mockUxSeries.name)).toBeInTheDocument();
+ });
+
+ it('should display input when editing name', async function () {
+ render();
+
+ let input = screen.queryByLabelText(mockUxSeries.name);
+
+ // read only
+ expect(input).not.toBeInTheDocument();
+
+ const editButton = screen.getByRole('button');
+ // toggle editing
+ fireEvent.click(editButton);
+
+ await waitFor(() => {
+ input = screen.getByLabelText(mockUxSeries.name);
+
+ expect(input).toBeInTheDocument();
+ });
+
+ // toggle readonly
+ fireEvent.click(editButton);
+
+ await waitFor(() => {
+ input = screen.getByLabelText(mockUxSeries.name);
+
+ expect(input).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
new file mode 100644
index 0000000000000..cff30a2b35059
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, ChangeEvent, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFieldText,
+ EuiText,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiOutsideClickDetector,
+} from '@elastic/eui';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesUrl } from '../../types';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+}
+
+export const StyledText = styled(EuiText)`
+ &.euiText.euiText--constrainedWidth {
+ max-width: 200px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+`;
+
+export function SeriesName({ series, seriesId }: Props) {
+ const { setSeries } = useSeriesStorage();
+
+ const [value, setValue] = useState(series.name);
+ const [isEditingEnabled, setIsEditingEnabled] = useState(false);
+ const inputRef = useRef(null);
+ const buttonRef = useRef(null);
+
+ const onChange = (e: ChangeEvent) => {
+ setValue(e.target.value);
+ };
+
+ const onSave = () => {
+ if (value !== series.name) {
+ setSeries(seriesId, { ...series, name: value });
+ }
+ };
+
+ const onOutsideClick = (event: Event) => {
+ if (event.target !== buttonRef.current) {
+ setIsEditingEnabled(false);
+ }
+ };
+
+ useEffect(() => {
+ setValue(series.name);
+ }, [series.name]);
+
+ useEffect(() => {
+ if (isEditingEnabled && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isEditingEnabled, inputRef]);
+
+ return (
+
+ {isEditingEnabled ? (
+
+
+
+
+
+ ) : (
+
+ {value}
+
+ )}
+
+ setIsEditingEnabled(!isEditingEnabled)}
+ iconType="pencil"
+ aria-label={i18n.translate('xpack.observability.expView.seriesEditor.editName', {
+ defaultMessage: 'Edit name',
+ })}
+ color="text"
+ buttonRef={buttonRef}
+ />
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
new file mode 100644
index 0000000000000..9f4de1b6dd519
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiHorizontalRule } from '@elastic/eui';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { ReportDefinitionCol } from './columns/report_definition_col';
+import { OperationTypeSelect } from './columns/operation_type_select';
+import { parseCustomFieldName } from '../configurations/lens_attributes';
+import { SeriesFilter } from './columns/series_filter';
+import { DatePickerCol } from './columns/date_picker_col';
+import { Breakdowns } from './columns/breakdowns';
+
+function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
+ const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
+
+ return columnType;
+}
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
+export function ExpandedSeriesRow(seriesProps: Props) {
+ const { seriesConfig, series, seriesId } = seriesProps;
+
+ if (!seriesConfig) {
+ return null;
+ }
+
+ const { selectedMetricField } = series ?? {};
+
+ const { hasOperationType, yAxisColumns } = seriesConfig;
+
+ const columnType = getColumnType(seriesConfig, selectedMetricField);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(hasOperationType || columnType === 'operation') && (
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+const BREAKDOWNS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.breakdowns', {
+ defaultMessage: 'Breakdowns',
+});
+
+const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', {
+ defaultMessage: 'Filters',
+});
+
+const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', {
+ defaultMessage: 'Operation',
+});
+
+const DATE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.date', {
+ defaultMessage: 'Date',
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
new file mode 100644
index 0000000000000..496e7a10f9c44
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiToolTip,
+ EuiPopover,
+ EuiButton,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiBadge,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useSeriesStorage } from '../hooks/use_series_storage';
+import { SeriesConfig, SeriesUrl } from '../types';
+import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
+
+interface Props {
+ seriesId: number;
+ series: SeriesUrl;
+ defaultValue?: string;
+ seriesConfig?: SeriesConfig;
+}
+
+export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) {
+ const { setSeries } = useSeriesStorage();
+ const [showOptions, setShowOptions] = useState(false);
+ const metricOptions = seriesConfig?.metricOptions;
+
+ const { indexPatterns } = useAppIndexPatternContext();
+
+ const onChange = (value?: string) => {
+ setSeries(seriesId, {
+ ...series,
+ selectedMetricField: value,
+ });
+ };
+
+ if (!series.dataType) {
+ return null;
+ }
+
+ const indexPattern = indexPatterns?.[series.dataType];
+
+ const options = (metricOptions ?? []).map(({ label, field, id }) => {
+ let disabled = false;
+
+ if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) {
+ disabled = !Boolean(indexPattern?.getFieldByName(field));
+ }
+ return {
+ disabled,
+ value: field || id,
+ dropdownDisplay: disabled ? (
+ {field},
+ }}
+ />
+ }
+ >
+ {label}
+
+ ) : (
+ label
+ ),
+ inputDisplay: label,
+ };
+ });
+
+ return (
+ <>
+ {!series.selectedMetricField && (
+ setShowOptions((prevState) => !prevState)}
+ fill
+ size="s"
+ >
+ {SELECT_REPORT_METRIC_LABEL}
+
+ }
+ isOpen={showOptions}
+ closePopover={() => setShowOptions((prevState) => !prevState)}
+ >
+
+ {options.map((option) => (
+ onChange(option.value)}
+ label={option.dropdownDisplay}
+ isDisabled={option.disabled}
+ />
+ ))}
+
+
+ )}
+ {series.selectedMetricField && (
+ onChange(undefined)}
+ iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL}
+ >
+ {
+ seriesConfig?.metricOptions?.find((option) => option.id === series.selectedMetricField)
+ ?.label
+ }
+
+ )}
+ >
+ );
+}
+
+const SELECT_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.selectReportMetric',
+ {
+ defaultMessage: 'Select report metric',
+ }
+);
+
+const REMOVE_REPORT_METRIC_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesEditor.removeReportMetric',
+ {
+ defaultMessage: 'Remove report metric',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
deleted file mode 100644
index 5d2ce6ba84951..0000000000000
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { useSeriesStorage } from '../hooks/use_series_storage';
-import { FilterLabel } from '../components/filter_label';
-import { SeriesConfig, UrlFilter } from '../types';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { useSeriesFilters } from '../hooks/use_series_filters';
-import { getFiltersFromDefs } from '../hooks/use_lens_attributes';
-
-interface Props {
- seriesId: string;
- seriesConfig: SeriesConfig;
- isNew?: boolean;
-}
-export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) {
- const { getSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const { reportDefinitions = {} } = series;
-
- const { labels } = seriesConfig;
-
- const filters: UrlFilter[] = series.filters ?? [];
-
- let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions);
-
- // we don't want to display report definition filters in new series view
- if (isNew) {
- definitionFilters = [];
- }
-
- const { removeFilter } = useSeriesFilters({ seriesId });
-
- const { indexPattern } = useAppIndexPatternContext(series.dataType);
-
- return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
-
-
- {filters.map(({ field, values, notValues }) => (
-
- {(values ?? []).map((val) => (
-
- removeFilter({ field, value: val, negate: false })}
- negate={false}
- indexPattern={indexPattern}
- />
-
- ))}
- {(notValues ?? []).map((val) => (
-
- removeFilter({ field, value: val, negate: true })}
- indexPattern={indexPattern}
- />
-
- ))}
-
- ))}
-
- {definitionFilters.map(({ field, values }) => (
-
- {(values ?? []).map((val) => (
-
- {
- // FIXME handle this use case
- }}
- negate={false}
- definitionFilter={true}
- indexPattern={indexPattern}
- />
-
- ))}
-
- ))}
-
-
- ) : null;
-}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
new file mode 100644
index 0000000000000..ea47ccd0b0426
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiAccordion, EuiSpacer } from '@elastic/eui';
+import { BuilderItem } from '../types';
+import { SeriesActions } from './columns/series_actions';
+import { SeriesInfo } from './columns/series_info';
+import { DataTypesSelect } from './columns/data_type_select';
+import { IncompleteBadge } from './columns/incomplete_badge';
+import { ExpandedSeriesRow } from './expanded_series_row';
+import { SeriesName } from './columns/series_name';
+import { ReportMetricOptions } from './report_metric_options';
+
+const StyledAccordion = styled(EuiAccordion)`
+ .euiAccordion__button {
+ width: auto;
+ flex-grow: 0;
+ }
+
+ .euiAccordion__optionalAction {
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+`;
+
+interface Props {
+ item: BuilderItem;
+ isExpanded: boolean;
+ toggleExpanded: () => void;
+}
+
+export function Series({ item, isExpanded, toggleExpanded }: Props) {
+ const { id } = item;
+ const seriesProps = {
+ ...item,
+ seriesId: id,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index c3cc8484d1751..d13857b5e9663 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -5,134 +5,226 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { SeriesFilter } from './columns/series_filter';
-import { SeriesConfig } from '../types';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
+import {
+ EuiSpacer,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiButtonEmpty,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+import { rgba } from 'polished';
+import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common';
+import { AppDataType, ReportViewType, BuilderItem } from '../types';
+import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage';
+import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
import { getDefaultConfigs } from '../configurations/default_configs';
-import { DatePickerCol } from './columns/date_picker_col';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
-import { SeriesActions } from './columns/series_actions';
-import { ChartEditOptions } from './chart_edit_options';
+import { ReportTypesSelect } from './columns/report_type_select';
+import { ViewActions } from '../views/view_actions';
+import { Series } from './series';
-interface EditItem {
- seriesConfig: SeriesConfig;
+export interface ReportTypeItem {
id: string;
+ reportType: ReportViewType;
+ label: string;
}
-export function SeriesEditor() {
- const { allSeries, allSeriesIds } = useSeriesStorage();
-
- const columns = [
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.name', {
- defaultMessage: 'Name',
- }),
- field: 'id',
- width: '15%',
- render: (seriesId: string) => (
-
- {' '}
- {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
- defaultMessage: 'Filters',
- }),
- field: 'defaultFilters',
- width: '15%',
- render: (seriesId: string, { seriesConfig, id }: EditItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
- defaultMessage: 'Breakdowns',
- }),
- field: 'id',
- width: '25%',
- render: (seriesId: string, { seriesConfig, id }: EditItem) => (
-
- ),
- },
- {
- name: (
-
-
-
- ),
- width: '20%',
- field: 'id',
- align: 'right' as const,
- render: (seriesId: string, item: EditItem) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (seriesId: string, item: EditItem) => ,
- },
- ];
-
- const { indexPatterns } = useAppIndexPatternContext();
- const items: EditItem[] = [];
-
- allSeriesIds.forEach((seriesKey) => {
- const series = allSeries[seriesKey];
- if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
- items.push({
- id: seriesKey,
- seriesConfig: getDefaultConfigs({
- indexPattern: indexPatterns[series.dataType],
- reportType: series.reportType,
- dataType: series.dataType,
- }),
+type ExpandedRowMap = Record;
+
+export const getSeriesToEdit = ({
+ indexPatterns,
+ allSeries,
+ reportType,
+}: {
+ allSeries: SeriesContextValue['allSeries'];
+ indexPatterns: IndexPatternState;
+ reportType: ReportViewType;
+}): BuilderItem[] => {
+ const getDataViewSeries = (dataType: AppDataType) => {
+ if (indexPatterns?.[dataType]) {
+ return getDefaultConfigs({
+ dataType,
+ reportType,
+ indexPattern: indexPatterns[dataType],
});
}
+ };
+
+ return allSeries.map((series, seriesIndex) => {
+ const seriesConfig = getDataViewSeries(series.dataType)!;
+
+ return { id: seriesIndex, series, seriesConfig };
});
+};
- if (items.length === 0 && allSeriesIds.length > 0) {
- return null;
- }
+export const SeriesEditor = React.memo(function () {
+ const [editorItems, setEditorItems] = useState([]);
+
+ const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({});
+
+ const [{ prevCount, curCount }, setSeriesCount] = useState<{
+ prevCount?: number;
+ curCount: number;
+ }>({
+ curCount: allSeries.length,
+ });
+
+ useEffect(() => {
+ setSeriesCount((oldParams) => ({ prevCount: oldParams.curCount, curCount: allSeries.length }));
+ if (typeof prevCount !== 'undefined' && !isNaN(prevCount) && prevCount < curCount) {
+ setItemIdToExpandedRowMap({});
+ }
+ }, [allSeries.length, curCount, prevCount]);
+
+ useEffect(() => {
+ const newExpandRows: ExpandedRowMap = {};
+
+ setEditorItems((prevState) => {
+ const newEditorItems = getSeriesToEdit({
+ reportType,
+ allSeries,
+ indexPatterns,
+ });
+
+ newEditorItems.forEach(({ series, id }) => {
+ const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id);
+ if (
+ prevSeriesItem &&
+ series.selectedMetricField &&
+ prevSeriesItem.series.selectedMetricField !== series.selectedMetricField
+ ) {
+ newExpandRows[id] = true;
+ }
+ });
+ return [...newEditorItems];
+ });
+
+ setItemIdToExpandedRowMap((prevState) => {
+ return { ...prevState, ...newExpandRows };
+ });
+ }, [allSeries, getSeries, indexPatterns, loading, reportType]);
+
+ const toggleDetails = (item: BuilderItem) => {
+ const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
+ if (itemIdToExpandedRowMapValues[item.id]) {
+ delete itemIdToExpandedRowMapValues[item.id];
+ } else {
+ itemIdToExpandedRowMapValues[item.id] = true;
+ }
+ setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
+ };
+
+ const resetView = () => {
+ const totalSeries = allSeries.length;
+ for (let i = totalSeries; i >= 0; i--) {
+ removeSeries(i);
+ }
+ setEditorItems([]);
+ setItemIdToExpandedRowMap({});
+ };
return (
- <>
-
-
-
- >
+
+
+
+
+
+
+
+
+ {reportType && (
+
+ resetView()} color="text">
+ {RESET_LABEL}
+
+
+ )}
+
+
+
+
+
+
+ {editorItems.map((item) => (
+
+ toggleDetails(item)}
+ isExpanded={itemIdToExpandedRowMap[item.id]}
+ />
+
+
+ ))}
+
+
+
);
-}
+});
+
+const Wrapper = euiStyled.div`
+ &::-webkit-scrollbar {
+ height: ${({ theme }) => theme.eui.euiScrollBar};
+ width: ${({ theme }) => theme.eui.euiScrollBar};
+ }
+ &::-webkit-scrollbar-thumb {
+ background-clip: content-box;
+ background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
+ border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
+ }
+ &::-webkit-scrollbar-corner,
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &&& {
+ .euiTableRow-isExpandedRow .euiTableRowCell {
+ border-top: none;
+ background-color: #FFFFFF;
+ border-bottom: 2px solid #d3dae6;
+ border-right: 2px solid rgb(211, 218, 230);
+ border-left: 2px solid rgb(211, 218, 230);
+ }
+
+ .isExpanded {
+ border-right: 2px solid rgb(211, 218, 230);
+ border-left: 2px solid rgb(211, 218, 230);
+ .euiTableRowCell {
+ border-bottom: none;
+ }
+ }
+ .isIncomplete .euiTableRowCell {
+ background-color: rgba(254, 197, 20, 0.1);
+ }
+ }
+`;
+
+export const LOADING_VIEW = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.loadingView',
+ {
+ defaultMessage: 'Loading view ...',
+ }
+);
+
+export const SELECT_REPORT_TYPE = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.selectReportType',
+ {
+ defaultMessage: 'No report type selected',
+ }
+);
+
+export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', {
+ defaultMessage: 'Reset',
+});
+
+export const REPORT_TYPE_LABEL = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.reportType',
+ {
+ defaultMessage: 'Report type',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 9817899412ce3..f3592a749a2c0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -6,7 +6,7 @@
*/
import { PaletteOutput } from 'src/plugins/charts/public';
-import { ExistsFilter } from '@kbn/es-query';
+import { ExistsFilter, PhraseFilter } from '@kbn/es-query';
import {
LastValueIndexPatternColumn,
DateHistogramIndexPatternColumn,
@@ -42,7 +42,7 @@ export interface MetricOption {
field?: string;
label: string;
description?: string;
- columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN';
+ columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count';
columnFilters?: ColumnFilter[];
timeScale?: string;
}
@@ -55,7 +55,7 @@ export interface SeriesConfig {
defaultSeriesType: SeriesType;
filterFields: Array;
seriesTypes: SeriesType[];
- baseFilters?: PersistableFilter[] | ExistsFilter[];
+ baseFilters?: Array;
definitionFields: string[];
metricOptions?: MetricOption[];
labels: Record;
@@ -69,6 +69,7 @@ export interface SeriesConfig {
export type URLReportDefinition = Record;
export interface SeriesUrl {
+ name: string;
time: {
to: string;
from: string;
@@ -76,12 +77,12 @@ export interface SeriesUrl {
breakdown?: string;
filters?: UrlFilter[];
seriesType?: SeriesType;
- reportType: ReportViewType;
operationType?: OperationType;
dataType: AppDataType;
reportDefinitions?: URLReportDefinition;
selectedMetricField?: string;
- isNew?: boolean;
+ hidden?: boolean;
+ color?: string;
}
export interface UrlFilter {
@@ -116,3 +117,9 @@ export interface FieldFormat {
params: FieldFormatParams;
};
}
+
+export interface BuilderItem {
+ id: number;
+ series: SeriesUrl;
+ seriesConfig?: SeriesConfig;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
new file mode 100644
index 0000000000000..978296a295efc
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.test.tsx
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { screen, waitFor, fireEvent } from '@testing-library/dom';
+import { render } from '../rtl_helpers';
+import { AddSeriesButton } from './add_series_button';
+import { DEFAULT_TIME, ReportTypes } from '../configurations/constants';
+import * as hooks from '../hooks/use_series_storage';
+
+const setSeries = jest.fn();
+
+describe('AddSeriesButton', () => {
+ beforeEach(() => {
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: [],
+ setSeries,
+ reportType: ReportTypes.KPI,
+ });
+ setSeries.mockClear();
+ });
+
+ it('renders AddSeriesButton', async () => {
+ render();
+
+ expect(screen.getByText(/Add series/i)).toBeInTheDocument();
+ });
+
+ it('calls setSeries when AddSeries Button is clicked', async () => {
+ const { rerender } = render();
+ let addSeriesButton = screen.getByText(/Add series/i);
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(1);
+ expect(setSeries).toBeCalledWith(0, { name: 'new-series-1', time: DEFAULT_TIME });
+ });
+
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: new Array(1),
+ setSeries,
+ reportType: ReportTypes.KPI,
+ });
+
+ rerender();
+
+ addSeriesButton = screen.getByText(/Add series/i);
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(1);
+ expect(setSeries).toBeCalledWith(1, { name: 'new-series-2', time: DEFAULT_TIME });
+ });
+ });
+
+ it.each([ReportTypes.DEVICE_DISTRIBUTION, ReportTypes.CORE_WEB_VITAL])(
+ 'does not allow adding more than 1 series for core web vitals or device distribution',
+ async (reportType) => {
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: new Array(1), // mock array of length 1
+ setSeries,
+ reportType,
+ });
+
+ render();
+ const addSeriesButton = screen.getByText(/Add series/i);
+ expect(addSeriesButton.closest('button')).toBeDisabled();
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(0);
+ });
+ }
+ );
+
+ it('does not allow adding a series when the report type is undefined', async () => {
+ jest.clearAllMocks();
+ jest.spyOn(hooks, 'useSeriesStorage').mockReturnValue({
+ ...jest.requireActual('../hooks/use_series_storage'),
+ allSeries: [],
+ setSeries,
+ });
+
+ render();
+ const addSeriesButton = screen.getByText(/Add series/i);
+ expect(addSeriesButton.closest('button')).toBeDisabled();
+
+ fireEvent.click(addSeriesButton);
+
+ await waitFor(() => {
+ expect(setSeries).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
new file mode 100644
index 0000000000000..71b16c9c0e682
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+
+import { EuiToolTip, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { SeriesUrl, BuilderItem } from '../types';
+import { getSeriesToEdit } from '../series_editor/series_editor';
+import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
+import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { DEFAULT_TIME, ReportTypes } from '../configurations/constants';
+
+export function AddSeriesButton() {
+ const [editorItems, setEditorItems] = useState([]);
+ const { getSeries, allSeries, setSeries, reportType } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ useEffect(() => {
+ setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType }));
+ }, [allSeries, getSeries, indexPatterns, loading, reportType]);
+
+ const addSeries = () => {
+ const prevSeries = allSeries?.[0];
+ const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`;
+ const nextSeries = { name } as SeriesUrl;
+
+ const nextSeriesId = allSeries.length;
+
+ if (reportType === 'data-distribution') {
+ setSeries(nextSeriesId, {
+ ...nextSeries,
+ time: prevSeries?.time || DEFAULT_TIME,
+ } as SeriesUrl);
+ } else {
+ setSeries(
+ nextSeriesId,
+ prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl)
+ );
+ }
+ };
+
+ const isAddDisabled =
+ !reportType ||
+ ((reportType === ReportTypes.CORE_WEB_VITAL ||
+ reportType === ReportTypes.DEVICE_DISTRIBUTION) &&
+ allSeries.length > 0);
+
+ return (
+
+ addSeries()}
+ isDisabled={isAddDisabled}
+ iconType="plusInCircle"
+ size="s"
+ >
+ {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
+ defaultMessage: 'Add series',
+ })}
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
new file mode 100644
index 0000000000000..00fbc8c0e522f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { RefObject } from 'react';
+
+import { SeriesEditor } from '../series_editor/series_editor';
+import { AddSeriesButton } from './add_series_button';
+import { PanelId } from '../exploratory_view';
+
+export function SeriesViews({
+ seriesBuilderRef,
+}: {
+ seriesBuilderRef: RefObject;
+ onSeriesPanelCollapse: (panel: PanelId) => void;
+}) {
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
new file mode 100644
index 0000000000000..f4416ef60441d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { isEqual } from 'lodash';
+import { allSeriesKey, convertAllShortSeries, useSeriesStorage } from '../hooks/use_series_storage';
+
+export function ViewActions() {
+ const { allSeries, storage, applyChanges } = useSeriesStorage();
+
+ const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? []));
+
+ return (
+
+
+ applyChanges()} isDisabled={noChanges} fill size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
+ defaultMessage: 'Apply changes',
+ })}
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
index fc562fa80e26d..0735df53888aa 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx
@@ -6,15 +6,24 @@
*/
import React, { useEffect, useState } from 'react';
-import { union } from 'lodash';
-import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui';
+import { union, isEmpty } from 'lodash';
+import {
+ EuiComboBox,
+ EuiFormControlLayout,
+ EuiComboBoxOptionOption,
+ EuiFormRow,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { FieldValueSelectionProps } from './types';
export const ALL_VALUES_SELECTED = 'ALL_VALUES';
const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => {
const uniqueValues = Array.from(
- new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values)
+ new Set(
+ allowAllValuesSelection && (values ?? []).length > 0
+ ? ['ALL_VALUES', ...(values ?? [])]
+ : values
+ )
);
return (uniqueValues ?? []).map((label) => ({
@@ -30,7 +39,9 @@ export function FieldValueCombobox({
loading,
values,
setQuery,
+ usePrependLabel = true,
compressed = true,
+ required = true,
allowAllValuesSelection,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
@@ -54,29 +65,35 @@ export function FieldValueCombobox({
onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl));
};
- return (
+ const comboBox = (
+ {
+ setQuery(searchVal);
+ }}
+ options={options}
+ selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))}
+ onChange={onChange}
+ isInvalid={required && isEmpty(selectedValue)}
+ />
+ );
+
+ return usePrependLabel ? (
- {
- setQuery(searchVal);
- }}
- options={options}
- selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))}
- onChange={onChange}
- />
+ {comboBox}
+ ) : (
+
+ {comboBox}
+
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
index aca29c4723688..dfcd917cf534b 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx
@@ -70,8 +70,8 @@ export function FieldValueSelection({
values = [],
selectedValue,
excludedValue,
- compressed = true,
allowExclusions = true,
+ compressed = true,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
const [options, setOptions] = useState(() =>
@@ -174,8 +174,8 @@ export function FieldValueSelection({
}}
options={options}
onChange={onChange}
- isLoading={loading && !query && options.length === 0}
allowExclusions={allowExclusions}
+ isLoading={loading && !query && options.length === 0}
>
{(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
index 556a8e7052347..6671c43dd8c7b 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx
@@ -95,6 +95,7 @@ describe('FieldValueSuggestions', () => {
selectedValue={[]}
filters={[]}
asCombobox={false}
+ allowExclusions={true}
/>
);
@@ -119,6 +120,7 @@ describe('FieldValueSuggestions', () => {
excludedValue={['Pak']}
filters={[]}
asCombobox={false}
+ allowExclusions={true}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
index 3de158ba0622f..1c5da15dd33df 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx
@@ -28,9 +28,11 @@ export function FieldValueSuggestions({
singleSelection,
compressed,
asFilterButton,
+ usePrependLabel,
allowAllValuesSelection,
+ required,
+ allowExclusions = true,
cardinalityField,
- allowExclusions,
asCombobox = true,
onChange: onSelectionChange,
}: FieldValueSuggestionsProps) {
@@ -67,8 +69,10 @@ export function FieldValueSuggestions({
width={width}
compressed={compressed}
asFilterButton={asFilterButton}
- allowAllValuesSelection={allowAllValuesSelection}
+ usePrependLabel={usePrependLabel}
allowExclusions={allowExclusions}
+ allowAllValuesSelection={allowAllValuesSelection}
+ required={required}
/>
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
index 046f98748cdf2..b6de2bafdd852 100644
--- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts
@@ -23,10 +23,11 @@ interface CommonProps {
compressed?: boolean;
asFilterButton?: boolean;
showCount?: boolean;
+ usePrependLabel?: boolean;
+ allowExclusions?: boolean;
allowAllValuesSelection?: boolean;
cardinalityField?: string;
required?: boolean;
- allowExclusions?: boolean;
}
export type FieldValueSuggestionsProps = CommonProps & {
diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
index 01d727071770d..9e7b96b02206f 100644
--- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx
@@ -18,21 +18,25 @@ export function buildFilterLabel({
negate,
}: {
label: string;
- value: string;
+ value: string | string[];
negate: boolean;
field: string;
indexPattern: IndexPattern;
}) {
const indexField = indexPattern.getFieldByName(field)!;
- const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern);
+ const filter =
+ value instanceof Array && value.length > 1
+ ? esFilters.buildPhrasesFilter(indexField, value, indexPattern)
+ : esFilters.buildPhraseFilter(indexField, value as string, indexPattern);
- filter.meta.value = value;
+ filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase';
+
+ filter.meta.value = value as string;
filter.meta.key = label;
filter.meta.alias = null;
filter.meta.negate = negate;
filter.meta.disabled = false;
- filter.meta.type = 'phrase';
return filter;
}
@@ -40,10 +44,10 @@ export function buildFilterLabel({
interface Props {
field: string;
label: string;
- value: string;
+ value: string | string[];
negate: boolean;
- removeFilter: (field: string, value: string, notVal: boolean) => void;
- invertFilter: (val: { field: string; value: string; negate: boolean }) => void;
+ removeFilter: (field: string, value: string | string[], notVal: boolean) => void;
+ invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void;
indexPattern: IndexPattern;
allowExclusion?: boolean;
}
diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx
index 9d557a40b7987..afc053604fcdf 100644
--- a/x-pack/plugins/observability/public/components/shared/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/index.tsx
@@ -6,6 +6,7 @@
*/
import React, { lazy, Suspense } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
import type { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type { FieldValueSuggestionsProps } from './field_value_suggestions/types';
@@ -26,7 +27,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal'));
export function HeaderMenuPortal(props: HeaderMenuPortalProps) {
return (
-
+ }>
);
diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
index 82a0fc39b8519..198b4092b0ed6 100644
--- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
+++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx
@@ -7,7 +7,7 @@
import { useUiSetting } from '../../../../../src/plugins/kibana_react/public';
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
-import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker';
+import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker';
export function useQuickTimeRanges() {
const timePickerQuickRanges = useUiSetting
(
diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts
index 118f0783f9688..10843bbd1d5b5 100644
--- a/x-pack/plugins/observability/public/plugin.ts
+++ b/x-pack/plugins/observability/public/plugin.ts
@@ -24,6 +24,7 @@ import type {
DataPublicPluginSetup,
DataPublicPluginStart,
} from '../../../../src/plugins/data/public';
+import type { DiscoverStart } from '../../../../src/plugins/discover/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type {
HomePublicPluginSetup,
@@ -58,6 +59,7 @@ export interface ObservabilityPublicPluginsStart {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
data: DataPublicPluginStart;
lens: LensPublicStart;
+ discover: DiscoverStart;
}
export type ObservabilityPublicStart = ReturnType;
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
index 00e487da7f9b7..ff03379e39963 100644
--- a/x-pack/plugins/observability/public/routes/index.tsx
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -99,7 +99,7 @@ export const routes = {
}),
},
},
- '/exploratory-view': {
+ '/exploratory-view/': {
handler: () => {
return ;
},
@@ -112,18 +112,4 @@ export const routes = {
}),
},
},
- // enable this to test multi series architecture
- // '/exploratory-view/multi': {
- // handler: () => {
- // return ;
- // },
- // params: {
- // query: t.partial({
- // rangeFrom: t.string,
- // rangeTo: t.string,
- // refreshPaused: jsonRt.pipe(t.boolean),
- // refreshInterval: jsonRt.pipe(t.number),
- // }),
- // },
- // },
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index c2d46fa5762d2..5da17d8a746a0 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -18947,36 +18947,19 @@
"xpack.observability.expView.operationType.95thPercentile": "95パーセンタイル",
"xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル",
"xpack.observability.expView.operationType.average": "平均",
- "xpack.observability.expView.operationType.label": "計算",
"xpack.observability.expView.operationType.median": "中央",
"xpack.observability.expView.operationType.sum": "合計",
- "xpack.observability.expView.reportType.noDataType": "データ型が選択されていません。",
"xpack.observability.expView.reportType.selectDataType": "ビジュアライゼーションを作成するデータ型を選択します。",
- "xpack.observability.expView.seriesBuilder.actions": "アクション",
"xpack.observability.expView.seriesBuilder.addSeries": "数列を追加",
"xpack.observability.expView.seriesBuilder.apply": "変更を適用",
- "xpack.observability.expView.seriesBuilder.autoApply": "自動適用",
- "xpack.observability.expView.seriesBuilder.breakdown": "内訳",
- "xpack.observability.expView.seriesBuilder.dataType": "データ型",
- "xpack.observability.expView.seriesBuilder.definition": "定義",
"xpack.observability.expView.seriesBuilder.emptyReportDefinition": "ビジュアライゼーションを作成するレポート定義を選択します。",
"xpack.observability.expView.seriesBuilder.emptyview": "表示する情報がありません。",
- "xpack.observability.expView.seriesBuilder.filters": "フィルター",
"xpack.observability.expView.seriesBuilder.loadingView": "ビューを読み込んでいます...",
- "xpack.observability.expView.seriesBuilder.report": "レポート",
- "xpack.observability.expView.seriesBuilder.selectDataType": "データ型が選択されていません",
"xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプが選択されていません",
"xpack.observability.expView.seriesBuilder.selectReportType.empty": "レポートタイプを選択すると、ビジュアライゼーションを作成します。",
- "xpack.observability.expView.seriesEditor.actions": "アクション",
- "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します",
- "xpack.observability.expView.seriesEditor.breakdowns": "内訳",
"xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去",
- "xpack.observability.expView.seriesEditor.filters": "フィルター",
- "xpack.observability.expView.seriesEditor.name": "名前",
"xpack.observability.expView.seriesEditor.notFound": "系列が見つかりません。系列を追加してください。",
"xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します",
- "xpack.observability.expView.seriesEditor.seriesNotFound": "系列が見つかりません。系列を追加してください。",
- "xpack.observability.expView.seriesEditor.time": "時間",
"xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。",
"xpack.observability.featureCatalogueTitle": "オブザーバビリティ",
"xpack.observability.featureRegistry.linkObservabilityTitle": "ケース",
@@ -19038,7 +19021,6 @@
"xpack.observability.overview.ux.title": "ユーザーエクスペリエンス",
"xpack.observability.overviewLinkTitle": "概要",
"xpack.observability.pageLayout.sideNavTitle": "オブザーバビリティ",
- "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません",
"xpack.observability.resources.documentation": "ドキュメント",
"xpack.observability.resources.forum": "ディスカッションフォーラム",
"xpack.observability.resources.quick_start": "クイックスタートビデオ",
@@ -19054,8 +19036,6 @@
"xpack.observability.section.apps.uptime.title": "アップタイム",
"xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください",
"xpack.observability.seriesEditor.clone": "系列をコピー",
- "xpack.observability.seriesEditor.edit": "系列を編集",
- "xpack.observability.seriesEditor.save": "系列を保存",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.average": "平均",
"xpack.observability.ux.coreVitals.averageMessage": " {bad}未満",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index e3f53a34449ef..6d5b94f49cb6f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -19220,36 +19220,19 @@
"xpack.observability.expView.operationType.95thPercentile": "第 95 个百分位",
"xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位",
"xpack.observability.expView.operationType.average": "平均值",
- "xpack.observability.expView.operationType.label": "计算",
"xpack.observability.expView.operationType.median": "中值",
"xpack.observability.expView.operationType.sum": "求和",
- "xpack.observability.expView.reportType.noDataType": "未选择任何数据类型。",
"xpack.observability.expView.reportType.selectDataType": "选择数据类型以创建可视化。",
- "xpack.observability.expView.seriesBuilder.actions": "操作",
"xpack.observability.expView.seriesBuilder.addSeries": "添加序列",
"xpack.observability.expView.seriesBuilder.apply": "应用更改",
- "xpack.observability.expView.seriesBuilder.autoApply": "自动应用",
- "xpack.observability.expView.seriesBuilder.breakdown": "分解",
- "xpack.observability.expView.seriesBuilder.dataType": "数据类型",
- "xpack.observability.expView.seriesBuilder.definition": "定义",
"xpack.observability.expView.seriesBuilder.emptyReportDefinition": "选择报告定义以创建可视化。",
"xpack.observability.expView.seriesBuilder.emptyview": "没有可显示的内容。",
- "xpack.observability.expView.seriesBuilder.filters": "筛选",
"xpack.observability.expView.seriesBuilder.loadingView": "正在加载视图......",
- "xpack.observability.expView.seriesBuilder.report": "报告",
- "xpack.observability.expView.seriesBuilder.selectDataType": "未选择任何数据类型",
"xpack.observability.expView.seriesBuilder.selectReportType": "未选择任何报告类型",
"xpack.observability.expView.seriesBuilder.selectReportType.empty": "选择报告类型以创建可视化。",
- "xpack.observability.expView.seriesEditor.actions": "操作",
- "xpack.observability.expView.seriesEditor.addFilter": "添加筛选",
- "xpack.observability.expView.seriesEditor.breakdowns": "分解",
"xpack.observability.expView.seriesEditor.clearFilter": "清除筛选",
- "xpack.observability.expView.seriesEditor.filters": "筛选",
- "xpack.observability.expView.seriesEditor.name": "名称",
"xpack.observability.expView.seriesEditor.notFound": "未找到任何序列。请添加序列。",
"xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列",
- "xpack.observability.expView.seriesEditor.seriesNotFound": "未找到任何序列。请添加序列。",
- "xpack.observability.expView.seriesEditor.time": "时间",
"xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。",
"xpack.observability.featureCatalogueTitle": "可观测性",
"xpack.observability.featureRegistry.linkObservabilityTitle": "案例",
@@ -19311,7 +19294,6 @@
"xpack.observability.overview.ux.title": "用户体验",
"xpack.observability.overviewLinkTitle": "概览",
"xpack.observability.pageLayout.sideNavTitle": "可观测性",
- "xpack.observability.reportTypeCol.nodata": "没有可用数据",
"xpack.observability.resources.documentation": "文档",
"xpack.observability.resources.forum": "讨论论坛",
"xpack.observability.resources.quick_start": "快速入门视频",
@@ -19327,8 +19309,6 @@
"xpack.observability.section.apps.uptime.title": "运行时间",
"xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试",
"xpack.observability.seriesEditor.clone": "复制序列",
- "xpack.observability.seriesEditor.edit": "编辑序列",
- "xpack.observability.seriesEditor.save": "保存序列",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.average": "平均值",
"xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}",
diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
index 1a53a2c9b64a0..aa981071b7ee2 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
+++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx
@@ -22,6 +22,7 @@ import React, { useContext } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import numeral from '@elastic/numeral';
import moment from 'moment';
+import { useSelector } from 'react-redux';
import { getChartDateLabel } from '../../../lib/helper';
import { ChartWrapper } from './chart_wrapper';
import { UptimeThemeContext } from '../../../contexts';
@@ -32,6 +33,7 @@ import { getDateRangeFromChartElement } from './utils';
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations';
import { createExploratoryViewUrl } from '../../../../../observability/public';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
+import { monitorStatusSelector } from '../../../state/selectors';
export interface PingHistogramComponentProps {
/**
@@ -73,6 +75,8 @@ export const PingHistogramComponent: React.FC = ({
const monitorId = useMonitorId();
+ const selectedMonitor = useSelector(monitorStatusSelector);
+
const { basePath } = useUptimeSettingsContext();
const [getUrlParams, updateUrlParams] = useUrlParams();
@@ -189,12 +193,21 @@ export const PingHistogramComponent: React.FC = ({
const pingHistogramExploratoryViewLink = createExploratoryViewUrl(
{
- 'pings-over-time': {
- dataType: 'synthetics',
- reportType: 'kpi-over-time',
- time: { from: dateRangeStart, to: dateRangeEnd },
- ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}),
- },
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${monitorId}-pings`,
+ dataType: 'synthetics',
+ selectedMetricField: 'summary.up',
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ reportDefinitions: {
+ 'monitor.name':
+ monitorId && selectedMonitor?.monitor?.name
+ ? [selectedMonitor.monitor.name]
+ : ['ALL_VALUES'],
+ },
+ },
+ ],
},
basePath
);
diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
index ef5e10394739a..c459fe46da975 100644
--- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
+++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx
@@ -10,13 +10,15 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useHistory } from 'react-router-dom';
-import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public';
+import { useSelector } from 'react-redux';
+import { createExploratoryViewUrl } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
import { useGetUrlParams } from '../../../hooks';
import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers';
import { SETTINGS_ROUTE } from '../../../../common/constants';
import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params';
+import { monitorStatusSelector } from '../../../state/selectors';
const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', {
defaultMessage: 'Add data',
@@ -38,13 +40,28 @@ export function ActionMenuContent(): React.ReactElement {
const { dateRangeStart, dateRangeEnd } = params;
const history = useHistory();
+ const selectedMonitor = useSelector(monitorStatusSelector);
+
+ const monitorId = selectedMonitor?.monitor?.id;
+
const syntheticExploratoryViewLink = createExploratoryViewUrl(
{
- 'synthetics-series': {
- dataType: 'synthetics',
- isNew: true,
- time: { from: dateRangeStart, to: dateRangeEnd },
- } as unknown as SeriesUrl,
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ dataType: 'synthetics',
+ seriesType: 'area_stacked',
+ selectedMetricField: 'monitor.duration.us',
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ breakdown: monitorId ? 'observer.geo.name' : 'monitor.type',
+ reportDefinitions: {
+ 'monitor.name': selectedMonitor?.monitor?.name
+ ? [selectedMonitor?.monitor?.name]
+ : ['ALL_VALUES'],
+ },
+ name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration',
+ },
+ ],
},
basePath
);
diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
index cbfba4ffcb239..35eab80c15967 100644
--- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
@@ -51,16 +51,19 @@ export const MonitorDuration: React.FC = ({ monitorId }) => {
const exploratoryViewLink = createExploratoryViewUrl(
{
- [`monitor-duration`]: {
- reportType: 'kpi-over-time',
- time: { from: dateRangeStart, to: dateRangeEnd },
- reportDefinitions: {
- 'monitor.id': [monitorId] as string[],
+ reportType: 'kpi-over-time',
+ allSeries: [
+ {
+ name: `${monitorId}-response-duration`,
+ time: { from: dateRangeStart, to: dateRangeEnd },
+ reportDefinitions: {
+ 'monitor.id': [monitorId] as string[],
+ },
+ breakdown: 'observer.geo.name',
+ operationType: 'average',
+ dataType: 'synthetics',
},
- breakdown: 'observer.geo.name',
- operationType: 'average',
- dataType: 'synthetics',
- },
+ ],
},
basePath
);
diff --git a/x-pack/test/observability_functional/apps/observability/exploratory_view.ts b/x-pack/test/observability_functional/apps/observability/exploratory_view.ts
new file mode 100644
index 0000000000000..8f27f20ce30e6
--- /dev/null
+++ b/x-pack/test/observability_functional/apps/observability/exploratory_view.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Path from 'path';
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['observability', 'common', 'header']);
+ const esArchiver = getService('esArchiver');
+ const find = getService('find');
+
+ const testSubjects = getService('testSubjects');
+
+ const rangeFrom = '2021-01-17T16%3A46%3A15.338Z';
+ const rangeTo = '2021-01-19T17%3A01%3A32.309Z';
+
+ // Failing: See https://github.com/elastic/kibana/issues/106934
+ describe.skip('ExploratoryView', () => {
+ before(async () => {
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0')
+ );
+
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0')
+ );
+
+ await esArchiver.loadIfNeeded(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data')
+ );
+
+ await PageObjects.common.navigateToApp('ux', {
+ search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`,
+ });
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ after(async () => {
+ await esArchiver.unload(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0')
+ );
+
+ await esArchiver.unload(
+ Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0')
+ );
+ });
+
+ it('should able to open exploratory view from ux app', async () => {
+ await testSubjects.exists('uxAnalyzeBtn');
+ await testSubjects.click('uxAnalyzeBtn');
+ expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true);
+ });
+
+ it('renders lens visualization', async () => {
+ expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true);
+
+ expect(
+ await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]')
+ ).to.eql(true);
+
+ expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true);
+ });
+
+ it('can do a breakdown per series', async () => {
+ await testSubjects.click('seriesBreakdown');
+
+ expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true);
+
+ await find.clickByCssSelector('[id="user_agent.name"]');
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true);
+ expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true);
+ });
+ });
+}
diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts
index 019fb0994715e..b163d4d6bb8d5 100644
--- a/x-pack/test/observability_functional/apps/observability/index.ts
+++ b/x-pack/test/observability_functional/apps/observability/index.ts
@@ -8,9 +8,10 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
- describe('Observability specs', function () {
+ describe('ObservabilityApp', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./feature_controls'));
+ loadTestFile(require.resolve('./exploratory_view'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./alerts/workflow_status'));
loadTestFile(require.resolve('./alerts/pagination'));
From 8c89daedba4a9ca4b361bc72f9d00a6094d3c4bf Mon Sep 17 00:00:00 2001
From: ymao1
Date: Mon, 4 Oct 2021 10:06:07 -0400
Subject: [PATCH 05/14] Adding range filter to ownerId aggregation (#113557)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/workload_statistics.test.ts | 27 +++++++++++++++----
.../server/monitoring/workload_statistics.ts | 21 ++++++++++++---
2 files changed, 39 insertions(+), 9 deletions(-)
diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
index 9c697be985155..9628e2807627a 100644
--- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
+++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
@@ -65,7 +65,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 1,
+ ownerIds: {
+ value: 1,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
@@ -127,8 +129,19 @@ describe('Workload Statistics Aggregator', () => {
missing: { field: 'task.schedule' },
},
ownerIds: {
- cardinality: {
- field: 'task.ownerId',
+ filter: {
+ range: {
+ 'task.startedAt': {
+ gte: 'now-1w/w',
+ },
+ },
+ },
+ aggs: {
+ ownerIds: {
+ cardinality: {
+ field: 'task.ownerId',
+ },
+ },
},
},
idleTasks: {
@@ -264,7 +277,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 1,
+ ownerIds: {
+ value: 1,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
@@ -605,7 +620,9 @@ describe('Workload Statistics Aggregator', () => {
doc_count: 13,
},
ownerIds: {
- value: 3,
+ ownerIds: {
+ value: 3,
+ },
},
// The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate`
// would not have a `buckets` property, but rather a keyed property that's inferred from the request.
diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
index b833e4ed57530..9ac528cfd1ced 100644
--- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
+++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts
@@ -147,8 +147,19 @@ export function createWorkloadAggregator(
missing: { field: 'task.schedule' },
},
ownerIds: {
- cardinality: {
- field: 'task.ownerId',
+ filter: {
+ range: {
+ 'task.startedAt': {
+ gte: 'now-1w/w',
+ },
+ },
+ },
+ aggs: {
+ ownerIds: {
+ cardinality: {
+ field: 'task.ownerId',
+ },
+ },
},
},
idleTasks: {
@@ -213,7 +224,7 @@ export function createWorkloadAggregator(
const taskTypes = aggregations.taskType.buckets;
const nonRecurring = aggregations.nonRecurringTasks.doc_count;
- const ownerIds = aggregations.ownerIds.value;
+ const ownerIds = aggregations.ownerIds.ownerIds.value;
const {
overdue: {
@@ -448,7 +459,9 @@ export interface WorkloadAggregationResponse {
doc_count: number;
};
ownerIds: {
- value: number;
+ ownerIds: {
+ value: number;
+ };
};
[otherAggs: string]: estypes.AggregationsAggregate;
}
From 69bee186c27aca043ab7393d91e4ad608ae6b35c Mon Sep 17 00:00:00 2001
From: "Joey F. Poon"
Date: Mon, 4 Oct 2021 09:48:01 -0500
Subject: [PATCH 06/14] [Security Solution] create task for auto restarting
failed OLM transforms (#113686)
---
.../security_solution/common/constants.ts | 21 +-
.../common/endpoint/constants.ts | 7 +-
.../management/pages/endpoint_hosts/mocks.ts | 8 +-
.../pages/endpoint_hosts/store/middleware.ts | 6 +-
.../store/mock_endpoint_result_list.ts | 4 +-
.../management/pages/endpoint_hosts/types.ts | 20 +-
.../pages/endpoint_hosts/view/index.test.tsx | 9 +-
.../pages/endpoint_hosts/view/index.tsx | 2 +-
.../check_metadata_transforms_task.test.ts | 250 ++++++++++++++++++
.../check_metadata_transforms_task.ts | 214 +++++++++++++++
.../server/endpoint/lib/metadata/index.ts | 8 +
.../security_solution/server/plugin.ts | 12 +
12 files changed, 526 insertions(+), 35 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 4316b1c033ec6..e91f74320c026 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -7,7 +7,7 @@
import type { TransformConfigSchema } from './transforms/types';
import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
-import { metadataTransformPattern } from './endpoint/constants';
+import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants';
export const APP_ID = 'securitySolution';
export const CASES_FEATURE_ID = 'securitySolutionCases';
@@ -331,6 +331,23 @@ export const showAllOthersBucket: string[] = [
*/
export const ELASTIC_NAME = 'estc';
-export const TRANSFORM_STATS_URL = `/api/transform/transforms/${metadataTransformPattern}-*/_stats`;
+export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`;
export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest';
+
+export const TRANSFORM_STATES = {
+ ABORTING: 'aborting',
+ FAILED: 'failed',
+ INDEXING: 'indexing',
+ STARTED: 'started',
+ STOPPED: 'stopped',
+ STOPPING: 'stopping',
+ WAITING: 'waiting',
+};
+
+export const WARNING_TRANSFORM_STATES = new Set([
+ TRANSFORM_STATES.ABORTING,
+ TRANSFORM_STATES.FAILED,
+ TRANSFORM_STATES.STOPPED,
+ TRANSFORM_STATES.STOPPING,
+]);
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index a38266c414e6b..c7949299c68db 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -20,10 +20,13 @@ export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*'
/** The metadata Transform Name prefix with NO (package) version) */
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
-/** The metadata Transform Name prefix with NO namespace and NO (package) version) */
-export const metadataTransformPattern = 'endpoint.metadata_current-*';
+// metadata transforms pattern for matching all metadata transform ids
+export const METADATA_TRANSFORMS_PATTERN = 'endpoint.metadata_*';
+// united metadata transform id
export const METADATA_UNITED_TRANSFORM = 'endpoint.metadata_united-default';
+
+// united metadata transform destination index
export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
index cf3f53b5b2ea9..010fe48f29418 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
@@ -37,8 +37,8 @@ import {
PendingActionsHttpMockInterface,
pendingActionsHttpMock,
} from '../../../common/lib/endpoint_pending_actions/mocks';
-import { TRANSFORM_STATS_URL } from '../../../../common/constants';
-import { TransformStatsResponse, TRANSFORM_STATE } from './types';
+import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants';
+import { TransformStatsResponse } from './types';
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
metadataList: () => HostResultList;
@@ -238,14 +238,14 @@ export const failedTransformStateMock = {
count: 1,
transforms: [
{
- state: TRANSFORM_STATE.FAILED,
+ state: TRANSFORM_STATES.FAILED,
},
],
};
export const transformsHttpMocks = httpHandlerMockFactory([
{
id: 'metadataTransformStats',
- path: TRANSFORM_STATS_URL,
+ path: METADATA_TRANSFORM_STATS_URL,
method: 'get',
handler: () => failedTransformStateMock,
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index 84cf3513d5d3a..7a45ff06c496b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -78,7 +78,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari
import { EndpointPackageInfoStateChanged } from './action';
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
import { getIsInvalidDateRange } from '../utils';
-import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
+import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
type EndpointPageStore = ImmutableMiddlewareAPI;
@@ -785,7 +785,9 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E
});
try {
- const transformStatsResponse: TransformStatsResponse = await http.get(TRANSFORM_STATS_URL);
+ const transformStatsResponse: TransformStatsResponse = await http.get(
+ METADATA_TRANSFORM_STATS_URL
+ );
dispatch({
type: 'metadataTransformStatsChanged',
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
index 8e8e5a61221a9..2e3de427e6960 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts
@@ -30,7 +30,7 @@ import {
import { GetPolicyListResponse } from '../../policy/types';
import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks';
import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants';
-import { TRANSFORM_STATS_URL } from '../../../../../common/constants';
+import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants';
import { TransformStats, TransformStatsResponse } from '../types';
const generator = new EndpointDocGenerator('seed');
@@ -163,7 +163,7 @@ const endpointListApiPathHandlerMocks = ({
return pendingActionsResponseMock();
},
- [TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
+ [METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({
count: transforms.length,
transforms,
}),
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
index dd0bc79f1ba52..0fa96fe00fd2c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
@@ -22,6 +22,7 @@ import { ServerApiError } from '../../../common/types';
import { GetPackagesResponse } from '../../../../../fleet/common';
import { IIndexPattern } from '../../../../../../../src/plugins/data/public';
import { AsyncResourceState } from '../../state';
+import { TRANSFORM_STATES } from '../../../../common/constants';
export interface EndpointState {
/** list of host **/
@@ -143,24 +144,7 @@ export interface EndpointIndexUIQueryParams {
admin_query?: string;
}
-export const TRANSFORM_STATE = {
- ABORTING: 'aborting',
- FAILED: 'failed',
- INDEXING: 'indexing',
- STARTED: 'started',
- STOPPED: 'stopped',
- STOPPING: 'stopping',
- WAITING: 'waiting',
-};
-
-export const WARNING_TRANSFORM_STATES = new Set([
- TRANSFORM_STATE.ABORTING,
- TRANSFORM_STATE.FAILED,
- TRANSFORM_STATE.STOPPED,
- TRANSFORM_STATE.STOPPING,
-]);
-
-const transformStates = Object.values(TRANSFORM_STATE);
+const transformStates = Object.values(TRANSFORM_STATES);
export type TransformState = typeof transformStates[number];
export interface TransformStats {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 33c45e6e2f548..b2c438659b771 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -46,8 +46,9 @@ import {
APP_PATH,
MANAGEMENT_PATH,
DEFAULT_TIMEPICKER_QUICK_RANGES,
+ TRANSFORM_STATES,
} from '../../../../../common/constants';
-import { TransformStats, TRANSFORM_STATE } from '../types';
+import { TransformStats } from '../types';
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
@@ -1403,7 +1404,7 @@ describe('when on the endpoint list page', () => {
const transforms: TransformStats[] = [
{
id: `${metadataTransformPrefix}-0.20.0`,
- state: TRANSFORM_STATE.STARTED,
+ state: TRANSFORM_STATES.STARTED,
} as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
@@ -1414,7 +1415,7 @@ describe('when on the endpoint list page', () => {
it('is not displayed when non-relevant transform is failing', () => {
const transforms: TransformStats[] = [
- { id: 'not-metadata', state: TRANSFORM_STATE.FAILED } as TransformStats,
+ { id: 'not-metadata', state: TRANSFORM_STATES.FAILED } as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
render();
@@ -1426,7 +1427,7 @@ describe('when on the endpoint list page', () => {
const transforms: TransformStats[] = [
{
id: `${metadataTransformPrefix}-0.20.0`,
- state: TRANSFORM_STATE.FAILED,
+ state: TRANSFORM_STATES.FAILED,
} as TransformStats,
];
setEndpointListApiMockImplementation(coreStart.http, { transforms });
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index e71474321c868..7845409353898 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -58,8 +58,8 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
import { TableRowActions } from './components/table_row_actions';
import { EndpointAgentStatus } from './components/endpoint_agent_status';
import { CallOut } from '../../../../common/components/callouts';
-import { WARNING_TRANSFORM_STATES } from '../types';
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
+import { WARNING_TRANSFORM_STATES } from '../../../../../common/constants';
const MAX_PAGINATED_ITEM = 9999;
const TRANSFORM_URL = '/data/transform';
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
new file mode 100644
index 0000000000000..0510743fdf05b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.test.ts
@@ -0,0 +1,250 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ApiResponse } from '@elastic/elasticsearch';
+import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/api/types';
+import {
+ CheckMetadataTransformsTask,
+ TYPE,
+ VERSION,
+ BASE_NEXT_ATTEMPT_DELAY,
+} from './check_metadata_transforms_task';
+import { createMockEndpointAppContext } from '../../mocks';
+import { coreMock } from '../../../../../../../src/core/server/mocks';
+import { taskManagerMock } from '../../../../../task_manager/server/mocks';
+import { TaskManagerSetupContract, TaskStatus } from '../../../../../task_manager/server';
+import { CoreSetup } from '../../../../../../../src/core/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
+import { TRANSFORM_STATES } from '../../../../common/constants';
+import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
+import { RunResult } from '../../../../../task_manager/server/task';
+
+const MOCK_TASK_INSTANCE = {
+ id: `${TYPE}:${VERSION}`,
+ runAt: new Date(),
+ attempts: 0,
+ ownerId: '',
+ status: TaskStatus.Running,
+ startedAt: new Date(),
+ scheduledAt: new Date(),
+ retryAt: new Date(),
+ params: {},
+ state: {},
+ taskType: TYPE,
+};
+const failedTransformId = 'failing-transform';
+const goodTransformId = 'good-transform';
+
+describe('check metadata transforms task', () => {
+ const { createSetup: coreSetupMock } = coreMock;
+ const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock;
+
+ let mockTask: CheckMetadataTransformsTask;
+ let mockCore: CoreSetup;
+ let mockTaskManagerSetup: jest.Mocked;
+ beforeAll(() => {
+ mockCore = coreSetupMock();
+ mockTaskManagerSetup = tmSetupMock();
+ mockTask = new CheckMetadataTransformsTask({
+ endpointAppContext: createMockEndpointAppContext(),
+ core: mockCore,
+ taskManager: mockTaskManagerSetup,
+ });
+ });
+
+ describe('task lifecycle', () => {
+ it('should create task', () => {
+ expect(mockTask).toBeInstanceOf(CheckMetadataTransformsTask);
+ });
+
+ it('should register task', () => {
+ expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled();
+ });
+
+ it('should schedule task', async () => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
+ });
+ });
+
+ describe('task logic', () => {
+ let esClient: ElasticsearchClientMock;
+ beforeEach(async () => {
+ const [{ elasticsearch }] = await mockCore.getStartServices();
+ esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock;
+ });
+
+ const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => {
+ const mockTaskManagerStart = tmStartMock();
+ await mockTask.start({ taskManager: mockTaskManagerStart });
+ const createTaskRunner =
+ mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner;
+ const taskRunner = createTaskRunner({ taskInstance });
+ return taskRunner.run();
+ };
+
+ const buildFailedStatsResponse = () =>
+ ({
+ body: {
+ transforms: [
+ {
+ id: goodTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ {
+ id: failedTransformId,
+ state: TRANSFORM_STATES.FAILED,
+ },
+ ],
+ },
+ } as unknown as ApiResponse);
+
+ it('should stop task if transform stats response fails', async () => {
+ esClient.transform.getTransformStats.mockRejectedValue({});
+ await runTask();
+ expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ expect(esClient.transform.stopTransform).not.toHaveBeenCalled();
+ expect(esClient.transform.startTransform).not.toHaveBeenCalled();
+ });
+
+ it('should attempt transform restart if failing state', async () => {
+ const transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ const taskResponse = (await runTask()) as RunResult;
+
+ expect(esClient.transform.getTransformStats).toHaveBeenCalledWith({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ expect(esClient.transform.stopTransform).toHaveBeenCalledWith({
+ transform_id: failedTransformId,
+ allow_no_match: true,
+ wait_for_completion: true,
+ force: true,
+ });
+ expect(esClient.transform.startTransform).toHaveBeenCalledWith({
+ transform_id: failedTransformId,
+ });
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 0,
+ });
+ });
+
+ it('should correctly track transform restart attempts', async () => {
+ const transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ let taskResponse = (await runTask()) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 1,
+ });
+
+ esClient.transform.startTransform.mockRejectedValueOnce({});
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 2,
+ });
+
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ expect(taskResponse?.state?.attempts).toEqual({
+ [goodTransformId]: 0,
+ [failedTransformId]: 0,
+ });
+ });
+
+ it('should correctly back off subsequent restart attempts', async () => {
+ let transformStatsResponseMock = buildFailedStatsResponse();
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ let taskStartedAt = new Date();
+ let taskResponse = (await runTask()) as RunResult;
+ let delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
+ let expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ let expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ esClient.transform.startTransform.mockRejectedValueOnce({});
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // should be exponential on second+ attempt
+ delay = BASE_NEXT_ATTEMPT_DELAY ** 2 * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ esClient.transform.stopTransform.mockRejectedValueOnce({});
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // should be exponential on second+ attempt
+ delay = BASE_NEXT_ATTEMPT_DELAY ** 3 * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ taskStartedAt = new Date();
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // back to base delay after success
+ delay = BASE_NEXT_ATTEMPT_DELAY * 60000;
+ expectedRunAt = taskStartedAt.getTime() + delay;
+ expect(taskResponse?.runAt?.getTime()).toBeGreaterThanOrEqual(expectedRunAt);
+ // we don't have the exact timestamp it uses so give a buffer
+ expectedRunAtUpperBound = expectedRunAt + 1000;
+ expect(taskResponse?.runAt?.getTime()).toBeLessThanOrEqual(expectedRunAtUpperBound);
+
+ transformStatsResponseMock = {
+ body: {
+ transforms: [
+ {
+ id: goodTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ {
+ id: failedTransformId,
+ state: TRANSFORM_STATES.STARTED,
+ },
+ ],
+ },
+ } as unknown as ApiResponse;
+ esClient.transform.getTransformStats.mockResolvedValue(transformStatsResponseMock);
+ taskResponse = (await runTask({
+ ...MOCK_TASK_INSTANCE,
+ state: taskResponse.state,
+ })) as RunResult;
+ // no more explicit runAt after subsequent success
+ expect(taskResponse?.runAt).toBeUndefined();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
new file mode 100644
index 0000000000000..68f149bcc64c4
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/check_metadata_transforms_task.ts
@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ApiResponse } from '@elastic/elasticsearch';
+import {
+ TransformGetTransformStatsResponse,
+ TransformGetTransformStatsTransformStats,
+} from '@elastic/elasticsearch/api/types';
+import { CoreSetup, ElasticsearchClient, Logger } from 'src/core/server';
+import {
+ ConcreteTaskInstance,
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+ throwUnrecoverableError,
+} from '../../../../../task_manager/server';
+import { EndpointAppContext } from '../../types';
+import { METADATA_TRANSFORMS_PATTERN } from '../../../../common/endpoint/constants';
+import { WARNING_TRANSFORM_STATES } from '../../../../common/constants';
+import { wrapErrorIfNeeded } from '../../utils';
+
+const SCOPE = ['securitySolution'];
+const INTERVAL = '2h';
+const TIMEOUT = '4m';
+export const TYPE = 'endpoint:metadata-check-transforms-task';
+export const VERSION = '0.0.1';
+const MAX_ATTEMPTS = 5;
+export const BASE_NEXT_ATTEMPT_DELAY = 5; // minutes
+
+export interface CheckMetadataTransformsTaskSetupContract {
+ endpointAppContext: EndpointAppContext;
+ core: CoreSetup;
+ taskManager: TaskManagerSetupContract;
+}
+
+export interface CheckMetadataTransformsTaskStartContract {
+ taskManager: TaskManagerStartContract;
+}
+
+export class CheckMetadataTransformsTask {
+ private logger: Logger;
+ private wasStarted: boolean = false;
+
+ constructor(setupContract: CheckMetadataTransformsTaskSetupContract) {
+ const { endpointAppContext, core, taskManager } = setupContract;
+ this.logger = endpointAppContext.logFactory.get(this.getTaskId());
+ taskManager.registerTaskDefinitions({
+ [TYPE]: {
+ title: 'Security Solution Endpoint Metadata Periodic Tasks',
+ timeout: TIMEOUT,
+ createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
+ return {
+ run: async () => {
+ return this.runTask(taskInstance, core);
+ },
+ cancel: async () => {},
+ };
+ },
+ },
+ });
+ }
+
+ public start = async ({ taskManager }: CheckMetadataTransformsTaskStartContract) => {
+ if (!taskManager) {
+ this.logger.error('missing required service during start');
+ return;
+ }
+
+ this.wasStarted = true;
+
+ try {
+ await taskManager.ensureScheduled({
+ id: this.getTaskId(),
+ taskType: TYPE,
+ scope: SCOPE,
+ schedule: {
+ interval: INTERVAL,
+ },
+ state: {
+ attempts: {},
+ },
+ params: { version: VERSION },
+ });
+ } catch (e) {
+ this.logger.debug(`Error scheduling task, received ${e.message}`);
+ }
+ };
+
+ private runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => {
+ // if task was not `.start()`'d yet, then exit
+ if (!this.wasStarted) {
+ this.logger.debug('[runTask()] Aborted. MetadataTask not started yet');
+ return;
+ }
+
+ // Check that this task is current
+ if (taskInstance.id !== this.getTaskId()) {
+ // old task, die
+ throwUnrecoverableError(new Error('Outdated task version'));
+ }
+
+ const [{ elasticsearch }] = await core.getStartServices();
+ const esClient = elasticsearch.client.asInternalUser;
+
+ let transformStatsResponse: ApiResponse;
+ try {
+ transformStatsResponse = await esClient?.transform.getTransformStats({
+ transform_id: METADATA_TRANSFORMS_PATTERN,
+ });
+ } catch (e) {
+ const err = wrapErrorIfNeeded(e);
+ const errMessage = `failed to get transform stats with error: ${err}`;
+ this.logger.error(errMessage);
+
+ return;
+ }
+
+ const { transforms } = transformStatsResponse.body;
+ if (!transforms.length) {
+ this.logger.info('no OLM metadata transforms found');
+ return;
+ }
+
+ let didAttemptRestart: boolean = false;
+ let highestAttempt: number = 0;
+ const attempts = { ...taskInstance.state.attempts };
+
+ for (const transform of transforms) {
+ const restartedTransform = await this.restartTransform(
+ esClient,
+ transform,
+ attempts[transform.id]
+ );
+ if (restartedTransform.didAttemptRestart) {
+ didAttemptRestart = true;
+ }
+ attempts[transform.id] = restartedTransform.attempts;
+ highestAttempt = Math.max(attempts[transform.id], highestAttempt);
+ }
+
+ // after a restart attempt run next check sooner with exponential backoff
+ let runAt: Date | undefined;
+ if (didAttemptRestart) {
+ const delay = BASE_NEXT_ATTEMPT_DELAY ** Math.max(highestAttempt, 1) * 60000;
+ runAt = new Date(new Date().getTime() + delay);
+ }
+
+ const nextState = { attempts };
+ const nextTask = runAt ? { state: nextState, runAt } : { state: nextState };
+ return nextTask;
+ };
+
+ private restartTransform = async (
+ esClient: ElasticsearchClient,
+ transform: TransformGetTransformStatsTransformStats,
+ currentAttempts: number = 0
+ ) => {
+ let attempts = currentAttempts;
+ let didAttemptRestart = false;
+
+ if (!WARNING_TRANSFORM_STATES.has(transform.state)) {
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ }
+
+ if (attempts > MAX_ATTEMPTS) {
+ this.logger.warn(
+ `transform ${transform.id} has failed to restart ${attempts} times. stopping auto restart attempts.`
+ );
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ }
+
+ try {
+ this.logger.info(`failed transform detected with id: ${transform.id}. attempting restart.`);
+ await esClient.transform.stopTransform({
+ transform_id: transform.id,
+ allow_no_match: true,
+ wait_for_completion: true,
+ force: true,
+ });
+ await esClient.transform.startTransform({
+ transform_id: transform.id,
+ });
+
+ // restart succeeded, reset attempt count
+ attempts = 0;
+ } catch (e) {
+ const err = wrapErrorIfNeeded(e);
+ const errMessage = `failed to restart transform ${transform.id} with error: ${err}`;
+ this.logger.error(errMessage);
+
+ // restart failed, increment attempt count
+ attempts = attempts + 1;
+ } finally {
+ didAttemptRestart = true;
+ }
+
+ return {
+ attempts,
+ didAttemptRestart,
+ };
+ };
+
+ private getTaskId = (): string => {
+ return `${TYPE}:${VERSION}`;
+ };
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts
new file mode 100644
index 0000000000000..6f5d6f5a4121b
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/metadata/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './check_metadata_transforms_task';
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 391beb3c40121..f69565cacceb5 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -59,6 +59,7 @@ import { initRoutes } from './routes';
import { isAlertExecutor } from './lib/detection_engine/signals/types';
import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type';
import { ManifestTask } from './endpoint/lib/artifacts';
+import { CheckMetadataTransformsTask } from './endpoint/lib/metadata';
import { initSavedObjects } from './saved_objects';
import { AppClientFactory } from './client';
import { createConfig, ConfigType } from './config';
@@ -157,6 +158,7 @@ export class Plugin implements IPlugin;
private telemetryUsageCounter?: UsageCounter;
@@ -363,6 +365,12 @@ export class Plugin implements IPlugin
Date: Mon, 4 Oct 2021 10:52:05 -0400
Subject: [PATCH 07/14] [Fleet] Fix how we get the default output in the Fleet
UI (#113620)
---
.../fleet_server_on_prem_instructions.tsx | 6 ++----
.../public/components/settings_flyout/index.tsx | 5 ++---
.../fleet/public/hooks/use_request/outputs.ts | 17 +++++++++++++++++
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
index 1d43f90b80def..a8cab77af447c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx
@@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { DownloadStep } from '../../../../components';
import {
useStartServices,
- useGetOutputs,
+ useDefaultOutput,
sendGenerateServiceToken,
usePlatform,
PLATFORM_OPTIONS,
@@ -242,7 +242,7 @@ export const FleetServerCommandStep = ({
};
export const useFleetServerInstructions = (policyId?: string) => {
- const outputsRequest = useGetOutputs();
+ const { output, refresh: refreshOutputs } = useDefaultOutput();
const { notifications } = useStartServices();
const [serviceToken, setServiceToken] = useState();
const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false);
@@ -250,9 +250,7 @@ export const useFleetServerInstructions = (policyId?: string) => {
const [deploymentMode, setDeploymentMode] = useState('production');
const { data: settings, resendRequest: refreshSettings } = useGetSettings();
const fleetServerHost = settings?.item.fleet_server_hosts?.[0];
- const output = outputsRequest.data?.items?.[0];
const esHost = output?.hosts?.[0];
- const refreshOutputs = outputsRequest.resendRequest;
const installCommand = useMemo((): string => {
if (!serviceToken || !esHost) {
diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
index e42733bbd2da0..9bedfca0d3bca 100644
--- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
@@ -36,7 +36,7 @@ import {
useGetSettings,
useInput,
sendPutSettings,
- useGetOutputs,
+ useDefaultOutput,
sendPutOutput,
} from '../../hooks';
import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common';
@@ -258,8 +258,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
- const outputsRequest = useGetOutputs();
- const output = outputsRequest.data?.items?.[0];
+ const { output } = useDefaultOutput();
const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose);
const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false);
diff --git a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
index 0fcaa262cf321..2d623da505c65 100644
--- a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { useMemo, useCallback } from 'react';
+
import { outputRoutesService } from '../../services';
import type { PutOutputRequest, GetOutputsResponse } from '../../types';
@@ -17,6 +19,21 @@ export function useGetOutputs() {
});
}
+export function useDefaultOutput() {
+ const outputsRequest = useGetOutputs();
+ const output = useMemo(() => {
+ return outputsRequest.data?.items.find((o) => o.is_default);
+ }, [outputsRequest.data]);
+
+ const refresh = useCallback(() => {
+ return outputsRequest.resendRequest();
+ }, [outputsRequest]);
+
+ return useMemo(() => {
+ return { output, refresh };
+ }, [output, refresh]);
+}
+
export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) {
return sendRequest({
method: 'put',
From c558f26dd13e7949edf19071d9eaa15d5dd05bad Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 4 Oct 2021 17:55:49 +0300
Subject: [PATCH 08/14] [TSVB] Update the series and metrics Ids that are
numbers to strings (#113619)
* [TSVB] Update the series and metrics Ids that are numbers to strings
* Minor changes
* Adds a unit test to TSVB plugin to test this case
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../timeseries/public/vis_state.test.ts | 126 ++++++++++++++++++
.../public/legacy/vis_update_state.js | 28 ++++
.../public/legacy/vis_update_state.test.js | 83 ++++++++++++
3 files changed, 237 insertions(+)
create mode 100644 src/plugins/vis_types/timeseries/public/vis_state.test.ts
diff --git a/src/plugins/vis_types/timeseries/public/vis_state.test.ts b/src/plugins/vis_types/timeseries/public/vis_state.test.ts
new file mode 100644
index 0000000000000..82e52a0493391
--- /dev/null
+++ b/src/plugins/vis_types/timeseries/public/vis_state.test.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { updateOldState } from '../../../visualizations/public';
+
+/**
+ * The reason we add this test is to ensure that `convertNumIdsToStringsForTSVB` of the updateOldState runs correctly
+ * for the TSVB vis state. As the `updateOldState` runs on the visualizations plugin. a change to our objects structure can
+ * result to forget this case.
+ * Just for reference the `convertNumIdsToStringsForTSVB` finds and converts the series and metrics ids that have only digits to strings
+ * by adding an x prefix. Number ids are never been generated from the editor, only programmatically.
+ * See https://github.com/elastic/kibana/issues/113601.
+ */
+describe('TimeseriesVisState', () => {
+ test('should format the TSVB visState correctly', () => {
+ const visState = {
+ title: 'test',
+ type: 'metrics',
+ aggs: [],
+ params: {
+ time_range_mode: 'entire_time_range',
+ id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497',
+ type: 'timeseries',
+ series: [
+ {
+ id: '1',
+ color: '#68BC00',
+ split_mode: 'terms',
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ metrics: [
+ {
+ id: '10',
+ type: 'count',
+ },
+ ],
+ separate_axis: 0,
+ axis_position: 'right',
+ formatter: 'default',
+ chart_type: 'line',
+ line_width: 1,
+ point_size: 1,
+ fill: 0.5,
+ stacked: 'none',
+ terms_field: 'Cancelled',
+ },
+ ],
+ time_field: '',
+ use_kibana_indexes: true,
+ interval: '',
+ axis_position: 'left',
+ axis_formatter: 'number',
+ axis_scale: 'normal',
+ show_legend: 1,
+ truncate_legend: 1,
+ max_lines_legend: 1,
+ show_grid: 1,
+ tooltip_mode: 'show_all',
+ drop_last_bucket: 0,
+ isModelInvalid: false,
+ index_pattern: {
+ id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b',
+ },
+ },
+ };
+ const newVisState = updateOldState(visState);
+ expect(newVisState).toEqual({
+ aggs: [],
+ params: {
+ axis_formatter: 'number',
+ axis_position: 'left',
+ axis_scale: 'normal',
+ drop_last_bucket: 0,
+ id: '0ecc58b1-30ba-43b9-aa3f-9ac32b482497',
+ index_pattern: {
+ id: '665cd2c0-21d6-11ec-b42f-f7077c64d21b',
+ },
+ interval: '',
+ isModelInvalid: false,
+ max_lines_legend: 1,
+ series: [
+ {
+ axis_position: 'right',
+ chart_type: 'line',
+ color: '#68BC00',
+ fill: 0.5,
+ formatter: 'default',
+ id: 'x1',
+ line_width: 1,
+ metrics: [
+ {
+ id: 'x10',
+ type: 'count',
+ },
+ ],
+ palette: {
+ name: 'default',
+ type: 'palette',
+ },
+ point_size: 1,
+ separate_axis: 0,
+ split_mode: 'terms',
+ stacked: 'none',
+ terms_field: 'Cancelled',
+ },
+ ],
+ show_grid: 1,
+ show_legend: 1,
+ time_field: '',
+ time_range_mode: 'entire_time_range',
+ tooltip_mode: 'show_all',
+ truncate_legend: 1,
+ type: 'timeseries',
+ use_kibana_indexes: true,
+ },
+ title: 'test',
+ type: 'metrics',
+ });
+ });
+});
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js
index d0ebe00b1a6f0..db6a9f2beb776 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.js
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.js
@@ -136,6 +136,30 @@ function convertSeriesParams(visState) {
];
}
+/**
+ * This function is responsible for updating old TSVB visStates.
+ * Specifically, it identifies if the series and metrics ids are numbers
+ * and convert them to string with an x prefix. Number ids are never been generated
+ * from the editor, only programmatically. See https://github.com/elastic/kibana/issues/113601.
+ */
+function convertNumIdsToStringsForTSVB(visState) {
+ if (visState.params.series) {
+ visState.params.series.forEach((s) => {
+ const seriesId = s.id;
+ const metrics = s.metrics;
+ if (!isNaN(seriesId)) {
+ s.id = `x${seriesId}`;
+ }
+ metrics?.forEach((m) => {
+ const metricId = m.id;
+ if (!isNaN(metricId)) {
+ m.id = `x${metricId}`;
+ }
+ });
+ });
+ }
+}
+
/**
* This function is responsible for updating old visStates - the actual saved object
* object - into the format, that will be required by the current Kibana version.
@@ -155,6 +179,10 @@ export const updateOldState = (visState) => {
convertSeriesParams(newState);
}
+ if (visState.params && visState.type === 'metrics') {
+ convertNumIdsToStringsForTSVB(newState);
+ }
+
if (visState.type === 'gauge' && visState.fontSize) {
delete newState.fontSize;
set(newState, 'gauge.style.fontSize', visState.fontSize);
diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.test.js b/src/plugins/visualizations/public/legacy/vis_update_state.test.js
index 3b0d732df2d1a..a7c2df506d313 100644
--- a/src/plugins/visualizations/public/legacy/vis_update_state.test.js
+++ b/src/plugins/visualizations/public/legacy/vis_update_state.test.js
@@ -93,4 +93,87 @@ describe('updateOldState', () => {
expect(state.params.showMeticsAtAllLevels).toBe(undefined);
});
});
+
+ describe('TSVB ids conversion', () => {
+ it('should update the seriesId from number to string with x prefix', () => {
+ const oldState = {
+ type: 'metrics',
+ params: {
+ series: [
+ {
+ id: '10',
+ },
+ {
+ id: 'ABC',
+ },
+ {
+ id: 1,
+ },
+ ],
+ },
+ };
+ const state = updateOldState(oldState);
+ expect(state.params.series).toEqual([
+ {
+ id: 'x10',
+ },
+ {
+ id: 'ABC',
+ },
+ {
+ id: 'x1',
+ },
+ ]);
+ });
+ it('should update the metrics ids from number to string with x prefix', () => {
+ const oldState = {
+ type: 'metrics',
+ params: {
+ series: [
+ {
+ id: '10',
+ metrics: [
+ {
+ id: '1000',
+ },
+ {
+ id: '74a66e70-ac44-11eb-9865-6b616e971cf8',
+ },
+ ],
+ },
+ {
+ id: 'ABC',
+ metrics: [
+ {
+ id: null,
+ },
+ ],
+ },
+ ],
+ },
+ };
+ const state = updateOldState(oldState);
+ expect(state.params.series).toEqual([
+ {
+ id: 'x10',
+ metrics: [
+ {
+ id: 'x1000',
+ },
+ {
+ id: '74a66e70-ac44-11eb-9865-6b616e971cf8',
+ },
+ ],
+ },
+ {
+ id: 'ABC',
+ metrics: [
+ {
+ id: 'xnull',
+ },
+ ],
+ },
+ ]);
+ });
+ });
});
From 252278a433850ed7775debcd1c74b8e63ef286ba Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Mon, 4 Oct 2021 10:57:32 -0400
Subject: [PATCH 09/14] [buildkite] Fix packer cache issues (#113769)
---
.buildkite/scripts/common/env.sh | 4 ++--
.buildkite/scripts/packer_cache.sh | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh
index 89121581c75d1..ac80a66d33fa0 100755
--- a/.buildkite/scripts/common/env.sh
+++ b/.buildkite/scripts/common/env.sh
@@ -56,8 +56,8 @@ else
fi
# These are for backwards-compatibility
-export GIT_COMMIT="$BUILDKITE_COMMIT"
-export GIT_BRANCH="$BUILDKITE_BRANCH"
+export GIT_COMMIT="${BUILDKITE_COMMIT:-}"
+export GIT_BRANCH="${BUILDKITE_BRANCH:-}"
export FLEET_PACKAGE_REGISTRY_PORT=6104
export TEST_CORS_SERVER_PORT=6105
diff --git a/.buildkite/scripts/packer_cache.sh b/.buildkite/scripts/packer_cache.sh
index 45d3dc439ff4d..617ea79c827b0 100755
--- a/.buildkite/scripts/packer_cache.sh
+++ b/.buildkite/scripts/packer_cache.sh
@@ -2,6 +2,7 @@
set -euo pipefail
+source .buildkite/scripts/common/util.sh
source .buildkite/scripts/common/env.sh
source .buildkite/scripts/common/setup_node.sh
From 3d7e04b799b11407e2689cde532a7f82297b4874 Mon Sep 17 00:00:00 2001
From: Domenico Andreoli
Date: Mon, 4 Oct 2021 17:04:24 +0200
Subject: [PATCH 10/14] [Security] Add EQL rule test in CCS config (#112852)
---
.../event_correlation_rule.spec.ts | 55 ++
.../security_solution/cypress/objects/rule.ts | 23 +
.../cypress/tasks/api_calls/rules.ts | 31 +-
.../es_archives/linux_process/data.json | 135 +++
.../es_archives/linux_process/mappings.json | 935 ++++++++++++++++++
5 files changed, 1178 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts
create mode 100644 x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
create mode 100644 x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts
new file mode 100644
index 0000000000000..c20e6cf6b6370
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_rules/event_correlation_rule.spec.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { esArchiverCCSLoad } from '../../tasks/es_archiver';
+import { getCCSEqlRule } from '../../objects/rule';
+
+import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts';
+
+import {
+ filterByCustomRules,
+ goToRuleDetails,
+ waitForRulesTableToBeLoaded,
+} from '../../tasks/alerts_detection_rules';
+import { createSignalsIndex, createEventCorrelationRule } from '../../tasks/api_calls/rules';
+import { cleanKibana } from '../../tasks/common';
+import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule';
+import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
+
+import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
+
+describe('Detection rules', function () {
+ const expectedNumberOfAlerts = '1 alert';
+
+ beforeEach('Reset signals index', function () {
+ cleanKibana();
+ createSignalsIndex();
+ });
+
+ it('EQL rule on remote indices generates alerts', function () {
+ esArchiverCCSLoad('linux_process');
+ this.rule = getCCSEqlRule();
+ createEventCorrelationRule(this.rule);
+
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
+ waitForRulesTableToBeLoaded();
+ filterByCustomRules();
+ goToRuleDetails();
+ waitForTheRuleToBeExecuted();
+ waitForAlertsToPopulate();
+
+ cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts);
+ cy.get(ALERT_DATA_GRID)
+ .invoke('text')
+ .then((text) => {
+ cy.log('ALERT_DATA_GRID', text);
+ expect(text).contains(this.rule.name);
+ expect(text).contains(this.rule.severity.toLowerCase());
+ expect(text).contains(this.rule.riskScore);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 173bfa524e66e..db76bfc3cf4df 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -72,6 +72,10 @@ export interface OverrideRule extends CustomRule {
timestampOverride: string;
}
+export interface EventCorrelationRule extends CustomRule {
+ language: string;
+}
+
export interface ThreatIndicatorRule extends CustomRule {
indicatorIndexPattern: string[];
indicatorMappingField: string;
@@ -326,6 +330,25 @@ export const getEqlRule = (): CustomRule => ({
maxSignals: 100,
});
+export const getCCSEqlRule = (): EventCorrelationRule => ({
+ customQuery: 'any where process.name == "run-parts"',
+ name: 'New EQL Rule',
+ index: [`${ccsRemoteName}:run-parts`],
+ description: 'New EQL rule description.',
+ severity: 'High',
+ riskScore: '17',
+ tags: ['test', 'newRule'],
+ referenceUrls: ['http://example.com/', 'https://example.com/'],
+ falsePositivesExamples: ['False1', 'False2'],
+ mitre: [getMitre1(), getMitre2()],
+ note: '# test markdown',
+ runsEvery: getRunsEvery(),
+ lookBack: getLookBack(),
+ timeline: getTimeline(),
+ maxSignals: 100,
+ language: 'eql',
+});
+
export const getEqlSequenceRule = (): CustomRule => ({
customQuery:
'sequence with maxspan=30s\
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
index 33bd8a06b9985..130467cde053d 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { CustomRule, ThreatIndicatorRule } from '../../objects/rule';
+import { CustomRule, EventCorrelationRule, ThreatIndicatorRule } from '../../objects/rule';
export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') =>
cy.request({
@@ -29,6 +29,27 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte
failOnStatusCode: false,
});
+export const createEventCorrelationRule = (rule: EventCorrelationRule, ruleId = 'rule_testing') =>
+ cy.request({
+ method: 'POST',
+ url: 'api/detection_engine/rules',
+ body: {
+ rule_id: ruleId,
+ risk_score: parseInt(rule.riskScore, 10),
+ description: rule.description,
+ interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
+ from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
+ name: rule.name,
+ severity: rule.severity.toLocaleLowerCase(),
+ type: 'eql',
+ index: rule.index,
+ query: rule.customQuery,
+ language: 'eql',
+ enabled: true,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
+
export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') =>
cy.request({
method: 'POST',
@@ -107,6 +128,14 @@ export const deleteCustomRule = (ruleId = '1') => {
});
};
+export const createSignalsIndex = () => {
+ cy.request({
+ method: 'POST',
+ url: 'api/detection_engine/index',
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
+};
+
export const removeSignalsIndex = () => {
cy.request({ url: '/api/detection_engine/index', failOnStatusCode: false }).then((response) => {
if (response.status === 200) {
diff --git a/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
new file mode 100644
index 0000000000000..ed29f3fe3e4e1
--- /dev/null
+++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/data.json
@@ -0,0 +1,135 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "qxnqn3sBBf0WZxoXk7tg",
+ "index": "run-parts",
+ "source": {
+ "@timestamp": "2021-09-01T05:52:29.9451497Z",
+ "agent": {
+ "id": "cda623db-f791-4869-a63d-5b8352dfaa56",
+ "type": "endpoint",
+ "version": "7.14.0"
+ },
+ "data_stream": {
+ "dataset": "endpoint.events.process",
+ "namespace": "default",
+ "type": "logs"
+ },
+ "ecs": {
+ "version": "1.6.0"
+ },
+ "elastic": {
+ "agent": {
+ "id": "cda623db-f791-4869-a63d-5b8352dfaa56"
+ }
+ },
+ "event": {
+ "action": "exec",
+ "agent_id_status": "verified",
+ "category": [
+ "process"
+ ],
+ "created": "2021-09-01T05:52:29.9451497Z",
+ "dataset": "endpoint.events.process",
+ "id": "MGwI0NpfzFKkX6gW+++++CVd",
+ "ingested": "2021-09-01T05:52:35.677424686Z",
+ "kind": "event",
+ "module": "endpoint",
+ "sequence": 3523,
+ "type": [
+ "start"
+ ]
+ },
+ "group": {
+ "Ext": {
+ "real": {
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "id": 0,
+ "name": "root"
+ },
+ "host": {
+ "architecture": "x86_64",
+ "hostname": "localhost",
+ "id": "f5c59e5f0c963f828782bc413653d324",
+ "ip": [
+ "127.0.0.1",
+ "::1"
+ ],
+ "mac": [
+ "00:16:3e:10:96:79"
+ ],
+ "name": "localhost",
+ "os": {
+ "Ext": {
+ "variant": "Debian"
+ },
+ "family": "debian",
+ "full": "Debian 10",
+ "kernel": "4.19.0-17-amd64 #1 SMP Debian 4.19.194-3 (2021-07-18)",
+ "name": "Linux",
+ "platform": "debian",
+ "version": "10"
+ }
+ },
+ "message": "Endpoint process event",
+ "process": {
+ "Ext": {
+ "ancestry": [
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTMwNzYyMTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI4OTI0ODAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDktMTMyNzQ5NDkxNDkuOTI3NDgwMzAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTI3ODA5NTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNDEtMTMyNzQ5NDkxNDYuNTIzNzEzOTAw",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNzgyMjQwMDA=",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTczOC0xMzI3NDk0ODg3OS4yNTQ1MTUzMDA=",
+ "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEtMTMyNzQ5NDg4NjkuMA=="
+ ]
+ },
+ "args": [
+ "run-parts",
+ "--lsbsysinit",
+ "/etc/update-motd.d"
+ ],
+ "args_count": 3,
+ "command_line": "run-parts --lsbsysinit /etc/update-motd.d",
+ "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTQ1MTQ5NzAw",
+ "executable": "/usr/bin/run-parts",
+ "hash": {
+ "md5": "c83b0578484bf5267893d795b55928bd",
+ "sha1": "46b6e74e28e5daf69c1dd0f18a8e911ae2922dda",
+ "sha256": "3346b4d47c637a8c02cb6865eee42d2a5aa9c4e46c6371a9143621348d27420f"
+ },
+ "name": "run-parts",
+ "parent": {
+ "args": [
+ "sh",
+ "-c",
+ "/usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new"
+ ],
+ "args_count": 0,
+ "command_line": "sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new",
+ "entity_id": "Y2ZhNjk5ZGItYzI5My00ODY5LWI2OGMtNWI4MzE0ZGZhYTU2LTEzNTAtMTMyNzQ5NDkxNDkuOTM2Njk1MDAw",
+ "executable": "/",
+ "name": "",
+ "pid": 1349
+ },
+ "pid": 1350
+ },
+ "user": {
+ "Ext": {
+ "real": {
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "id": 0,
+ "name": "root"
+ }
+ },
+ "type": "_doc"
+ }
+}
diff --git a/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
new file mode 100644
index 0000000000000..d244defbdab0b
--- /dev/null
+++ b/x-pack/test/security_solution_cypress/es_archives/linux_process/mappings.json
@@ -0,0 +1,935 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ },
+ "index": "run-parts",
+ "mappings": {
+ "_data_stream_timestamp": {
+ "enabled": true
+ },
+ "_meta": {
+ "managed": true,
+ "managed_by": "ingest-manager",
+ "package": {
+ "name": "endpoint"
+ }
+ },
+ "date_detection": false,
+ "dynamic": "false",
+ "dynamic_templates": [
+ {
+ "strings_as_keyword": {
+ "mapping": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "match_mapping_type": "string"
+ }
+ }
+ ],
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "agent": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "data_stream": {
+ "properties": {
+ "dataset": {
+ "type": "constant_keyword",
+ "value": "endpoint.events.process"
+ },
+ "namespace": {
+ "type": "constant_keyword",
+ "value": "default"
+ },
+ "type": {
+ "type": "constant_keyword",
+ "value": "logs"
+ }
+ }
+ },
+ "destination": {
+ "properties": {
+ "geo": {
+ "properties": {
+ "city_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "continent_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "location": {
+ "type": "geo_point"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "ecs": {
+ "properties": {
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "event": {
+ "properties": {
+ "action": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "agent_id_status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "category": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "created": {
+ "type": "date"
+ },
+ "dataset": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "hash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "ingested": {
+ "type": "date"
+ },
+ "kind": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "module": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "outcome": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "provider": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sequence": {
+ "type": "long"
+ },
+ "severity": {
+ "type": "long"
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "group": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "host": {
+ "properties": {
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "hostname": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "ip": {
+ "type": "ip"
+ },
+ "mac": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "os": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "variant": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "family": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "full": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "kernel": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "platform": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ }
+ }
+ },
+ "message": {
+ "type": "text"
+ },
+ "package": {
+ "properties": {
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "process": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "ancestry": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "authentication_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ },
+ "type": "nested"
+ },
+ "defense_evasions": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "dll": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "mapped_address": {
+ "type": "unsigned_long"
+ },
+ "mapped_size": {
+ "type": "unsigned_long"
+ }
+ }
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "path": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "protection": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "session": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "token": {
+ "properties": {
+ "elevation": {
+ "type": "boolean"
+ },
+ "elevation_level": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "elevation_type": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "integrity_level_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "args": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "args_count": {
+ "type": "long"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ }
+ },
+ "command_line": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "entity_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "executable": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "exit_code": {
+ "type": "long"
+ },
+ "hash": {
+ "properties": {
+ "md5": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha1": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha256": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha512": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "parent": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "architecture": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ },
+ "type": "nested"
+ },
+ "protection": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "real": {
+ "properties": {
+ "pid": {
+ "type": "long"
+ }
+ }
+ },
+ "user": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "args": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "args_count": {
+ "type": "long"
+ },
+ "code_signature": {
+ "properties": {
+ "exists": {
+ "type": "boolean"
+ },
+ "status": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "subject_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "trusted": {
+ "type": "boolean"
+ },
+ "valid": {
+ "type": "boolean"
+ }
+ }
+ },
+ "command_line": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "entity_id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "executable": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "exit_code": {
+ "type": "long"
+ },
+ "hash": {
+ "properties": {
+ "md5": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha1": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha256": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "sha512": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "name": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "pe": {
+ "properties": {
+ "company": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "description": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "file_version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "imphash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "original_file_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "product": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pgid": {
+ "type": "long"
+ },
+ "pid": {
+ "type": "long"
+ },
+ "ppid": {
+ "type": "long"
+ },
+ "thread": {
+ "properties": {
+ "id": {
+ "type": "long"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "title": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ },
+ "working_directory": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pe": {
+ "properties": {
+ "company": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "description": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "file_version": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "imphash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "original_file_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "product": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "pgid": {
+ "type": "long"
+ },
+ "pid": {
+ "type": "long"
+ },
+ "ppid": {
+ "type": "long"
+ },
+ "thread": {
+ "properties": {
+ "id": {
+ "type": "long"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "title": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "uptime": {
+ "type": "long"
+ },
+ "working_directory": {
+ "fields": {
+ "caseless": {
+ "ignore_above": 1024,
+ "normalizer": "lowercase",
+ "type": "keyword"
+ },
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "source": {
+ "properties": {
+ "geo": {
+ "properties": {
+ "city_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "continent_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "country_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "location": {
+ "type": "geo_point"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_iso_code": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "region_name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "user": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "email": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "full_name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "group": {
+ "properties": {
+ "Ext": {
+ "properties": {
+ "real": {
+ "properties": {
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "domain": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ },
+ "hash": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "id": {
+ "ignore_above": 1024,
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "ignore_above": 1024,
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_replicas": "0",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
From 10ca6f42d6910352bb592ccc1d6ee8faf714488d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 4 Oct 2021 11:11:08 -0400
Subject: [PATCH 11/14] [APM] Show APM Server stand-alone mode in Kibana
Upgrade Assistant (cloud-only) (#113567)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../server/deprecations/deprecations.test.ts | 70 ++++++++++++++++++
.../plugins/apm/server/deprecations/index.ts | 74 +++++++++++++++++++
x-pack/plugins/apm/server/plugin.ts | 7 ++
3 files changed, 151 insertions(+)
create mode 100644 x-pack/plugins/apm/server/deprecations/deprecations.test.ts
create mode 100644 x-pack/plugins/apm/server/deprecations/index.ts
diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts
new file mode 100644
index 0000000000000..d706146faf212
--- /dev/null
+++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { GetDeprecationsContext } from '../../../../../src/core/server';
+import { CloudSetup } from '../../../cloud/server';
+import { getDeprecations } from './';
+import { APMRouteHandlerResources } from '../';
+import { AgentPolicy } from '../../../fleet/common';
+
+const deprecationContext = {
+ esClient: {},
+ savedObjectsClient: {},
+} as GetDeprecationsContext;
+
+describe('getDeprecations', () => {
+ describe('when fleet is disabled', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({});
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+
+ describe('when running on cloud with legacy apm-server', () => {
+ it('returns deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({
+ agentPolicyService: { get: () => undefined },
+ }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).not.toEqual([]);
+ });
+ });
+
+ describe('when running on cloud with fleet', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({
+ agentPolicyService: { get: () => ({ id: 'foo' } as AgentPolicy) },
+ }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+
+ describe('when running on prem', () => {
+ it('returns no deprecations', async () => {
+ const deprecationsCallback = getDeprecations({
+ cloudSetup: { isCloudEnabled: false } as unknown as CloudSetup,
+ fleet: {
+ start: () => ({ agentPolicyService: { get: () => undefined } }),
+ } as unknown as APMRouteHandlerResources['plugins']['fleet'],
+ });
+ const deprecations = await deprecationsCallback(deprecationContext);
+ expect(deprecations).toEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts
new file mode 100644
index 0000000000000..b592a2bf13268
--- /dev/null
+++ b/x-pack/plugins/apm/server/deprecations/index.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { GetDeprecationsContext, DeprecationsDetails } from 'src/core/server';
+import { i18n } from '@kbn/i18n';
+import { isEmpty } from 'lodash';
+import { CloudSetup } from '../../../cloud/server';
+import { getCloudAgentPolicy } from '../lib/fleet/get_cloud_apm_package_policy';
+import { APMRouteHandlerResources } from '../';
+
+export function getDeprecations({
+ cloudSetup,
+ fleet,
+}: {
+ cloudSetup?: CloudSetup;
+ fleet?: APMRouteHandlerResources['plugins']['fleet'];
+}) {
+ return async ({
+ savedObjectsClient,
+ }: GetDeprecationsContext): Promise => {
+ const deprecations: DeprecationsDetails[] = [];
+ if (!fleet) {
+ return deprecations;
+ }
+
+ const fleetPluginStart = await fleet.start();
+ const cloudAgentPolicy = await getCloudAgentPolicy({
+ fleetPluginStart,
+ savedObjectsClient,
+ });
+
+ const isCloudEnabled = !!cloudSetup?.isCloudEnabled;
+
+ const hasCloudAgentPolicy = !isEmpty(cloudAgentPolicy);
+
+ if (isCloudEnabled && !hasCloudAgentPolicy) {
+ deprecations.push({
+ title: i18n.translate('xpack.apm.deprecations.legacyModeTitle', {
+ defaultMessage: 'APM Server running in legacy mode',
+ }),
+ message: i18n.translate('xpack.apm.deprecations.message', {
+ defaultMessage:
+ 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.',
+ }),
+ documentationUrl:
+ 'https://www.elastic.co/guide/en/apm/server/current/apm-integration.html',
+ level: 'warning',
+ correctiveActions: {
+ manualSteps: [
+ i18n.translate('xpack.apm.deprecations.steps.apm', {
+ defaultMessage: 'Navigate to Observability/APM',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.settings', {
+ defaultMessage: 'Click on "Settings"',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.schema', {
+ defaultMessage: 'Select "Schema" tab',
+ }),
+ i18n.translate('xpack.apm.deprecations.steps.switch', {
+ defaultMessage:
+ 'Click "Switch to data streams". You will be guided through the process',
+ }),
+ ],
+ },
+ });
+ }
+
+ return deprecations;
+ };
+}
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index 56185d846562f..2296227de2a33 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -51,6 +51,7 @@ import {
TRANSACTION_TYPE,
} from '../common/elasticsearch_fieldnames';
import { tutorialProvider } from './tutorial';
+import { getDeprecations } from './deprecations';
export class APMPlugin
implements
@@ -222,6 +223,12 @@ export class APMPlugin
);
})();
});
+ core.deprecations.registerDeprecations({
+ getDeprecations: getDeprecations({
+ cloudSetup: plugins.cloud,
+ fleet: resourcePlugins.fleet,
+ }),
+ });
return {
config$: mergedConfig$,
From 4693c3812e5decfefcb6bc8835702e141a5f3ed6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Mon, 4 Oct 2021 16:19:03 +0100
Subject: [PATCH 12/14] =?UTF-8?q?[console]=C2=A0Deprecate=20"proxyFilter"?=
=?UTF-8?q?=20and=20"proxyConfig"=20on=208.x=20(#113555)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/plugins/console/common/constants/index.ts | 9 ++
.../console/common/constants/plugin.ts | 9 ++
src/plugins/console/kibana.json | 4 +-
src/plugins/console/server/config.ts | 87 ++++++++++-----
src/plugins/console/server/index.ts | 9 +-
src/plugins/console/server/plugin.ts | 22 +++-
.../api/console/proxy/create_handler.ts | 44 +++++---
.../server/routes/api/console/proxy/mocks.ts | 32 ++++--
.../routes/api/console/proxy/params.test.ts | 104 ++++++++++--------
src/plugins/console/server/routes/index.ts | 6 +-
10 files changed, 214 insertions(+), 112 deletions(-)
create mode 100644 src/plugins/console/common/constants/index.ts
create mode 100644 src/plugins/console/common/constants/plugin.ts
diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts
new file mode 100644
index 0000000000000..0a8dac9b7fff3
--- /dev/null
+++ b/src/plugins/console/common/constants/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { MAJOR_VERSION } from './plugin';
diff --git a/src/plugins/console/common/constants/plugin.ts b/src/plugins/console/common/constants/plugin.ts
new file mode 100644
index 0000000000000..cd301ec296395
--- /dev/null
+++ b/src/plugins/console/common/constants/plugin.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const MAJOR_VERSION = '8.0.0';
diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json
index 69c7176ff6a47..e2345514d76b9 100644
--- a/src/plugins/console/kibana.json
+++ b/src/plugins/console/kibana.json
@@ -1,12 +1,14 @@
{
"id": "console",
- "version": "kibana",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
"server": true,
"ui": true,
"owner": {
"name": "Stack Management",
"githubTeam": "kibana-stack-management"
},
+ "configPath": ["console"],
"requiredPlugins": ["devTools", "share"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"]
diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts
index 4e42e3c21d2ad..6d667fed081e8 100644
--- a/src/plugins/console/server/config.ts
+++ b/src/plugins/console/server/config.ts
@@ -6,37 +6,70 @@
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
import { schema, TypeOf } from '@kbn/config-schema';
+import { PluginConfigDescriptor } from 'kibana/server';
-export type ConfigType = TypeOf;
+import { MAJOR_VERSION } from '../common/constants';
-export const config = schema.object(
- {
- enabled: schema.boolean({ defaultValue: true }),
- proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
- ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
- proxyConfig: schema.arrayOf(
- schema.object({
- match: schema.object({
- protocol: schema.string({ defaultValue: '*' }),
- host: schema.string({ defaultValue: '*' }),
- port: schema.string({ defaultValue: '*' }),
- path: schema.string({ defaultValue: '*' }),
- }),
-
- timeout: schema.number(),
- ssl: schema.object(
- {
- verify: schema.boolean(),
- ca: schema.arrayOf(schema.string()),
- cert: schema.string(),
- key: schema.string(),
- },
- { defaultValue: undefined }
- ),
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
+const baseSettings = {
+ enabled: schema.boolean({ defaultValue: true }),
+ ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
+};
+
+// Settings only available in 7.x
+const deprecatedSettings = {
+ proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
+ proxyConfig: schema.arrayOf(
+ schema.object({
+ match: schema.object({
+ protocol: schema.string({ defaultValue: '*' }),
+ host: schema.string({ defaultValue: '*' }),
+ port: schema.string({ defaultValue: '*' }),
+ path: schema.string({ defaultValue: '*' }),
}),
- { defaultValue: [] }
- ),
+
+ timeout: schema.number(),
+ ssl: schema.object(
+ {
+ verify: schema.boolean(),
+ ca: schema.arrayOf(schema.string()),
+ cert: schema.string(),
+ key: schema.string(),
+ },
+ { defaultValue: undefined }
+ ),
+ }),
+ { defaultValue: [] }
+ ),
+};
+
+const configSchema = schema.object(
+ {
+ ...baseSettings,
},
{ defaultValue: undefined }
);
+
+const configSchema7x = schema.object(
+ {
+ ...baseSettings,
+ ...deprecatedSettings,
+ },
+ { defaultValue: undefined }
+);
+
+export type ConfigType = TypeOf;
+export type ConfigType7x = TypeOf;
+
+export const config: PluginConfigDescriptor = {
+ schema: kibanaVersion.major < 8 ? configSchema7x : configSchema,
+ deprecations: ({ deprecate, unused }) => [
+ deprecate('enabled', '8.0.0'),
+ deprecate('proxyFilter', '8.0.0'),
+ deprecate('proxyConfig', '8.0.0'),
+ unused('ssl'),
+ ],
+};
diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts
index cd05652c62838..6ae518f5dc796 100644
--- a/src/plugins/console/server/index.ts
+++ b/src/plugins/console/server/index.ts
@@ -6,16 +6,11 @@
* Side Public License, v 1.
*/
-import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
+import { PluginInitializerContext } from 'kibana/server';
-import { ConfigType, config as configSchema } from './config';
import { ConsoleServerPlugin } from './plugin';
export { ConsoleSetup, ConsoleStart } from './types';
+export { config } from './config';
export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx);
-
-export const config: PluginConfigDescriptor = {
- deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')],
- schema: configSchema,
-};
diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts
index a5f1ca6107600..613337b286fbf 100644
--- a/src/plugins/console/server/plugin.ts
+++ b/src/plugins/console/server/plugin.ts
@@ -7,10 +7,11 @@
*/
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
+import { SemVer } from 'semver';
import { ProxyConfigCollection } from './lib';
import { SpecDefinitionsService, EsLegacyConfigService } from './services';
-import { ConfigType } from './config';
+import { ConfigType, ConfigType7x } from './config';
import { registerRoutes } from './routes';
@@ -23,7 +24,7 @@ export class ConsoleServerPlugin implements Plugin {
esLegacyConfigService = new EsLegacyConfigService();
- constructor(private readonly ctx: PluginInitializerContext) {
+ constructor(private readonly ctx: PluginInitializerContext) {
this.log = this.ctx.logger.get();
}
@@ -34,10 +35,17 @@ export class ConsoleServerPlugin implements Plugin {
save: true,
},
}));
-
+ const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version);
const config = this.ctx.config.get();
const globalConfig = this.ctx.config.legacy.get();
- const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str));
+
+ let pathFilters: RegExp[] | undefined;
+ let proxyConfigCollection: ProxyConfigCollection | undefined;
+ if (kibanaVersion.major < 8) {
+ // "pathFilters" and "proxyConfig" are only used in 7.x
+ pathFilters = (config as ConfigType7x).proxyFilter.map((str: string) => new RegExp(str));
+ proxyConfigCollection = new ProxyConfigCollection((config as ConfigType7x).proxyConfig);
+ }
this.esLegacyConfigService.setup(elasticsearch.legacy.config$);
@@ -51,7 +59,6 @@ export class ConsoleServerPlugin implements Plugin {
specDefinitionService: this.specDefinitionsService,
},
proxy: {
- proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig),
readLegacyESConfig: async (): Promise => {
const legacyConfig = await this.esLegacyConfigService.readConfig();
return {
@@ -59,8 +66,11 @@ export class ConsoleServerPlugin implements Plugin {
...legacyConfig,
};
},
- pathFilters: proxyPathFilters,
+ // Deprecated settings (only used in 7.x):
+ proxyConfigCollection,
+ pathFilters,
},
+ kibanaVersion,
});
return {
diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index 8ca5720d559ce..9ece066246e4a 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -9,6 +9,7 @@
import { Agent, IncomingMessage } from 'http';
import * as url from 'url';
import { pick, trimStart, trimEnd } from 'lodash';
+import { SemVer } from 'semver';
import { KibanaRequest, RequestHandler } from 'kibana/server';
@@ -58,17 +59,22 @@ function filterHeaders(originalHeaders: object, headersToKeep: string[]): object
function getRequestConfig(
headers: object,
esConfig: ESConfigForProxy,
- proxyConfigCollection: ProxyConfigCollection,
- uri: string
+ uri: string,
+ kibanaVersion: SemVer,
+ proxyConfigCollection?: ProxyConfigCollection
): { agent: Agent; timeout: number; headers: object; rejectUnauthorized?: boolean } {
const filteredHeaders = filterHeaders(headers, esConfig.requestHeadersWhitelist);
const newHeaders = setHeaders(filteredHeaders, esConfig.customHeaders);
- if (proxyConfigCollection.hasConfig()) {
- return {
- ...proxyConfigCollection.configForUri(uri),
- headers: newHeaders,
- };
+ if (kibanaVersion.major < 8) {
+ // In 7.x we still support the proxyConfig setting defined in kibana.yml
+ // From 8.x we don't support it anymore so we don't try to read it here.
+ if (proxyConfigCollection!.hasConfig()) {
+ return {
+ ...proxyConfigCollection!.configForUri(uri),
+ headers: newHeaders,
+ };
+ }
}
return {
@@ -106,18 +112,23 @@ export const createHandler =
({
log,
proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection },
+ kibanaVersion,
}: RouteDependencies): RequestHandler =>
async (ctx, request, response) => {
const { body, query } = request;
const { path, method } = query;
- if (!pathFilters.some((re) => re.test(path))) {
- return response.forbidden({
- body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
- headers: {
- 'Content-Type': 'text/plain',
- },
- });
+ if (kibanaVersion.major < 8) {
+ // The "console.proxyFilter" setting in kibana.yaml has been deprecated in 8.x
+ // We only read it on the 7.x branch
+ if (!pathFilters!.some((re) => re.test(path))) {
+ return response.forbidden({
+ body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ });
+ }
}
const legacyConfig = await readLegacyESConfig();
@@ -134,8 +145,9 @@ export const createHandler =
const { timeout, agent, headers, rejectUnauthorized } = getRequestConfig(
request.headers,
legacyConfig,
- proxyConfigCollection,
- uri.toString()
+ uri.toString(),
+ kibanaVersion,
+ proxyConfigCollection
);
const requestHeaders = {
diff --git a/src/plugins/console/server/routes/api/console/proxy/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/mocks.ts
index 010e35ab505af..d06ca90adf556 100644
--- a/src/plugins/console/server/routes/api/console/proxy/mocks.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/mocks.ts
@@ -5,28 +5,41 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
jest.mock('../../../../lib/proxy_request', () => ({
proxyRequest: jest.fn(),
}));
import { duration } from 'moment';
+import { MAJOR_VERSION } from '../../../../../common/constants';
import { ProxyConfigCollection } from '../../../../lib';
import { RouteDependencies, ProxyDependencies } from '../../../../routes';
import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services';
import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks';
-const defaultProxyValue = Object.freeze({
- readLegacyESConfig: async () => ({
- requestTimeout: duration(30000),
- customHeaders: {},
- requestHeadersWhitelist: [],
- hosts: ['http://localhost:9200'],
- }),
- pathFilters: [/.*/],
- proxyConfigCollection: new ProxyConfigCollection([]),
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
+const readLegacyESConfig = async () => ({
+ requestTimeout: duration(30000),
+ customHeaders: {},
+ requestHeadersWhitelist: [],
+ hosts: ['http://localhost:9200'],
+});
+
+let defaultProxyValue = Object.freeze({
+ readLegacyESConfig,
});
+if (kibanaVersion.major < 8) {
+ // In 7.x we still support the "pathFilter" and "proxyConfig" kibana.yml settings
+ defaultProxyValue = Object.freeze({
+ readLegacyESConfig,
+ pathFilters: [/.*/],
+ proxyConfigCollection: new ProxyConfigCollection([]),
+ });
+}
+
interface MockDepsArgument extends Partial> {
proxy?: Partial;
}
@@ -51,5 +64,6 @@ export const getProxyRouteHandlerDeps = ({
}
: defaultProxyValue,
log,
+ kibanaVersion,
};
};
diff --git a/src/plugins/console/server/routes/api/console/proxy/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/params.test.ts
index e08d2f8adecbf..edefb2f11f1f1 100644
--- a/src/plugins/console/server/routes/api/console/proxy/params.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/params.test.ts
@@ -5,14 +5,17 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { SemVer } from 'semver';
import { kibanaResponseFactory } from '../../../../../../../core/server';
-import { getProxyRouteHandlerDeps } from './mocks';
-import { createResponseStub } from './stubs';
+import { getProxyRouteHandlerDeps } from './mocks'; // import need to come first
+import { createResponseStub } from './stubs'; // import needs to come first
+import { MAJOR_VERSION } from '../../../../../common/constants';
import * as requestModule from '../../../../lib/proxy_request';
-
import { createHandler } from './create_handler';
+const kibanaVersion = new SemVer(MAJOR_VERSION);
+
describe('Console Proxy Route', () => {
let handler: ReturnType;
@@ -21,58 +24,71 @@ describe('Console Proxy Route', () => {
});
describe('params', () => {
- describe('pathFilters', () => {
- describe('no matches', () => {
- it('rejects with 403', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
- );
+ if (kibanaVersion.major < 8) {
+ describe('pathFilters', () => {
+ describe('no matches', () => {
+ it('rejects with 403', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({
+ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
+ })
+ );
- const { status } = await handler(
- {} as any,
- { query: { method: 'POST', path: '/baz/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { query: { method: 'POST', path: '/baz/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(403);
+ expect(status).toBe(403);
+ });
});
- });
- describe('one match', () => {
- it('allows the request', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
- );
- (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+ describe('one match', () => {
+ it('allows the request', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({
+ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
+ })
+ );
+
+ (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
- const { status } = await handler(
- {} as any,
- { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(200);
- expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ expect(status).toBe(200);
+ expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ });
});
- });
- describe('all match', () => {
- it('allows the request', async () => {
- handler = createHandler(
- getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
- );
- (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+ describe('all match', () => {
+ it('allows the request', async () => {
+ handler = createHandler(
+ getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
+ );
+
+ (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
- const { status } = await handler(
- {} as any,
- { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
- kibanaResponseFactory
- );
+ const { status } = await handler(
+ {} as any,
+ { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
+ kibanaResponseFactory
+ );
- expect(status).toBe(200);
- expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ expect(status).toBe(200);
+ expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
+ });
});
});
- });
+ } else {
+ // jest requires to have at least one test in the file
+ test('dummy required test', () => {
+ expect(true).toBe(true);
+ });
+ }
});
});
diff --git a/src/plugins/console/server/routes/index.ts b/src/plugins/console/server/routes/index.ts
index 2c46547f92f1b..3911e8cfabc60 100644
--- a/src/plugins/console/server/routes/index.ts
+++ b/src/plugins/console/server/routes/index.ts
@@ -7,6 +7,7 @@
*/
import { IRouter, Logger } from 'kibana/server';
+import { SemVer } from 'semver';
import { EsLegacyConfigService, SpecDefinitionsService } from '../services';
import { ESConfigForProxy } from '../types';
@@ -18,8 +19,8 @@ import { registerSpecDefinitionsRoute } from './api/console/spec_definitions';
export interface ProxyDependencies {
readLegacyESConfig: () => Promise;
- pathFilters: RegExp[];
- proxyConfigCollection: ProxyConfigCollection;
+ pathFilters?: RegExp[]; // Only present in 7.x
+ proxyConfigCollection?: ProxyConfigCollection; // Only present in 7.x
}
export interface RouteDependencies {
@@ -30,6 +31,7 @@ export interface RouteDependencies {
esLegacyConfigService: EsLegacyConfigService;
specDefinitionService: SpecDefinitionsService;
};
+ kibanaVersion: SemVer;
}
export const registerRoutes = (dependencies: RouteDependencies) => {
From 3d0da7f0f69aa37db96db7489566c8bd2198be58 Mon Sep 17 00:00:00 2001
From: Sandra G
Date: Mon, 4 Oct 2021 11:37:19 -0400
Subject: [PATCH 13/14] [Stack Monitoring] Migrate Index Views to React
(#113660)
* index views
* fix type
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monitoring/public/application/index.tsx | 17 +++
.../elasticsearch/index_advanced_page.tsx | 77 +++++++++++++
.../pages/elasticsearch/index_page.tsx | 106 ++++++++++++++++++
.../pages/elasticsearch/item_template.tsx | 36 ++++++
.../pages/elasticsearch/node_page.tsx | 56 +++++----
.../elasticsearch/index/index_react.js | 70 ++++++++++++
.../components/cluster_view_react.js | 5 +-
7 files changed, 335 insertions(+), 32 deletions(-)
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index acdf3b0986a64..a958e6061215d 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -27,6 +27,8 @@ import { KibanaOverviewPage } from './pages/kibana/overview';
import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS, CODE_PATH_KIBANA } from '../../common/constants';
import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page';
import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page';
+import { ElasticsearchIndexPage } from './pages/elasticsearch/index_page';
+import { ElasticsearchIndexAdvancedPage } from './pages/elasticsearch/index_advanced_page';
import { ElasticsearchNodePage } from './pages/elasticsearch/node_page';
import { MonitoringTimeContainer } from './hooks/use_monitoring_time';
import { BreadcrumbContainer } from './hooks/use_breadcrumbs';
@@ -84,6 +86,21 @@ const MonitoringApp: React.FC<{
/>
{/* ElasticSearch Views */}
+
+
+
+
+
= ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { index }: { index: string } = useParams();
+ const { zoomInfo, onBrush } = useCharts();
+ const clusterUuid = globalState.cluster_uuid;
+ const [data, setData] = useState({} as any);
+
+ const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', {
+ defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ is_advanced: true,
+ }),
+ });
+ setData(response);
+ }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]);
+
+ return (
+
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
new file mode 100644
index 0000000000000..b23f9c71a98bf
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useContext, useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useParams } from 'react-router-dom';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { GlobalStateContext } from '../../global_state_context';
+// @ts-ignore
+import { IndexReact } from '../../../components/elasticsearch/index/index_react';
+import { ComponentProps } from '../../route_init';
+import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
+import { useCharts } from '../../hooks/use_charts';
+import { ItemTemplate } from './item_template';
+// @ts-ignore
+import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes';
+// @ts-ignore
+import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
+
+interface SetupModeProps {
+ setupMode: any;
+ flyoutComponent: any;
+ bottomBarComponent: any;
+}
+
+export const ElasticsearchIndexPage: React.FC = ({ clusters }) => {
+ const globalState = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { index }: { index: string } = useParams();
+ const { zoomInfo, onBrush } = useCharts();
+ const clusterUuid = globalState.cluster_uuid;
+ const [data, setData] = useState({} as any);
+ const [indexLabel, setIndexLabel] = useState(labels.index as any);
+ const [nodesByIndicesData, setNodesByIndicesData] = useState([]);
+
+ const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', {
+ defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.index.overview.pageTitle', {
+ defaultMessage: 'Index: {indexName}',
+ values: {
+ indexName: index,
+ },
+ });
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ is_advanced: false,
+ }),
+ });
+ setData(response);
+ const transformer = indicesByNodes();
+ setNodesByIndicesData(transformer(response.shards, response.nodes));
+
+ const shards = response.shards;
+ if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) {
+ setIndexLabel(labels.indexWithUnassigned);
+ }
+ }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]);
+
+ return (
+
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
new file mode 100644
index 0000000000000..1f06ba18bf102
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { PageTemplate } from '../page_template';
+import { TabMenuItem, PageTemplateProps } from '../page_template';
+
+interface ItemTemplateProps extends PageTemplateProps {
+ id: string;
+ pageType: string;
+}
+export const ItemTemplate: React.FC = (props) => {
+ const { pageType, id, ...rest } = props;
+ const tabs: TabMenuItem[] = [
+ {
+ id: 'overview',
+ label: i18n.translate('xpack.monitoring.esItemNavigation.overviewLinkText', {
+ defaultMessage: 'Overview',
+ }),
+ route: `/elasticsearch/${pageType}/${id}`,
+ },
+ {
+ id: 'advanced',
+ label: i18n.translate('xpack.monitoring.esItemNavigation.advancedLinkText', {
+ defaultMessage: 'Advanced',
+ }),
+ route: `/elasticsearch/${pageType}/${id}/advanced`,
+ },
+ ];
+
+ return ;
+};
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
index ffbde2efcac6b..9b3a67f612e5c 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
@@ -7,8 +7,7 @@
import React, { useContext, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
-import { find } from 'lodash';
-import { ElasticsearchTemplate } from './elasticsearch_template';
+import { ItemTemplate } from './item_template';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../global_state_context';
import { NodeReact } from '../../../components/elasticsearch';
@@ -18,6 +17,8 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont
import { useLocalStorage } from '../../hooks/use_local_storage';
import { useCharts } from '../../hooks/use_charts';
import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices';
+// @ts-ignore
+import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
interface SetupModeProps {
setupMode: any;
@@ -38,9 +39,6 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) =>
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
- const cluster = find(clusters, {
- cluster_uuid: clusterUuid,
- });
const [data, setData] = useState({} as any);
const [nodesByIndicesData, setNodesByIndicesData] = useState([]);
@@ -92,33 +90,33 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) =>
}, [showSystemIndices, setShowSystemIndices]);
return (
-
-
- (
-
- {flyoutComponent}
-
- {bottomBarComponent}
-
- )}
- />
-
-
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
);
};
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
new file mode 100644
index 0000000000000..70bac52a0926c
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index_react.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import {
+ EuiPage,
+ EuiPageContent,
+ EuiPageBody,
+ EuiPanel,
+ EuiSpacer,
+ EuiFlexGrid,
+ EuiFlexItem,
+} from '@elastic/eui';
+import { IndexDetailStatus } from '../index_detail_status';
+import { MonitoringTimeseriesContainer } from '../../chart';
+import { ShardAllocationReact } from '../shard_allocation/shard_allocation_react';
+import { Logs } from '../../logs';
+import { AlertsCallout } from '../../../alerts/callout';
+
+export const IndexReact = ({
+ indexSummary,
+ metrics,
+ clusterUuid,
+ indexUuid,
+ logs,
+ alerts,
+ ...props
+}) => {
+ const metricsToShow = [
+ metrics.index_mem,
+ metrics.index_size,
+ metrics.index_search_request_rate,
+ metrics.index_request_rate,
+ metrics.index_segment_count,
+ metrics.index_document_count,
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {metricsToShow.map((metric, index) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
index 987ca467931f4..2d0c4b59df4b8 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view_react.js
@@ -8,13 +8,12 @@
import React from 'react';
import { TableHeadReact } from './table_head_react';
import { TableBody } from './table_body';
-import { labels } from '../lib/labels';
export const ClusterViewReact = (props) => {
return (
@@ -22,7 +21,7 @@ export const ClusterViewReact = (props) => {
filter={props.filter}
totalCount={props.totalCount}
rows={props.nodesByIndices}
- cols={labels.node.length}
+ cols={props.labels.length}
shardStats={props.shardStats}
/>
From 59b15df115f5f0ed8f6d6a54c862594534dcd13a Mon Sep 17 00:00:00 2001
From: Sergi Massaneda
Date: Mon, 4 Oct 2021 17:49:14 +0200
Subject: [PATCH 14/14] fix priority reset bug (#113626)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../jira/jira_params.test.tsx | 18 ++++++++++++++++++
.../builtin_action_types/jira/jira_params.tsx | 4 ++--
.../jira/use_get_fields_by_issue_type.tsx | 2 +-
3 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
index a05db00f141ab..812c234e80d9e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
@@ -95,6 +95,10 @@ describe('JiraParamsFields renders', () => {
description: { allowedValues: [], defaultValue: {} },
},
};
+ const useGetFieldsByIssueTypeResponseLoading = {
+ isLoading: true,
+ fields: {},
+ };
beforeEach(() => {
jest.clearAllMocks();
@@ -421,5 +425,19 @@ describe('JiraParamsFields renders', () => {
expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium');
expect(editAction.mock.calls[1][1].incident.priority).toEqual(null);
});
+
+ test('Preserve priority when the issue type fields are loading and hasPriority becomes stale', () => {
+ useGetFieldsByIssueTypeMock
+ .mockReturnValueOnce(useGetFieldsByIssueTypeResponseLoading)
+ .mockReturnValue(useGetFieldsByIssueTypeResponse);
+ const wrapper = mount();
+
+ expect(editAction).not.toBeCalled();
+
+ wrapper.setProps({ ...defaultProps }); // just to force component call useGetFieldsByIssueType again
+
+ expect(editAction).toBeCalledTimes(1);
+ expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium');
+ });
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
index 834892f2bf374..32390c163cf2a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
@@ -147,11 +147,11 @@ const JiraParamsFields: React.FunctionComponent {
- if (!hasPriority && incident.priority != null) {
+ if (!isLoadingFields && !hasPriority && incident.priority != null) {
editSubActionProperty('priority', null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasPriority]);
+ }, [hasPriority, isLoadingFields]);
const labelOptions = useMemo(
() => (incident.labels ? incident.labels.map((label: string) => ({ label })) : []),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
index 38be618119c4a..61db73c129db6 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
@@ -62,8 +62,8 @@ export const useGetFieldsByIssueType = ({
});
if (!didCancel) {
- setIsLoading(false);
setFields(res.data ?? {});
+ setIsLoading(false);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.FIELDS_API_ERROR,