From 4600acc11be76e87d064ad90cb9cd449fca517bd Mon Sep 17 00:00:00 2001 From: Kavitha Conjeevaram Mohan Date: Mon, 18 Apr 2022 23:14:49 +0530 Subject: [PATCH] Modularize event Analytics live tail and fix bug (#647) * module event live Signed-off-by: Kavitha Conjeevaram Mohan * fix bug- stops live tail when moved using breadcrumbs Signed-off-by: Kavitha Conjeevaram Mohan * format livetail file, optional args and specific cypress name Signed-off-by: Kavitha Conjeevaram Mohan * format using prettier Signed-off-by: Kavitha Conjeevaram Mohan * change jest test location Signed-off-by: Kavitha Conjeevaram Mohan * change test location Signed-off-by: Kavitha Conjeevaram Mohan --- .../integration/event_analytics.spec.js | 4 +- .../common/constants/shared.ts | 51 ++++ .../common/types/explorer.ts | 8 + .../live_tail_button.test.tsx.snap | 275 ++++++++++++++++++ .../__tests__/live_tail_button.test.tsx | 75 +++++ .../common/live_tail/live_tail_button.tsx | 55 ++++ .../components/common/search/search.tsx | 29 +- .../event_analytics/explorer/explorer.tsx | 224 ++++---------- 8 files changed, 543 insertions(+), 178 deletions(-) create mode 100644 dashboards-observability/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap create mode 100644 dashboards-observability/public/components/common/live_tail/__tests__/live_tail_button.test.tsx create mode 100644 dashboards-observability/public/components/common/live_tail/live_tail_button.tsx diff --git a/dashboards-observability/.cypress/integration/event_analytics.spec.js b/dashboards-observability/.cypress/integration/event_analytics.spec.js index 46b03b715..981b10b53 100644 --- a/dashboards-observability/.cypress/integration/event_analytics.spec.js +++ b/dashboards-observability/.cypress/integration/event_analytics.spec.js @@ -394,7 +394,7 @@ describe('Switch on and off livetail', () => { cy.get('[data-test-subj="searchAutocompleteTextArea"]').type(TEST_QUERIES[1].query); cy.get('[data-test-subj=eventLiveTail]').click(); - cy.get('[data-test-subj=eventLiveTail__delay10]').click(); + cy.get('[data-test-subj=eventLiveTail__delay10s]').click(); cy.wait(delay * 2); cy.get('.euiToastHeader__title').contains('On').should('exist'); @@ -412,7 +412,7 @@ describe('Live tail stop automatically', () => { cy.get('[data-test-subj="searchAutocompleteTextArea"]').type(TEST_QUERIES[1].query); cy.get('[data-test-subj=eventLiveTail]').click(); - cy.get('[data-test-subj=eventLiveTail__delay10]').click(); + cy.get('[data-test-subj=eventLiveTail__delay10s]').click(); cy.wait(delay * 2); cy.get('.euiToastHeader__title').contains('On').should('exist'); }); diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 7f6e0a2ca..19c0fb8fd 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -74,3 +74,54 @@ export const pageStyles: CSS.Properties = { export const NUMERICAL_FIELDS = ['short', 'integer', 'long', 'float', 'double']; export const ENABLED_VIS_TYPES = ['bar', 'horizontal_bar', 'line', 'pie', 'heatmap', 'text']; + +//Live tail constants +export const LIVE_OPTIONS = [ + { + label:'5s', + startTime: 'now-5s', + delayTime: 5000, + }, + { + label:'10s', + startTime: 'now-10s', + delayTime: 10000, + }, + { + label:'30s', + startTime: 'now-30s', + delayTime: 30000, + }, + { + label:'1m', + startTime: 'now-1m', + delayTime: 60000, + }, + { + label:'5m', + startTime: 'now-5m', + delayTime: 60000 * 5, + }, + { + label:'15m', + startTime: 'now-15m', + delayTime: 60000 * 15, + }, + { + label:'30m', + startTime: 'now-30m', + delayTime: 60000 * 30, + }, + { + label:'1h', + startTime: 'now-1h', + delayTime: 60000 * 60, + }, + { + label:'2h', + startTime: 'now-2h', + delayTime: 60000 * 120, + }, +]; + +export const LIVE_END_TIME ='now'; \ No newline at end of file diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index 9f5c15fb0..fcf9017d2 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -217,3 +217,11 @@ export interface IDefaultTimestampState { default_timestamp: string; message: string; } + +export interface LiveTailProps { + isLiveTailOn: boolean; + setIsLiveTailPopoverOpen: React.Dispatch>; + liveTailName: string; + isLiveTailPopoverOpen: boolean; + dataTestSubj: string; +} diff --git a/dashboards-observability/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap b/dashboards-observability/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap new file mode 100644 index 000000000..78104e652 --- /dev/null +++ b/dashboards-observability/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap @@ -0,0 +1,275 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Live tail button change live tail to 10s interval 1`] = ` + + + + + + + +`; + +exports[`Live tail button starts live tail with 5s interval 1`] = ` + + + + + + + +`; + +exports[`Live tail off button stop live tail 1`] = ` + + + + + + + +`; diff --git a/dashboards-observability/public/components/common/live_tail/__tests__/live_tail_button.test.tsx b/dashboards-observability/public/components/common/live_tail/__tests__/live_tail_button.test.tsx new file mode 100644 index 000000000..4cb2a478f --- /dev/null +++ b/dashboards-observability/public/components/common/live_tail/__tests__/live_tail_button.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount, shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { LiveTailButton, StopLiveButton } from '../live_tail_button'; +import { waitFor } from '@testing-library/dom'; + + describe('Live tail button', () => { + configure({ adapter: new Adapter() }); + + it('starts live tail with 5s interval', async () => { + const setIsLiveTailPopoverOpen = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('change live tail to 10s interval', async () => { + const setIsLiveTailPopoverOpen = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + }); + + describe('Live tail off button', () => { + configure({ adapter: new Adapter() }); + + it('stop live tail', async () => { + const StopLive = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + }); \ No newline at end of file diff --git a/dashboards-observability/public/components/common/live_tail/live_tail_button.tsx b/dashboards-observability/public/components/common/live_tail/live_tail_button.tsx new file mode 100644 index 000000000..0e53152fa --- /dev/null +++ b/dashboards-observability/public/components/common/live_tail/live_tail_button.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +//Define pop over interval options for live tail button in your plugin + +import { EuiButton } from "@elastic/eui"; +import React, { useMemo } from "react"; +import { LiveTailProps } from "common/types/explorer"; + +//Live Tail Button +export const LiveTailButton = ({ + isLiveTailOn, + isLiveTailPopoverOpen, + setIsLiveTailPopoverOpen, + liveTailName, + dataTestSubj, +}: LiveTailProps) => { + const liveButton = useMemo(() => { + return ( + setIsLiveTailPopoverOpen(!isLiveTailPopoverOpen)} + data-test-subj={dataTestSubj} + > + {liveTailName} + + ); + }, [isLiveTailPopoverOpen, isLiveTailOn]); + return liveButton; +}; + +export const StopLiveButton = (props: any) => { + const { StopLive, dataTestSubj } = props; + + const stopButton = () => { + return ( + StopLive()} + color="danger" + data-test-subj={dataTestSubj} + > + Stop + + ); + }; + return stopButton(); +}; + +export const sleep = (milliseconds: number | undefined) => { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +}; diff --git a/dashboards-observability/public/components/common/search/search.tsx b/dashboards-observability/public/components/common/search/search.tsx index 46078c69b..799ae2688 100644 --- a/dashboards-observability/public/components/common/search/search.tsx +++ b/dashboards-observability/public/components/common/search/search.tsx @@ -25,6 +25,7 @@ import { SavePanel } from '../../event_analytics/explorer/save_panel'; import { PPLReferenceFlyout } from '../helpers'; import { uiSettingsService } from '../../../../common/utils'; import { APP_ANALYTICS_TAB_ID_REGEX } from '../../../../common/constants/explorer'; +import { LiveTailButton, StopLiveButton } from '../live_tail/live_tail_button'; export interface IQueryBarProps { query: string; tempQuery: string; @@ -78,6 +79,8 @@ export const Search = (props: any) => { tabId = '', baseQuery = '', stopLive, + setIsLiveTailPopoverOpen, + liveTailName, } = props; const appLogEvents = tabId.match(APP_ANALYTICS_TAB_ID_REGEX); @@ -113,6 +116,16 @@ export const Search = (props: any) => { ); + const liveButton = ( + + ); + return (
@@ -165,11 +178,11 @@ export const Search = (props: any) => { /> )} - {!showSavePanelOptionsList && ( + {showSaveButton && !showSavePanelOptionsList && ( @@ -179,14 +192,10 @@ export const Search = (props: any) => { )} {isLiveTailOn && ( - stopLive()} - color="danger" - data-test-subj="eventLiveTail__off" - > - Stop - + )} {showSaveButton && searchBarConfigs[selectedSubTabId]?.showSaveButton && ( diff --git a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx index 3171f8476..37cd5affd 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx @@ -20,7 +20,6 @@ import { EuiFlexItem, EuiLink, EuiContextMenuItem, - EuiButton, } from '@elastic/eui'; import dateMath from '@elastic/datemath'; import classNames from 'classnames'; @@ -55,7 +54,7 @@ import { FINAL_QUERY, DATE_PICKER_FORMAT, } from '../../../../common/constants/explorer'; -import { PPL_STATS_REGEX, PPL_NEWLINE_REGEX } from '../../../../common/constants/shared'; +import { PPL_STATS_REGEX, PPL_NEWLINE_REGEX, LIVE_OPTIONS, LIVE_END_TIME } from '../../../../common/constants/shared'; import { getIndexPatternFromRawQuery, preprocessQuery, buildQuery } from '../../../../common/utils'; import { useFetchEvents, useFetchVisualizations } from '../hooks'; import { changeQuery, changeDateRange, selectQueries } from '../redux/slices/query_slice'; @@ -77,6 +76,7 @@ import { onItemSelect, } from '../../common/search/autocomplete_logic'; import { formatError } from '../utils'; +import { sleep } from '../../common/live_tail/live_tail_button'; const TYPE_TAB_MAPPING = { [SAVED_QUERY]: TAB_EVENT_ID, @@ -298,7 +298,7 @@ export const Explorer = ({ indexPattern: string ): Promise => await timestampUtils.getTimestamp(indexPattern); - const fetchData = async () => { + const fetchData = async (startTime?: string, endTime?: string) => { const curQuery = queryRef.current; const rawQueryStr = buildQuery(appBaseQuery, curQuery![RAW_QUERY]); const curIndex = getIndexPatternFromRawQuery(rawQueryStr); @@ -323,11 +323,16 @@ export const Explorer = ({ } } + if ((isEqual(typeof startTime, 'undefined')) && (isEqual(typeof endTime, 'undefined'))) { + startTime = curQuery![SELECTED_DATE_RANGE][0]; + endTime = curQuery![SELECTED_DATE_RANGE][1]; + } + // compose final query const finalQuery = composeFinalQuery( curQuery, - curQuery![SELECTED_DATE_RANGE][0], - curQuery![SELECTED_DATE_RANGE][1], + startTime, + endTime, curTimestamp, isLiveTailOnRef.current ); @@ -347,84 +352,39 @@ export const Explorer = ({ getVisualizations(); getAvailableFields(`search source=${curIndex}`); } else { - findAutoInterval(curQuery![SELECTED_DATE_RANGE][0], curQuery![SELECTED_DATE_RANGE][1]); - getEvents(undefined, (error) => { - const formattedError = formatError(error.name, error.message, error.body.message); - notifications.toasts.addError(formattedError, { - title: 'Error fetching events', + findAutoInterval(startTime, endTime); + if (isLiveTailOnRef.current){ + getLiveTail(undefined, (error) => { + const formattedError = formatError(error.name, error.message, error.body.message); + notifications.toasts.addError(formattedError, { + title: 'Error fetching events', + }); }); - }); + } else { + getEvents(undefined, (error) => { + const formattedError = formatError(error.name, error.message, error.body.message); + notifications.toasts.addError(formattedError, { + title: 'Error fetching events', + }); + }); + } getCountVisualizations(minInterval); } // for comparing usage if for the same tab, user changed index from one to another - setPrevIndex(curTimestamp); - if (!queryRef.current!.isLoaded) { - dispatch( - changeQuery({ - tabId, - query: { - isLoaded: true, - }, - }) - ); - } - }; - - const fetchLiveData = async (startTime: string, endTime: string) => { - const curQuery = queryRef.current; - const rawQueryStr = buildQuery(appBaseQuery, curQuery![RAW_QUERY]); - const curIndex = getIndexPatternFromRawQuery(rawQueryStr); - if (isEmpty(rawQueryStr)) { - return; - } - - if (isEmpty(curIndex)) { - setToast('Query does not include vaild index.', 'danger'); - return; - } - - let curTimestamp: string = curQuery![SELECTED_TIMESTAMP]; - - if (isEmpty(curTimestamp)) { - const defaultTimestamp = await getDefaultTimestampByIndexPattern(curIndex); - if (isEmpty(defaultTimestamp.default_timestamp)) { - setToast(defaultTimestamp.message, 'danger'); - return; - } - curTimestamp = defaultTimestamp.default_timestamp; - if (defaultTimestamp.hasSchemaConflict) { - setToast(defaultTimestamp.message, 'danger'); + if (!isLiveTailOnRef.current){ + setPrevIndex(curTimestamp); + if (!queryRef.current!.isLoaded) { + dispatch( + changeQuery({ + tabId, + query: { + isLoaded: true, + }, + }) + ); } } - - // compose final query - const finalQuery = composeFinalQuery( - curQuery, - startTime, - endTime, - curTimestamp, - isLiveTailOnRef.current - ); - - await dispatch( - changeQuery({ - tabId, - query: { - finalQuery, - [SELECTED_TIMESTAMP]: curTimestamp, - }, - }) - ); - - findAutoInterval(startTime, endTime); - getLiveTail(undefined, (error) => { - const formattedError = formatError(error.name, error.message, error.body.message); - notifications.toasts.addError(formattedError, { - title: 'Error fetching events', - }); - }); - getCountVisualizations(minInterval); }; const isIndexPatternChanged = (currentQuery: string, prevTabQuery: string) => @@ -1057,23 +1017,6 @@ export const Explorer = ({ } }; - const wrappedPopoverButton = useMemo(() => { - return ( - setIsLiveTailPopoverOpen(!isLiveTailPopoverOpen)} - data-test-subj="eventLiveTail" - > - {liveTailNameRef.current} - - ); - }, [isLiveTailPopoverOpen, isLiveTailOn]); - - const sleep = (milliseconds: number | undefined) => { - return new Promise((resolve) => setTimeout(resolve, milliseconds)); - }; - const liveTailLoop = async ( name: string, startTime: string, @@ -1115,81 +1058,29 @@ export const Explorer = ({ } }, [selectedContentTabId, browserTabFocus]); - const popoverItems: ReactElement[] = [ - { - liveTailLoop('5s', 'now-5s', 'now', 5000); - }} - > - 5s - , - { - liveTailLoop('10s', 'now-10s', 'now', 10000); - }} - > - 10s - , - { - liveTailLoop('30s', 'now-30s', 'now', 30000); - }} - > - 30s - , - { - liveTailLoop('1m', 'now-1m', 'now', 60000); - }} - > - 1m - , - { - liveTailLoop('5m', 'now-5m', 'now', 60000 * 5); - }} - > - 5m - , - { - liveTailLoop('15m', 'now-15m', 'now', 60000 * 15); - }} - > - 15m - , - { - liveTailLoop('30m', 'now-30m', 'now', 60000 * 30); - }} - > - 30m - , - { - liveTailLoop('1h', 'now-1h', 'now', 60000 * 60); - }} - > - 1h - , + //stop live tail if the page is moved using breadcrumbs + let lastUrl = location.href; + new MutationObserver(() => { + const url = location.href; + if (url !== lastUrl) { + lastUrl = url; + stopLive(); + } + }).observe(document, {subtree: true, childList: true}); + + const popoverItems: ReactElement[] = LIVE_OPTIONS.map((e) => { + return ( { - liveTailLoop('2h', 'now-2h', 'now', 60000 * 120); + liveTailLoop(e.label, e.startTime, LIVE_END_TIME, e.delayTime); }} + data-test-subj={'eventLiveTail__delay'+e.label} > - 2h - , - ]; + {e.label} + + ) + }); const dateRange = isEmpty(startTime) || isEmpty(endTime) @@ -1201,7 +1092,7 @@ export const Explorer = ({ const handleLiveTailSearch = useCallback( async (startTime: string, endTime: string) => { await updateQueryInStore(tempQuery); - fetchLiveData(startTime, endTime); + fetchData(startTime, endTime); }, [tempQuery] ); @@ -1238,7 +1129,6 @@ export const Explorer = ({ savedObjects={savedObjects} showSavePanelOptionsList={isEqual(selectedContentTabId, TAB_CHART_ID)} handleTimeRangePickerRefresh={handleTimeRangePickerRefresh} - liveTailButton={wrappedPopoverButton} isLiveTailPopoverOpen={isLiveTailPopoverOpen} closeLiveTailPopover={() => setIsLiveTailPopoverOpen(false)} popoverItems={popoverItems} @@ -1250,6 +1140,8 @@ export const Explorer = ({ tabId={tabId} baseQuery={appBaseQuery} stopLive={stopLive} + setIsLiveTailPopoverOpen={setIsLiveTailPopoverOpen} + liveTailName={liveTailNameRef.current} />