From 7702b957c991656e8f4e849c56030a663bc3145a Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 15 Oct 2020 10:25:09 +0200 Subject: [PATCH 01/13] obs perf --- .../public/application/index.tsx | 7 +- .../public/context/has_data_context.test.tsx | 353 ++++++++++++++++++ .../public/context/has_data_context.tsx | 104 ++++++ .../observability/public/data_handler.ts | 43 +-- .../public/hooks/use_has_data.ts | 12 + .../public/hooks/use_route_params.tsx | 11 +- .../public/hooks/use_time_range.test.ts | 98 +++++ .../public/hooks/use_time_range.ts | 34 ++ .../observability/public/pages/home/index.tsx | 23 +- .../public/pages/overview/data_sections.tsx | 11 +- .../public/pages/overview/index.tsx | 54 +-- .../observability/public/routes/index.tsx | 15 +- .../typings/fetch_overview_data/index.ts | 16 +- 13 files changed, 664 insertions(+), 117 deletions(-) create mode 100644 x-pack/plugins/observability/public/context/has_data_context.test.tsx create mode 100644 x-pack/plugins/observability/public/context/has_data_context.tsx create mode 100644 x-pack/plugins/observability/public/hooks/use_has_data.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_time_range.test.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_time_range.ts diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 70493b5634f7d..3fb71f5d837c2 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -18,6 +18,7 @@ import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; import { Breadcrumbs, routes } from '../routes'; import { ObservabilityPluginSetupDeps } from '../plugin'; +import { HasDataContextProvider } from '../context/has_data_context'; const observabilityLabelBreadcrumb = { text: i18n.translate('xpack.observability.observability.breadcrumb.', { @@ -45,7 +46,7 @@ function App() { core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb)); }, [core, breadcrumb]); - const { query, path: pathParams } = useRouteParams(route.params); + const { query, path: pathParams } = useRouteParams(path); return route.handler({ query, path: pathParams }); }; return ; @@ -70,7 +71,9 @@ export const renderApp = ( - + + + diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx new file mode 100644 index 0000000000000..75ebdc073481e --- /dev/null +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -0,0 +1,353 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// import { act, getByText } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { registerDataHandler, unregisterDataHandler } from '../data_handler'; +import { useHasData } from '../hooks/use_has_data'; +import * as routeParams from '../hooks/use_route_params'; +import * as timeRange from '../hooks/use_time_range'; +import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data'; +import { HasDataContextProvider } from './has_data_context'; + +const rangeFrom = '2020-10-08T06:00:00.000Z'; +const rangeTo = '2020-10-08T07:00:00.000Z'; + +function wrapper({ children }: { children: React.ReactElement }) { + return {children}; +} + +function unregisterAll() { + unregisterDataHandler({ appName: 'apm' }); + unregisterDataHandler({ appName: 'infra_logs' }); + unregisterDataHandler({ appName: 'infra_metrics' }); + unregisterDataHandler({ appName: 'uptime' }); + unregisterDataHandler({ appName: 'ux' }); +} + +function registerApps( + apps: Array<{ appName: T; hasData: HasData }> +) { + apps.forEach(({ appName, hasData }) => { + registerDataHandler({ + appName, + fetchData: () => ({} as any), + hasData, + }); + }); +} + +describe('HasDataContextProvider', () => { + beforeAll(() => { + jest.spyOn(routeParams, 'useRouteParams').mockImplementation(() => ({ + query: { + from: rangeFrom, + to: rangeTo, + }, + path: {}, + })); + jest.spyOn(timeRange, 'useTimeRange').mockImplementation(() => ({ + rangeFrom, + rangeTo, + absStart: new Date(rangeFrom).valueOf(), + absEnd: new Date(rangeTo).valueOf(), + })); + }); + + describe('when no plugin has registered', () => { + it.only('hasAnyData returns false and all apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + }, + hasAnyData: false, + }); + }); + + describe('when plugins have registered', () => { + describe('all apps return false', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => false }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false and all apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: false, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + }, + hasAnyData: false, + }); + }); + }); + + describe('at least one app returns true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true apm returns true and all other apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + }, + hasAnyData: true, + }); + }); + }); + + describe('all apps return true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true and all apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + }, + hasAnyData: true, + }); + }); + }); + + describe('only apm is registered', () => { + describe('when apm returns true', () => { + beforeAll(() => { + registerApps([{ appName: 'apm', hasData: async () => true }]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true, apm returns true and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + }, + hasAnyData: true, + }); + }); + }); + + describe('when apm returns false', () => { + beforeAll(() => { + registerApps([{ appName: 'apm', hasData: async () => false }]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false, apm returns false and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: false, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + }, + hasAnyData: false, + }); + }); + }); + }); + + describe('when an app throws an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true, apm is undefined and all other apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + }, + hasAnyData: true, + }); + }); + }); + + describe('when all apps throw an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'infra_logs', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'infra_metrics', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'uptime', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + { + appName: 'ux', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns false and all apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: undefined, + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: undefined, status: 'failure' }, + infra_logs: { hasData: undefined, status: 'failure' }, + infra_metrics: { hasData: undefined, status: 'failure' }, + ux: { hasData: undefined, status: 'failure' }, + }, + hasAnyData: false, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx new file mode 100644 index 0000000000000..799389f98c10d --- /dev/null +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; +import React, { createContext, useEffect, useState } from 'react'; +import { getDataHandler } from '../data_handler'; +import { FETCH_STATUS } from '../hooks/use_fetcher'; +import { useRouteParams } from '../hooks/use_route_params'; +import { useTimeRange } from '../hooks/use_time_range'; +import { + ObservabilityFetchDataPlugins, + UXHasDataResponse, + ObservabilityHasDataResponse, +} from '../typings/fetch_overview_data'; + +export type HasDataMap = Record< + ObservabilityFetchDataPlugins, + { status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse } +>; + +export interface HasDataContextValue { + hasData: Partial; + hasAnyData: boolean | undefined; +} + +export const HasDataContext = createContext({} as HasDataContextValue); + +const apps: ObservabilityFetchDataPlugins[] = [ + 'apm', + 'uptime', + 'infra_logs', + 'infra_metrics', + 'ux', +]; + +export function HasDataContextProvider({ children }: { children: React.ReactNode }) { + const { rangeFrom, rangeTo } = useRouteParams('/overview').query; + const { absStart, absEnd } = useTimeRange({ rangeFrom, rangeTo }); + + const [hasData, setHasData] = useState({}); + + useEffect(() => { + apps.forEach(async (app) => { + try { + const params = + app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; + + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } catch (e) { + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + }); + }, [absStart, absEnd]); + + const allRequestCompleted = isEmpty(hasData) + ? false + : Object.values(hasData).every((dataContext) => dataContext?.status !== FETCH_STATUS.LOADING); + + const hasSomeData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( + (app) => hasData[app]?.hasData === true + ); + + let hasAnyData; + if (hasSomeData === true) { + hasAnyData = true; + // Waits until all requests are complete to set hasAnyData to false + } else if (hasSomeData === false && allRequestCompleted === true) { + hasAnyData = false; + } + + const _hasData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).reduce< + Partial + >((acc, app) => { + const data = hasData[app]; + if (!data) { + return acc; + } + if (app === 'ux') { + acc[app] = data.hasData as UXHasDataResponse; + } else { + acc[app] = data.hasData as boolean; + } + return acc; + }, {}); + + console.log('### caue: HasDataContextProvider -> _hasData', _hasData); + return ; +} diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 91043a3da0dab..7ee7db7ede17d 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - DataHandler, - HasDataResponse, - ObservabilityFetchDataPlugins, -} from './typings/fetch_overview_data'; +import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data'; const dataHandlers: Partial> = {}; @@ -34,40 +30,3 @@ export function getDataHandler(appName: return dataHandler as DataHandler; } } - -export async function fetchHasData(absoluteTime: { - start: number; - end: number; -}): Promise> { - const apps: ObservabilityFetchDataPlugins[] = [ - 'apm', - 'uptime', - 'infra_logs', - 'infra_metrics', - 'ux', - ]; - - const promises = apps.map( - async (app) => - getDataHandler(app)?.hasData(app === 'ux' ? { absoluteTime } : undefined) || false - ); - - const results = await Promise.allSettled(promises); - - const [apm, uptime, logs, metrics, ux] = results.map((result) => { - if (result.status === 'fulfilled') { - return result.value; - } - - console.error('Error while fetching has data', result.reason); - return false; - }); - - return { - apm, - uptime, - ux, - infra_logs: logs, - infra_metrics: metrics, - }; -} diff --git a/x-pack/plugins/observability/public/hooks/use_has_data.ts b/x-pack/plugins/observability/public/hooks/use_has_data.ts new file mode 100644 index 0000000000000..9c66fa8861420 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_has_data.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext } from 'react'; +import { HasDataContext } from '../context/has_data_context'; + +export function useHasData() { + return useContext(HasDataContext); +} diff --git a/x-pack/plugins/observability/public/hooks/use_route_params.tsx b/x-pack/plugins/observability/public/hooks/use_route_params.tsx index 1b32933eec3e6..e429733ddc280 100644 --- a/x-pack/plugins/observability/public/hooks/use_route_params.tsx +++ b/x-pack/plugins/observability/public/hooks/use_route_params.tsx @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { useLocation, useParams } from 'react-router-dom'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { Params } from '../routes'; +import { Params, RouteParams, routes } from '../routes'; function getQueryParams(location: ReturnType) { const urlSearchParms = new URLSearchParams(location.search); @@ -23,14 +23,15 @@ function getQueryParams(location: ReturnType) { * It removes any aditional item which is not declared in the type. * @param params */ -export function useRouteParams(params: Params) { +export function useRouteParams(pathName: T): RouteParams { const location = useLocation(); const pathParams = useParams(); const queryParams = getQueryParams(location); + const { query, path } = routes[pathName].params as Params; const rts = { - queryRt: params.query ? t.exact(params.query) : t.strict({}), - pathRt: params.path ? t.exact(params.path) : t.strict({}), + queryRt: query ? t.exact(query) : t.strict({}), + pathRt: path ? t.exact(path) : t.strict({}), }; const queryResult = rts.queryRt.decode(queryParams); @@ -46,5 +47,5 @@ export function useRouteParams(params: Params) { return { query: isLeft(queryResult) ? {} : queryResult.right, path: isLeft(pathResult) ? {} : pathResult.right, - }; + } as RouteParams; } diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts new file mode 100644 index 0000000000000..f3904cd094cca --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useTimeRange } from './use_time_range'; +import * as pluginContext from './use_plugin_context'; +import { CoreStart } from 'kibana/public'; +import { ObservabilityPluginSetupDeps } from '../plugin'; +import * as kibanaUISettings from './use_kibana_ui_settings'; + +describe('useTimeRange', () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: {} as CoreStart, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({ + from: '2020-10-08T05:00:00.000Z', + to: '2020-10-08T06:00:00.000Z', + })); + }); + describe('when range from and to are provided', () => { + it('returns the same ranges and its absolute time', () => { + const rangeFrom = '2020-10-08T07:00:00.000Z'; + const rangeTo = '2020-10-08T08:00:00.000Z'; + const timeRange = useTimeRange({ rangeFrom, rangeTo }); + expect(timeRange).toEqual({ + rangeFrom, + rangeTo, + absStart: new Date(rangeFrom).valueOf(), + absEnd: new Date(rangeTo).valueOf(), + }); + }); + }); + + describe('when range from and to are not provided', () => { + describe('when data plugin has time set', () => { + it('returns ranges and absolute times from data plugin', () => { + const rangeFrom = '2020-10-08T06:00:00.000Z'; + const rangeTo = '2020-10-08T07:00:00.000Z'; + const timeRange = useTimeRange({}); + expect(timeRange).toEqual({ + rangeFrom, + rangeTo, + absStart: new Date(rangeFrom).valueOf(), + absEnd: new Date(rangeTo).valueOf(), + }); + }); + }); + describe("when data plugin doesn't have time set", () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: {} as CoreStart, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: undefined, + to: undefined, + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); + it('returns ranges and absolute times from kibana default settings', () => { + const rangeFrom = '2020-10-08T05:00:00.000Z'; + const rangeTo = '2020-10-08T06:00:00.000Z'; + const timeRange = useTimeRange({}); + expect(timeRange).toEqual({ + rangeFrom, + rangeTo, + absStart: new Date(rangeFrom).valueOf(), + absEnd: new Date(rangeTo).valueOf(), + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.ts b/x-pack/plugins/observability/public/hooks/use_time_range.ts new file mode 100644 index 0000000000000..c6aff35b61d5e --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_time_range.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +import { TimePickerTime } from '../components/shared/data_picker'; +import { getAbsoluteTime } from '../utils/date'; +import { useKibanaUISettings, UI_SETTINGS } from './use_kibana_ui_settings'; +import { usePluginContext } from './use_plugin_context'; + +export function useTimeRange({ rangeFrom, rangeTo }: { rangeFrom?: string; rangeTo?: string }) { + const { plugins } = usePluginContext(); + const timePickerTimeDefaults = useKibanaUISettings( + UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS + ); + + const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime(); + + const _rangeFrom = rangeFrom ?? timePickerSharedState.from ?? timePickerTimeDefaults.from; + const _rangeTo = rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to; + + return useMemo( + () => ({ + rangeFrom: _rangeFrom, + rangeTo: _rangeTo, + absStart: getAbsoluteTime(_rangeFrom)!, + absEnd: getAbsoluteTime(_rangeTo, { roundUp: true })!, + }), + [_rangeFrom, _rangeTo] + ); +} diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 77b812dddd327..a199ee2a33ca0 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -5,33 +5,20 @@ */ import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { fetchHasData } from '../../data_handler'; -import { useFetcher } from '../../hooks/use_fetcher'; -import { useQueryParams } from '../../hooks/use_query_params'; +import { useHasData } from '../../hooks/use_has_data'; import { LoadingObservability } from '../overview/loading_observability'; export function HomePage() { const history = useHistory(); - - const { absStart, absEnd } = useQueryParams(); - - const { data = {} } = useFetcher( - () => fetchHasData({ start: absStart, end: absEnd }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const values = Object.values(data); - const hasSomeData = values.length ? values.some((hasData) => hasData) : null; + const { hasAnyData } = useHasData(); useEffect(() => { - if (hasSomeData === true) { + if (hasAnyData === true) { history.push({ pathname: '/overview' }); - } - if (hasSomeData === false) { + } else if (hasAnyData === false) { history.push({ pathname: '/landing' }); } - }, [hasSomeData, history]); + }, [hasAnyData, history]); return ; } diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index dfb335902b7b8..94ba0874502bc 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -11,17 +11,14 @@ import { MetricsSection } from '../../components/app/section/metrics'; import { APMSection } from '../../components/app/section/apm'; import { UptimeSection } from '../../components/app/section/uptime'; import { UXSection } from '../../components/app/section/ux'; -import { - HasDataResponse, - ObservabilityFetchDataPlugins, - UXHasDataResponse, -} from '../../typings/fetch_overview_data'; +import { UXHasDataResponse } from '../../typings/fetch_overview_data'; +import { HasDataMap } from '../../context/has_data_context'; interface Props { bucketSize: string; absoluteTime: { start?: number; end?: number }; relativeTime: { start: string; end: string }; - hasData: Record; + hasData?: Partial; } export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime }: Props) { @@ -67,7 +64,7 @@ export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime } {hasData?.ux && ( ; @@ -37,27 +36,17 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) { } export function OverviewPage({ routeParams }: Props) { - const { core, plugins } = usePluginContext(); - - // read time from state and update the url - const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime(); - - const timePickerDefaults = useKibanaUISettings( - UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS - ); - - const relativeTime = { - start: routeParams.query.rangeFrom || timePickerSharedState.from || timePickerDefaults.from, - end: routeParams.query.rangeTo || timePickerSharedState.to || timePickerDefaults.to, - }; - - const absoluteTime = { - start: getAbsoluteTime(relativeTime.start) as number, - end: getAbsoluteTime(relativeTime.end, { roundUp: true }) as number, - }; - useTrackPageview({ app: 'observability', path: 'overview' }); useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); + const { core } = usePluginContext(); + + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange({ + rangeFrom: routeParams.query.rangeFrom, + rangeTo: routeParams.query.rangeTo, + }); + + const relativeTime = { start: rangeFrom, end: rangeTo }; + const absoluteTime = { start: absStart, end: absEnd }; const { data: alerts = [], status: alertStatus } = useFetcher(() => { return getObservabilityAlerts({ core }); @@ -67,14 +56,9 @@ export function OverviewPage({ routeParams }: Props) { const theme = useContext(ThemeContext); - const result = useFetcher( - () => fetchHasData(absoluteTime), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const hasData = result.data; + const { hasData, hasAnyData } = useHasData(); - if (!hasData) { + if (hasAnyData === undefined) { return ; } @@ -89,7 +73,7 @@ export function OverviewPage({ routeParams }: Props) { if (id === 'alert') { return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; } - return !hasData[id]; + return hasData[id]?.hasData === false; }); // Hides the data section when all 'hasData' is false or undefined diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index bee6a4dd7133a..cc97dfe1f4de9 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -28,7 +28,10 @@ export const routes = { handler: () => { return ; }, - params: {}, + params: { + query: t.partial({}), + path: t.partial({}), + }, breadcrumb: [ { text: i18n.translate('xpack.observability.home.breadcrumb', { @@ -41,7 +44,10 @@ export const routes = { handler: () => { return ; }, - params: {}, + params: { + query: t.partial({}), + path: t.partial({}), + }, breadcrumb: [ { text: i18n.translate('xpack.observability.landing.breadcrumb', { @@ -51,8 +57,8 @@ export const routes = { ], }, '/overview': { - handler: ({ query }: any) => { - return ; + handler: ({ query, path }: any) => { + return ; }, params: { query: t.partial({ @@ -61,6 +67,7 @@ export const routes = { refreshPaused: jsonRt.pipe(t.boolean), refreshInterval: jsonRt.pipe(t.number), }), + path: t.partial({}), }, breadcrumb: [ { diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index a64e6fc55b85a..2380d7614f78c 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -37,13 +37,13 @@ export interface UXHasDataResponse { serviceName: string | number | undefined; } -export type HasDataResponse = UXHasDataResponse | boolean; - export type FetchData = ( fetchDataParams: FetchDataParams ) => Promise; -export type HasData = (params?: HasDataParams) => Promise; +export type HasData = ( + params?: HasDataParams +) => Promise; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, @@ -54,7 +54,7 @@ export interface DataHandler< T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins > { fetchData: FetchData; - hasData: HasData; + hasData: HasData; } export interface FetchDataResponse { @@ -113,3 +113,11 @@ export interface ObservabilityFetchDataResponse { uptime: UptimeFetchDataResponse; ux: UxFetchDataResponse; } + +export interface ObservabilityHasDataResponse { + apm: boolean; + infra_metrics: boolean; + infra_logs: boolean; + uptime: boolean; + ux: UXHasDataResponse; +} From 5afc4849ed19e0046f31f5869d7f0d40a0c3c3bf Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 15 Oct 2020 14:32:00 +0200 Subject: [PATCH 02/13] fixing unit tests --- .../public/application/application.test.tsx | 7 + .../public/context/has_data_context.test.tsx | 2 +- .../public/context/has_data_context.tsx | 16 -- .../observability/public/data_handler.test.ts | 213 +----------------- .../public/hooks/use_time_range.ts | 17 +- .../public/pages/overview/data_sections.tsx | 10 +- 6 files changed, 20 insertions(+), 245 deletions(-) diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 85489525cc306..42bbf03889da1 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -14,6 +14,13 @@ describe('renderApp', () => { it('renders', async () => { const plugins = ({ usageCollection: { reportUiStats: () => {} }, + data: { + query: { + timefilter: { + timefilter: { setTime: jest.fn(), getTime: jest.fn().mockImplementation(() => ({})) }, + }, + }, + }, } as unknown) as ObservabilityPluginSetupDeps; const core = ({ application: { currentAppId$: new Observable(), navigateToUrl: () => {} }, diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 75ebdc073481e..4a61a1c648ef7 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -58,7 +58,7 @@ describe('HasDataContextProvider', () => { }); describe('when no plugin has registered', () => { - it.only('hasAnyData returns false and all apps return undefined', async () => { + it('hasAnyData returns false and all apps return undefined', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 799389f98c10d..893a5914b4db2 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -84,21 +84,5 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode hasAnyData = false; } - const _hasData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).reduce< - Partial - >((acc, app) => { - const data = hasData[app]; - if (!data) { - return acc; - } - if (app === 'ux') { - acc[app] = data.hasData as UXHasDataResponse; - } else { - acc[app] = data.hasData as boolean; - } - return acc; - }, {}); - - console.log('### caue: HasDataContextProvider -> _hasData', _hasData); return ; } diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index dae2f62777d30..e7bc105e486c1 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -3,20 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - registerDataHandler, - getDataHandler, - unregisterDataHandler, - fetchHasData, -} from './data_handler'; +import { registerDataHandler, getDataHandler } from './data_handler'; import moment from 'moment'; -import { - ApmFetchDataResponse, - LogsFetchDataResponse, - MetricsFetchDataResponse, - UptimeFetchDataResponse, - UxFetchDataResponse, -} from './typings'; const params = { absoluteTime: { @@ -445,203 +433,4 @@ describe('registerDataHandler', () => { expect(hasData).toBeTruthy(); }); }); - describe('fetchHasData', () => { - it('returns false when an exception happens', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - it('returns true when has data and false when an exception happens', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => { - throw new Error('BOOM'); - }, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: true, - uptime: false, - infra_logs: true, - infra_metrics: false, - ux: false, - }); - }); - it('returns true when has data', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => true, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => ({ - hasData: true, - serviceName: 'elastic-co', - }), - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: true, - uptime: true, - infra_logs: true, - infra_metrics: true, - ux: { - hasData: true, - serviceName: 'elastic-co', - }, - }); - }); - it('returns false when has no data', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - registerDataHandler({ - appName: 'apm', - fetchData: async () => (({} as unknown) as ApmFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'infra_logs', - fetchData: async () => (({} as unknown) as LogsFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'infra_metrics', - fetchData: async () => (({} as unknown) as MetricsFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'uptime', - fetchData: async () => (({} as unknown) as UptimeFetchDataResponse), - hasData: async () => false, - }); - registerDataHandler({ - appName: 'ux', - fetchData: async () => (({} as unknown) as UxFetchDataResponse), - hasData: async () => false, - }); - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - it('returns false when has data was not registered', async () => { - unregisterDataHandler({ appName: 'apm' }); - unregisterDataHandler({ appName: 'infra_logs' }); - unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'uptime' }); - unregisterDataHandler({ appName: 'ux' }); - - expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({ - apm: false, - uptime: false, - infra_logs: false, - infra_metrics: false, - ux: false, - }); - }); - }); }); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.ts b/x-pack/plugins/observability/public/hooks/use_time_range.ts index c6aff35b61d5e..16e946f5f1e69 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useMemo } from 'react'; - import { TimePickerTime } from '../components/shared/data_picker'; import { getAbsoluteTime } from '../utils/date'; import { useKibanaUISettings, UI_SETTINGS } from './use_kibana_ui_settings'; @@ -22,13 +20,10 @@ export function useTimeRange({ rangeFrom, rangeTo }: { rangeFrom?: string; range const _rangeFrom = rangeFrom ?? timePickerSharedState.from ?? timePickerTimeDefaults.from; const _rangeTo = rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to; - return useMemo( - () => ({ - rangeFrom: _rangeFrom, - rangeTo: _rangeTo, - absStart: getAbsoluteTime(_rangeFrom)!, - absEnd: getAbsoluteTime(_rangeTo, { roundUp: true })!, - }), - [_rangeFrom, _rangeTo] - ); + return { + rangeFrom: _rangeFrom, + rangeTo: _rangeTo, + absStart: getAbsoluteTime(_rangeFrom)!, + absEnd: getAbsoluteTime(_rangeTo, { roundUp: true })!, + }; } diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index 94ba0874502bc..7ae847d6c8223 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -25,7 +25,7 @@ export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime } return ( - {hasData?.infra_logs && ( + {hasData?.infra_logs?.hasData && ( )} - {hasData?.infra_metrics && ( + {hasData?.infra_metrics?.hasData && ( )} - {hasData?.apm && ( + {hasData?.apm?.hasData && ( )} - {hasData?.uptime && ( + {hasData?.uptime?.hasData && ( )} - {hasData?.ux && ( + {hasData?.ux?.hasData && ( Date: Thu, 15 Oct 2020 14:46:11 +0200 Subject: [PATCH 03/13] fixing ts issues --- .../app/RumDashboard/ux_overview_fetchers.ts | 5 ++++- .../public/utils/logs_overview_fetchers.ts | 11 ++--------- .../public/context/has_data_context.tsx | 6 +----- .../public/pages/overview/overview.stories.tsx | 18 ++++++++++++++++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts index a9f2486a3c288..a8fca26a72d48 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts @@ -8,6 +8,7 @@ import { FetchDataParams, HasDataParams, UxFetchDataResponse, + UXHasDataResponse, } from '../../../../../observability/public/'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -35,7 +36,9 @@ export const fetchUxOverviewDate = async ({ }; }; -export async function hasRumData({ absoluteTime }: HasDataParams) { +export async function hasRumData({ + absoluteTime, +}: HasDataParams): Promise { return await callApmApi({ pathname: '/api/apm/observability_overview/has_rum_data', params: { diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 9ca6db40a3054..32812f19a2541 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -6,12 +6,7 @@ import { encode } from 'rison-node'; import { SearchResponse } from 'elasticsearch'; -import { - FetchData, - FetchDataParams, - HasData, - LogsFetchDataResponse, -} from '../../../observability/public'; +import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; import { DEFAULT_SOURCE_ID } from '../../common/constants'; import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; @@ -38,9 +33,7 @@ interface LogParams { type StatsAndSeries = Pick; -export function getLogsHasDataFetcher( - getStartServices: InfraClientCoreSetup['getStartServices'] -): HasData { +export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) { return async () => { const [core] = await getStartServices(); const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch); diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 893a5914b4db2..e99b30e669b44 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -10,11 +10,7 @@ import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; import { useRouteParams } from '../hooks/use_route_params'; import { useTimeRange } from '../hooks/use_time_range'; -import { - ObservabilityFetchDataPlugins, - UXHasDataResponse, - ObservabilityHasDataResponse, -} from '../typings/fetch_overview_data'; +import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data'; export type HasDataMap = Record< ObservabilityFetchDataPlugins, diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 608a5e3100276..947f7b56245d5 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -10,6 +10,7 @@ import { CoreStart } from 'kibana/public'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; +import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; import { ObservabilityPluginSetupDeps } from '../../plugin'; @@ -49,7 +50,9 @@ const withCore = makeDecorator({ } as unknown) as ObservabilityPluginSetupDeps, }} > - {storyFn(context)} + + {storyFn(context)} + ); @@ -183,7 +186,7 @@ storiesOf('app/Overview', module) hasData: async () => false, }); - return ; + return ; }) .add('Single Panel', () => { registerDataHandler({ @@ -196,6 +199,7 @@ storiesOf('app/Overview', module) ); @@ -216,6 +220,7 @@ storiesOf('app/Overview', module) ); @@ -238,6 +243,7 @@ storiesOf('app/Overview', module) ); @@ -267,6 +273,7 @@ storiesOf('app/Overview', module) ); @@ -299,6 +306,7 @@ storiesOf('app/Overview', module) ); @@ -331,6 +339,7 @@ storiesOf('app/Overview', module) ); @@ -364,6 +373,7 @@ storiesOf('app/Overview', module) ); @@ -396,6 +406,7 @@ storiesOf('app/Overview', module) ); @@ -435,6 +446,7 @@ storiesOf('app/Overview', module) ); @@ -480,6 +492,7 @@ storiesOf('app/Overview', module) ); @@ -524,6 +537,7 @@ storiesOf('app/Overview', module) ); From f35e2c09c1b9ad6c66d6138faec16171ba57e8f5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 15 Oct 2020 15:34:29 +0200 Subject: [PATCH 04/13] fixing empty state --- .../public/context/has_data_context.tsx | 57 +++++++++++-------- .../public/pages/overview/index.tsx | 2 +- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index e99b30e669b44..67313b40d07d7 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -38,35 +38,42 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [hasData, setHasData] = useState({}); - useEffect(() => { - apps.forEach(async (app) => { - try { - const params = - app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; + useEffect( + () => { + apps.forEach(async (app) => { + try { + const params = + app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; - const result = await getDataHandler(app)?.hasData(params); - setHasData((prevState) => ({ - ...prevState, - [app]: { - hasData: result, - status: FETCH_STATUS.SUCCESS, - }, - })); - } catch (e) { - setHasData((prevState) => ({ - ...prevState, - [app]: { - hasData: undefined, - status: FETCH_STATUS.FAILURE, - }, - })); - } - }); - }, [absStart, absEnd]); + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } catch (e) { + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); const allRequestCompleted = isEmpty(hasData) ? false - : Object.values(hasData).every((dataContext) => dataContext?.status !== FETCH_STATUS.LOADING); + : apps.every((app) => { + const appStatus = hasData[app]?.status; + return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; + }); const hasSomeData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( (app) => hasData[app]?.hasData === true diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 04e5f20ea4d9b..ec4f08bca1d6b 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -73,7 +73,7 @@ export function OverviewPage({ routeParams }: Props) { if (id === 'alert') { return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; } - return hasData[id]?.hasData === false; + return hasData[id]?.status === FETCH_STATUS.FAILURE || hasData[id]?.hasData === false; }); // Hides the data section when all 'hasData' is false or undefined From d37c788f013d2eb5d0767f3ce3a904dee0656494 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Oct 2020 11:58:52 +0200 Subject: [PATCH 05/13] addressing pr comments --- .../public/context/has_data_context.test.tsx | 32 +++++++++---- .../public/context/has_data_context.tsx | 31 ++++++------- .../public/pages/home/index.test.tsx | 45 +++++++++++++++++++ .../observability/public/pages/home/index.tsx | 6 +-- 4 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/home/index.test.tsx diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 4a61a1c648ef7..bde039796b835 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -62,7 +62,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -76,6 +77,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: undefined, status: 'success' }, }, hasAnyData: false, + isAllRequestsComplete: true, }); }); @@ -97,7 +99,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -111,6 +114,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, }, hasAnyData: false, + isAllRequestsComplete: true, }); }); }); @@ -132,7 +136,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -146,6 +151,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, }, hasAnyData: true, + isAllRequestsComplete: true, }); }); }); @@ -167,7 +173,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -181,6 +188,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, }, hasAnyData: true, + isAllRequestsComplete: true, }); }); }); @@ -199,7 +207,8 @@ describe('HasDataContextProvider', () => { }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -213,6 +222,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: undefined, status: 'success' }, }, hasAnyData: true, + isAllRequestsComplete: true, }); }); }); @@ -230,7 +240,8 @@ describe('HasDataContextProvider', () => { }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -244,6 +255,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: undefined, status: 'success' }, }, hasAnyData: false, + isAllRequestsComplete: true, }); }); }); @@ -271,7 +283,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -285,6 +298,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, }, hasAnyData: true, + isAllRequestsComplete: true, }); }); }); @@ -331,7 +345,8 @@ describe('HasDataContextProvider', () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ hasData: {}, - hasAnyData: undefined, + hasAnyData: false, + isAllRequestsComplete: false, }); await waitForNextUpdate(); @@ -345,6 +360,7 @@ describe('HasDataContextProvider', () => { ux: { hasData: undefined, status: 'failure' }, }, hasAnyData: false, + isAllRequestsComplete: true, }); }); }); diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 67313b40d07d7..4455e0e9c1437 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; @@ -19,7 +18,8 @@ export type HasDataMap = Record< export interface HasDataContextValue { hasData: Partial; - hasAnyData: boolean | undefined; + hasAnyData: boolean; + isAllRequestsComplete: boolean; } export const HasDataContext = createContext({} as HasDataContextValue); @@ -68,24 +68,19 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode [] ); - const allRequestCompleted = isEmpty(hasData) - ? false - : apps.every((app) => { - const appStatus = hasData[app]?.status; - return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; - }); + const isAllRequestsComplete = apps.every((app) => { + const appStatus = hasData[app]?.status; + return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; + }); - const hasSomeData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( + const hasAnyData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( (app) => hasData[app]?.hasData === true ); - let hasAnyData; - if (hasSomeData === true) { - hasAnyData = true; - // Waits until all requests are complete to set hasAnyData to false - } else if (hasSomeData === false && allRequestCompleted === true) { - hasAnyData = false; - } - - return ; + return ( + + ); } diff --git a/x-pack/plugins/observability/public/pages/home/index.test.tsx b/x-pack/plugins/observability/public/pages/home/index.test.tsx new file mode 100644 index 0000000000000..0507bc4642093 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/home/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import * as hasData from '../../hooks/use_has_data'; +import { render } from '../../utils/test_helper'; +import { HomePage } from './'; + +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +describe('Home page', () => { + beforeAll(() => { + jest.restoreAllMocks(); + }); + + it('renders loading component while requests are not returned', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation(() => ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false })); + const { getByText } = render(); + expect(getByText('Loading Observability')).toBeInTheDocument(); + }); + it('renders landing page', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation(() => ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true })); + render(); + expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' }); + }); + it('renders overview page', () => { + jest + .spyOn(hasData, 'useHasData') + .mockImplementation(() => ({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false })); + render(); + expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index a199ee2a33ca0..a2a7cad1d5620 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -10,15 +10,15 @@ import { LoadingObservability } from '../overview/loading_observability'; export function HomePage() { const history = useHistory(); - const { hasAnyData } = useHasData(); + const { hasAnyData, isAllRequestsComplete } = useHasData(); useEffect(() => { if (hasAnyData === true) { history.push({ pathname: '/overview' }); - } else if (hasAnyData === false) { + } else if (hasAnyData === false && isAllRequestsComplete === true) { history.push({ pathname: '/landing' }); } - }, [hasAnyData, history]); + }, [hasAnyData, isAllRequestsComplete, history]); return ; } From e5b149b38b846bfd11bf4b7e0c5a3a3c5a69156d Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Oct 2020 13:27:15 +0200 Subject: [PATCH 06/13] addressing pr comments --- .../public/hooks/use_route_params.tsx | 4 ++-- .../plugins/observability/public/routes/index.tsx | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/observability/public/hooks/use_route_params.tsx b/x-pack/plugins/observability/public/hooks/use_route_params.tsx index e429733ddc280..9774d9bed4244 100644 --- a/x-pack/plugins/observability/public/hooks/use_route_params.tsx +++ b/x-pack/plugins/observability/public/hooks/use_route_params.tsx @@ -44,8 +44,8 @@ export function useRouteParams(pathName: T): Rout console.error(PathReporter.report(pathResult)[0]); } - return { + return ({ query: isLeft(queryResult) ? {} : queryResult.right, path: isLeft(pathResult) ? {} : pathResult.right, - } as RouteParams; + } as unknown) as RouteParams; } diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index cc97dfe1f4de9..bee6a4dd7133a 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -28,10 +28,7 @@ export const routes = { handler: () => { return ; }, - params: { - query: t.partial({}), - path: t.partial({}), - }, + params: {}, breadcrumb: [ { text: i18n.translate('xpack.observability.home.breadcrumb', { @@ -44,10 +41,7 @@ export const routes = { handler: () => { return ; }, - params: { - query: t.partial({}), - path: t.partial({}), - }, + params: {}, breadcrumb: [ { text: i18n.translate('xpack.observability.landing.breadcrumb', { @@ -57,8 +51,8 @@ export const routes = { ], }, '/overview': { - handler: ({ query, path }: any) => { - return ; + handler: ({ query }: any) => { + return ; }, params: { query: t.partial({ @@ -67,7 +61,6 @@ export const routes = { refreshPaused: jsonRt.pipe(t.boolean), refreshInterval: jsonRt.pipe(t.number), }), - path: t.partial({}), }, breadcrumb: [ { From af22249e78090cc26426c6ec0fe330e5248a7bb5 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 19 Oct 2020 14:06:37 +0200 Subject: [PATCH 07/13] fixing TS issue --- .../observability/public/application/index.tsx | 4 ++-- .../public/pages/overview/overview.stories.tsx | 13 +------------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 3fb71f5d837c2..26992aec7d921 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -46,8 +46,8 @@ function App() { core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb)); }, [core, breadcrumb]); - const { query, path: pathParams } = useRouteParams(path); - return route.handler({ query, path: pathParams }); + const params = useRouteParams(path); + return route.handler(params); }; return ; })} diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 947f7b56245d5..44e5dea42a143 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -186,7 +186,7 @@ storiesOf('app/Overview', module) hasData: async () => false, }); - return ; + return ; }) .add('Single Panel', () => { registerDataHandler({ @@ -199,7 +199,6 @@ storiesOf('app/Overview', module) ); @@ -220,7 +219,6 @@ storiesOf('app/Overview', module) ); @@ -243,7 +241,6 @@ storiesOf('app/Overview', module) ); @@ -273,7 +270,6 @@ storiesOf('app/Overview', module) ); @@ -306,7 +302,6 @@ storiesOf('app/Overview', module) ); @@ -339,7 +334,6 @@ storiesOf('app/Overview', module) ); @@ -373,7 +367,6 @@ storiesOf('app/Overview', module) ); @@ -406,7 +399,6 @@ storiesOf('app/Overview', module) ); @@ -446,7 +438,6 @@ storiesOf('app/Overview', module) ); @@ -492,7 +483,6 @@ storiesOf('app/Overview', module) ); @@ -537,7 +527,6 @@ storiesOf('app/Overview', module) ); From 7ba1593c324b9f3f8ca9314070678ca8b6eea33a Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 2 Nov 2020 14:17:11 -0300 Subject: [PATCH 08/13] fixing some stuff --- .../public/pages/overview/data_sections.tsx | 4 +-- .../public/pages/overview/index.tsx | 25 ++++++++++--------- .../services/get_observability_alerts.ts | 4 +-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index ff4a6e2be60ce..4320e63f3678c 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -61,10 +61,10 @@ export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime } /> )} - {(hasData.ux as UXHasDataResponse).hasData && ( + {(hasData?.ux?.hasData as UXHasDataResponse)?.hasData && ( ; @@ -74,16 +71,20 @@ export function OverviewPage({ routeParams }: Props) { const appEmptySections = getEmptySections({ core }).filter(({ id }) => { if (id === 'alert') { - return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; - } else if (id === 'ux') { - return !(hasData[id] as UXHasDataResponse).hasData; + return ( + alertStatus === FETCH_STATUS.FAILURE || + (alertStatus === FETCH_STATUS.SUCCESS && alerts.length === 0) + ); + } else { + const app = hasData[id]; + if (app) { + const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData; + return app.status === FETCH_STATUS.FAILURE || !_hasData; + } } - return hasData[id]?.status === FETCH_STATUS.FAILURE || hasData[id]?.hasData === false; + return false; }); - // Hides the data section when all 'hasData' is false or undefined - const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData); - return ( {/* Data sections */} - {showDataSections && ( + {hasAnyData && ( Date: Thu, 5 Nov 2020 09:29:42 -0300 Subject: [PATCH 09/13] refactoring --- .../empty_section.test.tsx} | 2 +- .../empty_section.tsx} | 0 .../components/app/empty_sections/index.tsx | 64 ++++++++++++++ .../components/app/section/apm/index.tsx | 43 ++++++---- .../components/app/section/logs/index.tsx | 49 ++++++----- .../components/app/section/metrics/index.tsx | 36 +++++--- .../components/app/section/uptime/index.tsx | 43 ++++++---- .../components/app/section/ux/index.tsx | 44 ++++++---- .../components/shared/data_picker/index.tsx | 3 + .../public/context/has_data_context.tsx | 83 ++++++++++++++----- .../public/hooks/use_time_range.ts | 19 ++++- .../public/pages/overview/data_sections.tsx | 70 ++++------------ .../public/pages/overview/index.tsx | 79 +++--------------- .../services/get_observability_alerts.test.ts | 10 +-- .../services/get_observability_alerts.ts | 2 +- 15 files changed, 313 insertions(+), 234 deletions(-) rename x-pack/plugins/observability/public/components/app/{empty_section/index.test.tsx => empty_sections/empty_section.test.tsx} (96%) rename x-pack/plugins/observability/public/components/app/{empty_section/index.tsx => empty_sections/empty_section.tsx} (100%) create mode 100644 x-pack/plugins/observability/public/components/app/empty_sections/index.tsx diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx similarity index 96% rename from x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx rename to x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx index 6a05749df6d7a..22867dde83a00 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { ISection } from '../../../typings/section'; import { render } from '../../../utils/test_helper'; -import { EmptySection } from './'; +import { EmptySection } from './empty_section'; describe('EmptySection', () => { it('renders without action button', () => { diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/empty_section.tsx similarity index 100% rename from x-pack/plugins/observability/public/components/app/empty_section/index.tsx rename to x-pack/plugins/observability/public/components/app/empty_sections/empty_section.tsx diff --git a/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx new file mode 100644 index 0000000000000..34522ef95e27b --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { Alert } from '../../../../../alerts/common'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useHasData } from '../../../hooks/use_has_data'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; +import { getEmptySections } from '../../../pages/overview/empty_section'; +import { UXHasDataResponse } from '../../../typings'; +import { EmptySection } from './empty_section'; + +export function EmptySections() { + const { core } = usePluginContext(); + const theme = useContext(ThemeContext); + const { hasData } = useHasData(); + + const appEmptySections = getEmptySections({ core }).filter(({ id }) => { + if (id === 'alert') { + const { status, hasData: alerts } = hasData.alert || {}; + return ( + status === FETCH_STATUS.FAILURE || + (status === FETCH_STATUS.SUCCESS && (alerts as Alert[]).length === 0) + ); + } else { + const app = hasData[id]; + if (app) { + const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData; + return app.status === FETCH_STATUS.FAILURE || !_hasData; + } + } + return false; + }); + return ( + + + 2 ? 2 : 1 + } + gutterSize="s" + > + {appEmptySections.map((app) => { + return ( + + + + ); + })} + + + ); +} diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index b635c2c68b926..b48b63accea83 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -12,17 +12,17 @@ import moment from 'moment'; import React, { useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { ThemeContext } from 'styled-components'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -30,25 +30,36 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } -export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const chartTheme = useChartTheme(); const history = useHistory(); + const { forceUpdate, hasData } = useHasData(); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('apm')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('apm')?.fetchData({ + absoluteTime: { start: absStart, end: absEnd }, + relativeTime: { start: rangeFrom, end: rangeTo }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, rangeFrom, rangeTo, forceUpdate] + ); + + if (!hasData.apm?.hasData) { + return null; + } const { appLink, stats, series } = data || {}; - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absStart).valueOf(); + const max = moment.utc(absEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -93,7 +104,7 @@ export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend={false} xDomain={{ min, max }} /> diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index 343611294bc45..7adbbbf2a6859 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -5,19 +5,19 @@ */ import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind, EuiSpacer, EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import moment from 'moment'; import React, { Fragment } from 'react'; import { useHistory } from 'react-router-dom'; -import { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer } from '@elastic/eui'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { LogsFetchDataResponse } from '../../../../typings'; import { formatStatValue } from '../../../../utils/format_stat_value'; import { ChartContainer } from '../../chart_container'; @@ -25,8 +25,6 @@ import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -45,22 +43,33 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) { return colorsPerItem; } -export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function LogsSection({ bucketSize }: Props) { const history = useHistory(); + const chartTheme = useChartTheme(); + const { forceUpdate, hasData } = useHasData(); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('infra_logs')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('infra_logs')?.fetchData({ + absoluteTime: { start: absStart, end: absEnd }, + relativeTime: { start: rangeFrom, end: rangeTo }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, rangeFrom, rangeTo, forceUpdate] + ); + + if (!hasData.infra_logs?.hasData) { + return null; + } - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absStart).valueOf(); + const max = moment.utc(absEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -115,7 +124,7 @@ export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend legendPosition={Position.Right} xDomain={{ min, max }} diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index 8bce8205902fa..a2c3838a42983 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -13,13 +13,13 @@ import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { Series } from '../../../../typings'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } @@ -46,19 +46,29 @@ const StyledProgress = styled.div<{ color?: string }>` } `; -export function MetricsSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function MetricsSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const { forceUpdate, hasData } = useHasData(); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('infra_metrics')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('infra_metrics')?.fetchData({ + absoluteTime: { start: absStart, end: absEnd }, + relativeTime: { start: rangeFrom, end: rangeTo }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, rangeFrom, rangeTo, forceUpdate] + ); + + if (!hasData.infra_metrics?.hasData) { + return null; + } const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 879d745ff2b64..54b9ddbc6dc9b 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -24,34 +24,45 @@ import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { useChartTheme } from '../../../../hooks/use_chart_theme'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; import { Series } from '../../../../typings'; import { ChartContainer } from '../../chart_container'; import { StyledStat } from '../../styled_stat'; import { onBrushEnd } from '../helper'; interface Props { - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; bucketSize?: string; } -export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) { +export function UptimeSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); + const chartTheme = useChartTheme(); const history = useHistory(); + const { forceUpdate, hasData } = useHasData(); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); - const { start, end } = absoluteTime; - const { data, status } = useFetcher(() => { - if (start && end && bucketSize) { - return getDataHandler('uptime')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - bucketSize, - }); - } - }, [start, end, bucketSize, relativeTime]); + const { data, status } = useFetcher( + () => { + if (bucketSize) { + return getDataHandler('uptime')?.fetchData({ + absoluteTime: { start: absStart, end: absEnd }, + relativeTime: { start: rangeFrom, end: rangeTo }, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, rangeFrom, rangeTo, forceUpdate] + ); + + if (!hasData.uptime?.hasData) { + return null; + } - const min = moment.utc(absoluteTime.start).valueOf(); - const max = moment.utc(absoluteTime.end).valueOf(); + const min = moment.utc(absStart).valueOf(); + const max = moment.utc(absEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); @@ -112,7 +123,7 @@ export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) onBrushEnd({ x, history })} - theme={useChartTheme()} + theme={chartTheme} showLegend={false} legendPosition={Position.Right} xDomain={{ min, max }} diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 0c40ce0bf7a2e..dc4eeabbc998a 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -9,28 +9,40 @@ import React from 'react'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useTimeRange } from '../../../../hooks/use_time_range'; +import { UXHasDataResponse } from '../../../../typings'; import { CoreVitals } from '../../../shared/core_web_vitals'; interface Props { - serviceName: string; bucketSize: string; - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; } -export function UXSection({ serviceName, bucketSize, absoluteTime, relativeTime }: Props) { - const { start, end } = absoluteTime; - - const { data, status } = useFetcher(() => { - if (start && end) { - return getDataHandler('ux')?.fetchData({ - absoluteTime: { start, end }, - relativeTime, - serviceName, - bucketSize, - }); - } - }, [start, end, relativeTime, serviceName, bucketSize]); +export function UXSection({ bucketSize }: Props) { + const { forceUpdate, hasData } = useHasData(); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {}; + const { serviceName } = uxHasDataResponse; + + const { data, status } = useFetcher( + () => { + if (serviceName && bucketSize) { + return getDataHandler('ux')?.fetchData({ + absoluteTime: { start: absStart, end: absEnd }, + relativeTime: { start: rangeFrom, end: rangeTo }, + serviceName: serviceName as string, + bucketSize, + }); + } + }, + // Absolute times shouldn't be used here, since it would refetch on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + [bucketSize, rangeFrom, rangeTo, forceUpdate, serviceName] + ); + + if (!uxHasDataResponse?.hasData) { + return null; + } const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx index 747ec8a441c42..32c6c6054f775 100644 --- a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx @@ -7,6 +7,7 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { useHasData } from '../../../hooks/use_has_data'; import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings'; import { usePluginContext } from '../../../hooks/use_plugin_context'; import { fromQuery, toQuery } from '../../../utils/url'; @@ -36,6 +37,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval const location = useLocation(); const history = useHistory(); const { plugins } = usePluginContext(); + const { onRefreshTimeRange } = useHasData(); useEffect(() => { plugins.data.query.timefilter.timefilter.setTime({ @@ -81,6 +83,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval function onTimeChange({ start, end }: { start: string; end: string }) { updateUrl({ rangeFrom: start, rangeTo: end }); + onRefreshTimeRange(); } return ( diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 4455e0e9c1437..beee1061c0b8d 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -4,37 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ +import { uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; +import { Alert } from '../../../alerts/common'; import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; -import { useRouteParams } from '../hooks/use_route_params'; +import { usePluginContext } from '../hooks/use_plugin_context'; import { useTimeRange } from '../hooks/use_time_range'; +import { getObservabilityAlerts } from '../services/get_observability_alerts'; import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data'; +type DataContextApps = ObservabilityFetchDataPlugins | 'alert'; + export type HasDataMap = Record< - ObservabilityFetchDataPlugins, - { status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse } + DataContextApps, + { status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse | Alert[] } >; export interface HasDataContextValue { hasData: Partial; hasAnyData: boolean; isAllRequestsComplete: boolean; + onRefreshTimeRange: () => void; + forceUpdate: string; } export const HasDataContext = createContext({} as HasDataContextValue); -const apps: ObservabilityFetchDataPlugins[] = [ - 'apm', - 'uptime', - 'infra_logs', - 'infra_metrics', - 'ux', -]; +const apps: DataContextApps[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics', 'ux', 'alert']; export function HasDataContextProvider({ children }: { children: React.ReactNode }) { - const { rangeFrom, rangeTo } = useRouteParams('/overview').query; - const { absStart, absEnd } = useTimeRange({ rangeFrom, rangeTo }); + const { core } = usePluginContext(); + const [forceUpdate, setForceUpdate] = useState(''); + const { absStart, absEnd } = useTimeRange(); const [hasData, setHasData] = useState({}); @@ -42,17 +44,19 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode () => { apps.forEach(async (app) => { try { - const params = - app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; + if (app !== 'alert') { + const params = + app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; - const result = await getDataHandler(app)?.hasData(params); - setHasData((prevState) => ({ - ...prevState, - [app]: { - hasData: result, - status: FETCH_STATUS.SUCCESS, - }, - })); + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } } catch (e) { setHasData((prevState) => ({ ...prevState, @@ -68,6 +72,31 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode [] ); + useEffect(() => { + async function fetchAlerts() { + try { + const alerts = await getObservabilityAlerts({ core }); + setHasData((prevState) => ({ + ...prevState, + alert: { + hasData: alerts, + status: FETCH_STATUS.SUCCESS, + }, + })); + } catch (e) { + setHasData((prevState) => ({ + ...prevState, + alert: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + } + + fetchAlerts(); + }, [forceUpdate, core]); + const isAllRequestsComplete = apps.every((app) => { const appStatus = hasData[app]?.status; return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; @@ -79,7 +108,15 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode return ( { + setForceUpdate(uniqueId()); + }, + }} children={children} /> ); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.ts b/x-pack/plugins/observability/public/hooks/use_time_range.ts index 16e946f5f1e69..7a61b11f9bc04 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.ts @@ -4,21 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; +import { useLocation } from 'react-router-dom'; import { TimePickerTime } from '../components/shared/data_picker'; import { getAbsoluteTime } from '../utils/date'; -import { useKibanaUISettings, UI_SETTINGS } from './use_kibana_ui_settings'; +import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings'; import { usePluginContext } from './use_plugin_context'; -export function useTimeRange({ rangeFrom, rangeTo }: { rangeFrom?: string; rangeTo?: string }) { +const getParsedParams = (search: string) => { + return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {}; +}; + +export function useTimeRange() { const { plugins } = usePluginContext(); + const timePickerTimeDefaults = useKibanaUISettings( UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS ); const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime(); - const _rangeFrom = rangeFrom ?? timePickerSharedState.from ?? timePickerTimeDefaults.from; - const _rangeTo = rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to; + const { rangeFrom, rangeTo } = getParsedParams(useLocation().search); + + const _rangeFrom = (rangeFrom ?? + timePickerSharedState.from ?? + timePickerTimeDefaults.from) as string; + const _rangeTo = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string; return { rangeFrom: _rangeFrom, diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index 4320e63f3678c..f0c56eb7137e2 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -4,73 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { APMSection } from '../../components/app/section/apm'; import { LogsSection } from '../../components/app/section/logs'; import { MetricsSection } from '../../components/app/section/metrics'; -import { APMSection } from '../../components/app/section/apm'; import { UptimeSection } from '../../components/app/section/uptime'; import { UXSection } from '../../components/app/section/ux'; -import { UXHasDataResponse } from '../../typings/fetch_overview_data'; import { HasDataMap } from '../../context/has_data_context'; interface Props { bucketSize: string; - absoluteTime: { start?: number; end?: number }; - relativeTime: { start: string; end: string }; hasData?: Partial; } -export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime }: Props) { +export function DataSections({ bucketSize }: Props) { return ( - {hasData?.infra_logs?.hasData && ( - - - - )} - {hasData?.infra_metrics?.hasData && ( - - - - )} - {hasData?.apm?.hasData && ( - - - - )} - {hasData?.uptime?.hasData && ( - - - - )} - {(hasData?.ux?.hasData as UXHasDataResponse)?.hasData && ( - - - - )} + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 2051744d8a9f2..792e78786e607 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -3,17 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { useTrackPageview, UXHasDataResponse } from '../..'; -import { EmptySection } from '../../components/app/empty_section'; +import { Alert } from '../../../../alerts/common'; +import { useTrackPageview } from '../..'; +import { EmptySections } from '../../components/app/empty_sections'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { NewsFeed } from '../../components/app/news_feed'; import { Resources } from '../../components/app/resources'; import { AlertsSection } from '../../components/app/section/alerts'; import { DatePicker } from '../../components/shared/data_picker'; -import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; +import { useFetcher } from '../../hooks/use_fetcher'; import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeRange } from '../../hooks/use_time_range'; @@ -22,7 +23,6 @@ import { getNewsFeed } from '../../services/get_news_feed'; import { getObservabilityAlerts } from '../../services/get_observability_alerts'; import { getBucketSize } from '../../utils/get_bucket_size'; import { DataSections } from './data_sections'; -import { getEmptySections } from './empty_section'; import { LoadingObservability } from './loading_observability'; interface Props { @@ -39,29 +39,23 @@ export function OverviewPage({ routeParams }: Props) { useTrackPageview({ app: 'observability', path: 'overview' }); useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); const { core } = usePluginContext(); + const theme = useContext(ThemeContext); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange({ - rangeFrom: routeParams.query.rangeFrom, - rangeTo: routeParams.query.rangeTo, - }); + const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); const relativeTime = { start: rangeFrom, end: rangeTo }; const absoluteTime = { start: absStart, end: absEnd }; - const { data: alerts = [], status: alertStatus } = useFetcher(() => { - return getObservabilityAlerts({ core }); - }, [core]); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); - const theme = useContext(ThemeContext); - const { hasData, hasAnyData } = useHasData(); if (hasAnyData === undefined) { return ; } + const alerts = (hasData.alert?.hasData as Alert[]) || []; + const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; const bucketSize = calculateBucketSize({ @@ -69,22 +63,6 @@ export function OverviewPage({ routeParams }: Props) { end: absoluteTime.end, }); - const appEmptySections = getEmptySections({ core }).filter(({ id }) => { - if (id === 'alert') { - return ( - alertStatus === FETCH_STATUS.FAILURE || - (alertStatus === FETCH_STATUS.SUCCESS && alerts.length === 0) - ); - } else { - const app = hasData[id]; - if (app) { - const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData; - return app.status === FETCH_STATUS.FAILURE || !_hasData; - } - } - return false; - }); - return ( {/* Data sections */} - {hasAnyData && ( - - )} - - {/* Empty sections */} - {!!appEmptySections.length && ( - - - 2 ? 2 : 1 - } - gutterSize="s" - > - {appEmptySections.map((app) => { - return ( - - - - ); - })} - - - )} + {hasAnyData && } + + {/* Alert section */} diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts index 64f5f4aab1c2b..57c78955b3f6f 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AppMountContext } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { getObservabilityAlerts } from './get_observability_alerts'; const basePath = { prepend: (path: string) => path }; @@ -27,7 +27,7 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([]); @@ -43,7 +43,7 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([]); @@ -80,7 +80,7 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([]); }); @@ -120,7 +120,7 @@ describe('getObservabilityAlerts', () => { }, basePath, }, - } as unknown) as AppMountContext['core']; + } as unknown) as CoreStart; const alerts = await getObservabilityAlerts({ core }); expect(alerts).toEqual([ diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts index da0aa38df3079..aad609dc82e98 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -21,6 +21,6 @@ export async function getObservabilityAlerts({ core }: { core: CoreStart }) { return data.filter(({ consumer }) => allowedConsumers.includes(consumer)); } catch (e) { console.error('Error while fetching alerts', e); - return []; + throw e; } } From 97be452ad1d94d2d3a102aa03183d66551f2f93c Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 5 Nov 2020 11:22:22 -0300 Subject: [PATCH 10/13] fixing ts issues and unit tests --- .../public/application/application.test.tsx | 9 + .../components/app/section/apm/index.test.tsx | 65 ++- .../components/app/section/ux/index.test.tsx | 78 +-- .../components/app/section/ux/index.tsx | 4 +- .../public/context/has_data_context.test.tsx | 526 +++++++++++------- .../public/hooks/use_time_range.test.ts | 23 +- .../public/pages/home/index.test.tsx | 16 +- .../public/pages/overview/index.tsx | 3 +- .../services/get_observability_alerts.test.ts | 58 +- .../services/get_observability_alerts.ts | 13 +- 10 files changed, 452 insertions(+), 343 deletions(-) diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 42bbf03889da1..bed51fea0bc21 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -11,6 +11,15 @@ import { ObservabilityPluginSetupDeps } from '../plugin'; import { renderApp } from './'; describe('renderApp', () => { + const originalConsole = global.console; + beforeAll(() => { + // mocks console to avoid poluting the test output + global.console = ({ error: jest.fn() } as unknown) as typeof console; + }); + + afterAll(() => { + global.console = originalConsole; + }); it('renders', async () => { const plugins = ({ usageCollection: { reportUiStats: () => {} }, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index 7b9d7276dd1c5..cc2c3f7cd9269 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -8,25 +8,57 @@ import * as fetcherHook from '../../../../hooks/use_fetcher'; import { render } from '../../../../utils/test_helper'; import { APMSection } from './'; import { response } from './mock_data/apm.mock'; -import moment from 'moment'; +import * as hasDataHook from '../../../../hooks/use_has_data'; +import * as pluginContext from '../../../../hooks/use_plugin_context'; +import { HasDataContextValue } from '../../../../context/has_data_context'; +import { CoreStart } from 'kibana/public'; +import { ObservabilityPluginSetupDeps } from '../../../../plugin'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + }), + useHistory: jest.fn(), +})); describe('APMSection', () => { + beforeAll(() => { + jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ + hasData: { + apm: { + status: fetcherHook.FETCH_STATUS.SUCCESS, + hasData: true, + }, + }, + } as HasDataContextValue); + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: ({ + uiSettings: { get: jest.fn() }, + http: { basePath: { prepend: jest.fn() } }, + } as unknown) as CoreStart, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByTestId } = render( - - ); + const { getByText, queryAllByTestId } = render(); expect(getByText('APM')).toBeInTheDocument(); expect(getByText('View in app')).toBeInTheDocument(); @@ -40,16 +72,7 @@ describe('APMSection', () => { status: fetcherHook.FETCH_STATUS.LOADING, refetch: jest.fn(), }); - const { getByText, queryAllByText, getByTestId } = render( - - ); + const { getByText, queryAllByText, getByTestId } = render(); expect(getByText('APM')).toBeInTheDocument(); expect(getByTestId('loading')).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index ef1820eaaeb3e..6527ed0177d7e 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -3,31 +3,61 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { CoreStart } from 'kibana/public'; import React from 'react'; -import moment from 'moment'; +import { HasDataContextValue } from '../../../../context/has_data_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; +import * as hasDataHook from '../../../../hooks/use_has_data'; +import * as pluginContext from '../../../../hooks/use_plugin_context'; +import { ObservabilityPluginSetupDeps } from '../../../../plugin'; import { render } from '../../../../utils/test_helper'; import { UXSection } from './'; import { response } from './mock_data/ux.mock'; +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + }), +})); + describe('UXSection', () => { + beforeAll(() => { + jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ + hasData: { + ux: { + status: fetcherHook.FETCH_STATUS.SUCCESS, + hasData: { hasData: true, serviceName: 'elastic-co-frontend' }, + }, + }, + } as HasDataContextValue); + jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + core: ({ + uiSettings: { get: jest.fn() }, + http: { basePath: { prepend: jest.fn() } }, + } as unknown) as CoreStart, + plugins: ({ + data: { + query: { + timefilter: { + timefilter: { + getTime: jest.fn().mockImplementation(() => ({ + from: '2020-10-08T06:00:00.000Z', + to: '2020-10-08T07:00:00.000Z', + })), + }, + }, + }, + }, + } as unknown) as ObservabilityPluginSetupDeps, + })); + }); it('renders with core web vitals', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, getAllByText } = render( - - ); + const { getByText, getAllByText } = render(); expect(getByText('User Experience')).toBeInTheDocument(); expect(getByText('View in app')).toBeInTheDocument(); @@ -59,17 +89,7 @@ describe('UXSection', () => { status: fetcherHook.FETCH_STATUS.LOADING, refetch: jest.fn(), }); - const { getByText, queryAllByText, getAllByText } = render( - - ); + const { getByText, queryAllByText, getAllByText } = render(); expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('--')).toHaveLength(3); @@ -82,17 +102,7 @@ describe('UXSection', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByText, getAllByText } = render( - - ); + const { getByText, queryAllByText, getAllByText } = render(); expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('No data is available.')).toHaveLength(3); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index dc4eeabbc998a..a7e124b344296 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -22,7 +22,7 @@ export function UXSection({ bucketSize }: Props) { const { forceUpdate, hasData } = useHasData(); const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {}; - const { serviceName } = uxHasDataResponse; + const serviceName = uxHasDataResponse.serviceName as string; const { data, status } = useFetcher( () => { @@ -30,7 +30,7 @@ export function UXSection({ bucketSize }: Props) { return getDataHandler('ux')?.fetchData({ absoluteTime: { start: absStart, end: absEnd }, relativeTime: { start: rangeFrom, end: rangeTo }, - serviceName: serviceName as string, + serviceName, bucketSize, }); } diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index bde039796b835..41052182f10aa 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -5,6 +5,7 @@ */ // import { act, getByText } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import { CoreStart } from 'kibana/public'; import React from 'react'; import { registerDataHandler, unregisterDataHandler } from '../data_handler'; import { useHasData } from '../hooks/use_has_data'; @@ -12,6 +13,8 @@ import * as routeParams from '../hooks/use_route_params'; import * as timeRange from '../hooks/use_time_range'; import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data'; import { HasDataContextProvider } from './has_data_context'; +import * as pluginContext from '../hooks/use_plugin_context'; +import { PluginContextValue } from './plugin_context'; const rangeFrom = '2020-10-08T06:00:00.000Z'; const rangeTo = '2020-10-08T07:00:00.000Z'; @@ -55,15 +58,20 @@ describe('HasDataContextProvider', () => { absStart: new Date(rangeFrom).valueOf(), absEnd: new Date(rangeTo).valueOf(), })); + jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({ + core: ({ http: { get: jest.fn() } } as unknown) as CoreStart, + } as PluginContextValue); }); describe('when no plugin has registered', () => { it('hasAnyData returns false and all apps return undefined', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); - expect(result.current).toEqual({ + expect(result.current).toMatchObject({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); await waitForNextUpdate(); @@ -75,106 +83,160 @@ describe('HasDataContextProvider', () => { infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, }, hasAnyData: false, isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); }); + }); + describe('when plugins have registered', () => { + describe('all apps return false', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => false }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); - describe('when plugins have registered', () => { - describe('all apps return false', () => { - beforeAll(() => { - registerApps([ - { appName: 'apm', hasData: async () => false }, - { appName: 'infra_logs', hasData: async () => false }, - { appName: 'infra_metrics', hasData: async () => false }, - { appName: 'uptime', hasData: async () => false }, - { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, - ]); + afterAll(unregisterAll); + + it('hasAnyData returns false and all apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); - afterAll(unregisterAll); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: false, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); - it('hasAnyData returns false and all apps return false', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); - expect(result.current).toEqual({ - hasData: {}, - hasAnyData: false, - isAllRequestsComplete: false, - }); + describe('at least one app returns true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => false }, + { appName: 'infra_metrics', hasData: async () => false }, + { appName: 'uptime', hasData: async () => false }, + { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, + ]); + }); - await waitForNextUpdate(); + afterAll(unregisterAll); - expect(result.current).toEqual({ - hasData: { - apm: { hasData: false, status: 'success' }, - uptime: { hasData: false, status: 'success' }, - infra_logs: { hasData: false, status: 'success' }, - infra_metrics: { hasData: false, status: 'success' }, - ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, - }, - hasAnyData: false, - isAllRequestsComplete: true, - }); + it('hasAnyData returns true apm returns true and all other apps return false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); - }); - describe('at least one app returns true', () => { - beforeAll(() => { - registerApps([ - { appName: 'apm', hasData: async () => true }, - { appName: 'infra_logs', hasData: async () => false }, - { appName: 'infra_metrics', hasData: async () => false }, - { appName: 'uptime', hasData: async () => false }, - { appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) }, - ]); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: false, status: 'success' }, + infra_logs: { hasData: false, status: 'success' }, + infra_metrics: { hasData: false, status: 'success' }, + ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); + }); + }); - afterAll(unregisterAll); + describe('all apps return true', () => { + beforeAll(() => { + registerApps([ + { appName: 'apm', hasData: async () => true }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); - it('hasAnyData returns true apm returns true and all other apps return false', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); - expect(result.current).toEqual({ - hasData: {}, - hasAnyData: false, - isAllRequestsComplete: false, - }); + afterAll(unregisterAll); - await waitForNextUpdate(); + it('hasAnyData returns true and all apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); - expect(result.current).toEqual({ - hasData: { - apm: { hasData: true, status: 'success' }, - uptime: { hasData: false, status: 'success' }, - infra_logs: { hasData: false, status: 'success' }, - infra_metrics: { hasData: false, status: 'success' }, - ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' }, - }, - hasAnyData: true, - isAllRequestsComplete: true, - }); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: true, status: 'success' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); }); + }); - describe('all apps return true', () => { + describe('only apm is registered', () => { + describe('when apm returns true', () => { beforeAll(() => { - registerApps([ - { appName: 'apm', hasData: async () => true }, - { appName: 'infra_logs', hasData: async () => true }, - { appName: 'infra_metrics', hasData: async () => true }, - { appName: 'uptime', hasData: async () => true }, - { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, - ]); + registerApps([{ appName: 'apm', hasData: async () => true }]); }); afterAll(unregisterAll); - it('hasAnyData returns true and all apps return true', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + it('hasAnyData returns true, apm returns true and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); expect(result.current).toEqual({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); await waitForNextUpdate(); @@ -182,187 +244,223 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasData: { apm: { hasData: true, status: 'success' }, - uptime: { hasData: true, status: 'success' }, - infra_logs: { hasData: true, status: 'success' }, - infra_metrics: { hasData: true, status: 'success' }, - ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, }, hasAnyData: true, isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); }); }); - describe('only apm is registered', () => { - describe('when apm returns true', () => { - beforeAll(() => { - registerApps([{ appName: 'apm', hasData: async () => true }]); - }); - - afterAll(unregisterAll); - - it('hasAnyData returns true, apm returns true and all other apps return undefined', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { - wrapper, - }); - expect(result.current).toEqual({ - hasData: {}, - hasAnyData: false, - isAllRequestsComplete: false, - }); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - hasData: { - apm: { hasData: true, status: 'success' }, - uptime: { hasData: undefined, status: 'success' }, - infra_logs: { hasData: undefined, status: 'success' }, - infra_metrics: { hasData: undefined, status: 'success' }, - ux: { hasData: undefined, status: 'success' }, - }, - hasAnyData: true, - isAllRequestsComplete: true, - }); - }); - }); - - describe('when apm returns false', () => { - beforeAll(() => { - registerApps([{ appName: 'apm', hasData: async () => false }]); - }); - - afterAll(unregisterAll); - - it('hasAnyData returns false, apm returns false and all other apps return undefined', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { - wrapper, - }); - expect(result.current).toEqual({ - hasData: {}, - hasAnyData: false, - isAllRequestsComplete: false, - }); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - hasData: { - apm: { hasData: false, status: 'success' }, - uptime: { hasData: undefined, status: 'success' }, - infra_logs: { hasData: undefined, status: 'success' }, - infra_metrics: { hasData: undefined, status: 'success' }, - ux: { hasData: undefined, status: 'success' }, - }, - hasAnyData: false, - isAllRequestsComplete: true, - }); - }); - }); - }); - - describe('when an app throws an error while fetching', () => { + describe('when apm returns false', () => { beforeAll(() => { - registerApps([ - { - appName: 'apm', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, - }, - { appName: 'infra_logs', hasData: async () => true }, - { appName: 'infra_metrics', hasData: async () => true }, - { appName: 'uptime', hasData: async () => true }, - { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, - ]); + registerApps([{ appName: 'apm', hasData: async () => false }]); }); afterAll(unregisterAll); - it('hasAnyData returns true, apm is undefined and all other apps return true', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + it('hasAnyData returns false, apm returns false and all other apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { + wrapper, + }); expect(result.current).toEqual({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); await waitForNextUpdate(); expect(result.current).toEqual({ hasData: { - apm: { hasData: undefined, status: 'failure' }, - uptime: { hasData: true, status: 'success' }, - infra_logs: { hasData: true, status: 'success' }, - infra_metrics: { hasData: true, status: 'success' }, - ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + apm: { hasData: false, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { hasData: [], status: 'success' }, }, - hasAnyData: true, + hasAnyData: false, isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); }); }); + }); - describe('when all apps throw an error while fetching', () => { - beforeAll(() => { - registerApps([ - { - appName: 'apm', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, + describe('when an app throws an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); }, - { - appName: 'infra_logs', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, + }, + { appName: 'infra_logs', hasData: async () => true }, + { appName: 'infra_metrics', hasData: async () => true }, + { appName: 'uptime', hasData: async () => true }, + { appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) }, + ]); + }); + + afterAll(unregisterAll); + + it('hasAnyData returns true, apm is undefined and all other apps return true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: true, status: 'success' }, + infra_logs: { hasData: true, status: 'success' }, + infra_metrics: { hasData: true, status: 'success' }, + ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: true, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + + describe('when all apps throw an error while fetching', () => { + beforeAll(() => { + registerApps([ + { + appName: 'apm', + hasData: async () => { + throw new Error('BOOMMMMM'); }, - { - appName: 'infra_metrics', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, + }, + { + appName: 'infra_logs', + hasData: async () => { + throw new Error('BOOMMMMM'); }, - { - appName: 'uptime', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, + }, + { + appName: 'infra_metrics', + hasData: async () => { + throw new Error('BOOMMMMM'); }, - { - appName: 'ux', - hasData: async () => { - throw new Error('BOOMMMMM'); - }, + }, + { + appName: 'uptime', + hasData: async () => { + throw new Error('BOOMMMMM'); }, - ]); - }); + }, + { + appName: 'ux', + hasData: async () => { + throw new Error('BOOMMMMM'); + }, + }, + ]); + }); - afterAll(unregisterAll); + afterAll(unregisterAll); - it('hasAnyData returns false and all apps return undefined', async () => { - const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); - expect(result.current).toEqual({ - hasData: {}, - hasAnyData: false, - isAllRequestsComplete: false, - }); + it('hasAnyData returns false and all apps return undefined', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); - await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'failure' }, + uptime: { hasData: undefined, status: 'failure' }, + infra_logs: { hasData: undefined, status: 'failure' }, + infra_metrics: { hasData: undefined, status: 'failure' }, + ux: { hasData: undefined, status: 'failure' }, + alert: { hasData: [], status: 'success' }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + }); + }); + }); - expect(result.current).toEqual({ - hasData: { - apm: { hasData: undefined, status: 'failure' }, - uptime: { hasData: undefined, status: 'failure' }, - infra_logs: { hasData: undefined, status: 'failure' }, - infra_metrics: { hasData: undefined, status: 'failure' }, - ux: { hasData: undefined, status: 'failure' }, + describe('with alerts', () => { + beforeAll(() => { + jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({ + core: ({ + http: { + get: async () => { + return { + data: [ + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + ], + }; }, - hasAnyData: false, - isAllRequestsComplete: true, - }); - }); + }, + } as unknown) as CoreStart, + } as PluginContextValue); + }); + + it('returns all alerts available', async () => { + const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); + expect(result.current).toEqual({ + hasData: {}, + hasAnyData: false, + isAllRequestsComplete: false, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), + }); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + hasData: { + apm: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, + infra_logs: { hasData: undefined, status: 'success' }, + infra_metrics: { hasData: undefined, status: 'success' }, + ux: { hasData: undefined, status: 'success' }, + alert: { + hasData: [ + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + ], + status: 'success', + }, + }, + hasAnyData: false, + isAllRequestsComplete: true, + forceUpdate: expect.any(String), + onRefreshTimeRange: expect.any(Function), }); }); }); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts index f3904cd094cca..993b6834573d3 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -10,6 +10,12 @@ import { CoreStart } from 'kibana/public'; import { ObservabilityPluginSetupDeps } from '../plugin'; import * as kibanaUISettings from './use_kibana_ui_settings'; +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/observability/overview/', + }), +})); + describe('useTimeRange', () => { beforeAll(() => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ @@ -34,26 +40,13 @@ describe('useTimeRange', () => { to: '2020-10-08T06:00:00.000Z', })); }); - describe('when range from and to are provided', () => { - it('returns the same ranges and its absolute time', () => { - const rangeFrom = '2020-10-08T07:00:00.000Z'; - const rangeTo = '2020-10-08T08:00:00.000Z'; - const timeRange = useTimeRange({ rangeFrom, rangeTo }); - expect(timeRange).toEqual({ - rangeFrom, - rangeTo, - absStart: new Date(rangeFrom).valueOf(), - absEnd: new Date(rangeTo).valueOf(), - }); - }); - }); describe('when range from and to are not provided', () => { describe('when data plugin has time set', () => { it('returns ranges and absolute times from data plugin', () => { const rangeFrom = '2020-10-08T06:00:00.000Z'; const rangeTo = '2020-10-08T07:00:00.000Z'; - const timeRange = useTimeRange({}); + const timeRange = useTimeRange(); expect(timeRange).toEqual({ rangeFrom, rangeTo, @@ -85,7 +78,7 @@ describe('useTimeRange', () => { it('returns ranges and absolute times from kibana default settings', () => { const rangeFrom = '2020-10-08T05:00:00.000Z'; const rangeTo = '2020-10-08T06:00:00.000Z'; - const timeRange = useTimeRange({}); + const timeRange = useTimeRange(); expect(timeRange).toEqual({ rangeFrom, rangeTo, diff --git a/x-pack/plugins/observability/public/pages/home/index.test.tsx b/x-pack/plugins/observability/public/pages/home/index.test.tsx index 0507bc4642093..2c06b7035f515 100644 --- a/x-pack/plugins/observability/public/pages/home/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { HasDataContextValue } from '../../context/has_data_context'; import * as hasData from '../../hooks/use_has_data'; import { render } from '../../utils/test_helper'; import { HomePage } from './'; @@ -24,21 +25,30 @@ describe('Home page', () => { it('renders loading component while requests are not returned', () => { jest .spyOn(hasData, 'useHasData') - .mockImplementation(() => ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false })); + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false } as HasDataContextValue) + ); const { getByText } = render(); expect(getByText('Loading Observability')).toBeInTheDocument(); }); it('renders landing page', () => { jest .spyOn(hasData, 'useHasData') - .mockImplementation(() => ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true })); + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true } as HasDataContextValue) + ); render(); expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' }); }); it('renders overview page', () => { jest .spyOn(hasData, 'useHasData') - .mockImplementation(() => ({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false })); + .mockImplementation( + () => + ({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false } as HasDataContextValue) + ); render(); expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' }); }); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 792e78786e607..263313383bd19 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -6,8 +6,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { Alert } from '../../../../alerts/common'; import { useTrackPageview } from '../..'; +import { Alert } from '../../../../alerts/common'; import { EmptySections } from '../../components/app/empty_sections'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { NewsFeed } from '../../components/app/news_feed'; @@ -20,7 +20,6 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeRange } from '../../hooks/use_time_range'; import { RouteParams } from '../../routes'; import { getNewsFeed } from '../../services/get_news_feed'; -import { getObservabilityAlerts } from '../../services/get_observability_alerts'; import { getBucketSize } from '../../utils/get_bucket_size'; import { DataSections } from './data_sections'; import { LoadingObservability } from './loading_observability'; diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts index 57c78955b3f6f..e3f8f877656bd 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts @@ -29,8 +29,7 @@ describe('getObservabilityAlerts', () => { }, } as unknown) as CoreStart; - const alerts = await getObservabilityAlerts({ core }); - expect(alerts).toEqual([]); + expect(getObservabilityAlerts({ core })).rejects.toThrow('Boom'); }); it('Returns empty array when api return undefined', async () => { @@ -55,26 +54,11 @@ describe('getObservabilityAlerts', () => { get: async () => { return { data: [ - { - id: 1, - consumer: 'siem', - }, - { - id: 2, - consumer: 'kibana', - }, - { - id: 3, - consumer: 'index', - }, - { - id: 4, - consumer: 'foo', - }, - { - id: 5, - consumer: 'bar', - }, + { id: 1, consumer: 'siem' }, + { id: 2, consumer: 'kibana' }, + { id: 3, consumer: 'index' }, + { id: 4, consumer: 'foo' }, + { id: 5, consumer: 'bar' }, ], }; }, @@ -91,30 +75,12 @@ describe('getObservabilityAlerts', () => { get: async () => { return { data: [ - { - id: 1, - consumer: 'siem', - }, - { - id: 2, - consumer: 'apm', - }, - { - id: 3, - consumer: 'uptime', - }, - { - id: 4, - consumer: 'logs', - }, - { - id: 5, - consumer: 'metrics', - }, - { - id: 6, - consumer: 'alerts', - }, + { id: 1, consumer: 'siem' }, + { id: 2, consumer: 'apm' }, + { id: 3, consumer: 'uptime' }, + { id: 4, consumer: 'logs' }, + { id: 5, consumer: 'metrics' }, + { id: 6, consumer: 'alerts' }, ], }; }, diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts index aad609dc82e98..b1f8f0fb1bddc 100644 --- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -11,12 +11,13 @@ const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts']; export async function getObservabilityAlerts({ core }: { core: CoreStart }) { try { - const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', { - query: { - page: 1, - per_page: 20, - }, - }); + const { data = [] }: { data: Alert[] } = + (await core.http.get('/api/alerts/_find', { + query: { + page: 1, + per_page: 20, + }, + })) || {}; return data.filter(({ consumer }) => allowedConsumers.includes(consumer)); } catch (e) { From 4f0332dc0267bcee761ae5453a6daf08094fac07 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 20 Nov 2020 13:45:42 +0100 Subject: [PATCH 11/13] addressing PR comments --- .../components/app/section/apm/index.test.tsx | 4 ++- .../components/app/section/apm/index.tsx | 12 ++++---- .../components/app/section/logs/index.tsx | 12 ++++---- .../components/app/section/metrics/index.tsx | 8 ++--- .../components/app/section/uptime/index.tsx | 12 ++++---- .../components/app/section/ux/index.test.tsx | 1 + .../components/app/section/ux/index.tsx | 8 ++--- .../public/context/has_data_context.tsx | 6 ++-- .../public/hooks/use_time_range.test.ts | 29 ++++++++++--------- .../public/hooks/use_time_range.ts | 16 +++++----- .../public/pages/overview/index.tsx | 10 +++---- 11 files changed, 63 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index cc2c3f7cd9269..9fdc59d61257e 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -11,12 +11,13 @@ import { response } from './mock_data/apm.mock'; import * as hasDataHook from '../../../../hooks/use_has_data'; import * as pluginContext from '../../../../hooks/use_plugin_context'; import { HasDataContextValue } from '../../../../context/has_data_context'; -import { CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import { ObservabilityPluginSetupDeps } from '../../../../plugin'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/observability/overview/', + search: '', }), useHistory: jest.fn(), })); @@ -36,6 +37,7 @@ describe('APMSection', () => { uiSettings: { get: jest.fn() }, http: { basePath: { prepend: jest.fn() } }, } as unknown) as CoreStart, + appMountParameters: {} as AppMountParameters, plugins: ({ data: { query: { diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index b48b63accea83..91d20d3478960 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -35,21 +35,21 @@ export function APMSection({ bucketSize }: Props) { const chartTheme = useChartTheme(); const history = useHistory(); const { forceUpdate, hasData } = useHasData(); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( () => { if (bucketSize) { return getDataHandler('apm')?.fetchData({ - absoluteTime: { start: absStart, end: absEnd }, - relativeTime: { start: rangeFrom, end: rangeTo }, + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, bucketSize, }); } }, // Absolute times shouldn't be used here, since it would refetch on every render // eslint-disable-next-line react-hooks/exhaustive-deps - [bucketSize, rangeFrom, rangeTo, forceUpdate] + [bucketSize, relativeStart, relativeEnd, forceUpdate] ); if (!hasData.apm?.hasData) { @@ -58,8 +58,8 @@ export function APMSection({ bucketSize }: Props) { const { appLink, stats, series } = data || {}; - const min = moment.utc(absStart).valueOf(); - const max = moment.utc(absEnd).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index 7adbbbf2a6859..f60cab86453d1 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -47,29 +47,29 @@ export function LogsSection({ bucketSize }: Props) { const history = useHistory(); const chartTheme = useChartTheme(); const { forceUpdate, hasData } = useHasData(); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( () => { if (bucketSize) { return getDataHandler('infra_logs')?.fetchData({ - absoluteTime: { start: absStart, end: absEnd }, - relativeTime: { start: rangeFrom, end: rangeTo }, + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, bucketSize, }); } }, // Absolute times shouldn't be used here, since it would refetch on every render // eslint-disable-next-line react-hooks/exhaustive-deps - [bucketSize, rangeFrom, rangeTo, forceUpdate] + [bucketSize, relativeStart, relativeEnd, forceUpdate] ); if (!hasData.infra_logs?.hasData) { return null; } - const min = moment.utc(absStart).valueOf(); - const max = moment.utc(absEnd).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index a2c3838a42983..f7fe3f5694a4a 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -49,21 +49,21 @@ const StyledProgress = styled.div<{ color?: string }>` export function MetricsSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const { forceUpdate, hasData } = useHasData(); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( () => { if (bucketSize) { return getDataHandler('infra_metrics')?.fetchData({ - absoluteTime: { start: absStart, end: absEnd }, - relativeTime: { start: rangeFrom, end: rangeTo }, + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, bucketSize, }); } }, // Absolute times shouldn't be used here, since it would refetch on every render // eslint-disable-next-line react-hooks/exhaustive-deps - [bucketSize, rangeFrom, rangeTo, forceUpdate] + [bucketSize, relativeStart, relativeEnd, forceUpdate] ); if (!hasData.infra_metrics?.hasData) { diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 54b9ddbc6dc9b..b0710a5c695a7 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -40,29 +40,29 @@ export function UptimeSection({ bucketSize }: Props) { const chartTheme = useChartTheme(); const history = useHistory(); const { forceUpdate, hasData } = useHasData(); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( () => { if (bucketSize) { return getDataHandler('uptime')?.fetchData({ - absoluteTime: { start: absStart, end: absEnd }, - relativeTime: { start: rangeFrom, end: rangeTo }, + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, bucketSize, }); } }, // Absolute times shouldn't be used here, since it would refetch on every render // eslint-disable-next-line react-hooks/exhaustive-deps - [bucketSize, rangeFrom, rangeTo, forceUpdate] + [bucketSize, relativeStart, relativeEnd, forceUpdate] ); if (!hasData.uptime?.hasData) { return null; } - const min = moment.utc(absStart).valueOf(); - const max = moment.utc(absEnd).valueOf(); + const min = moment.utc(absoluteStart).valueOf(); + const max = moment.utc(absoluteEnd).valueOf(); const formatter = niceTimeFormatter([min, max]); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index 6527ed0177d7e..0c9021204e73c 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -17,6 +17,7 @@ import { response } from './mock_data/ux.mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/observability/overview/', + search: '', }), })); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index a7e124b344296..43f1072d06fc2 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -20,7 +20,7 @@ interface Props { export function UXSection({ bucketSize }: Props) { const { forceUpdate, hasData } = useHasData(); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {}; const serviceName = uxHasDataResponse.serviceName as string; @@ -28,8 +28,8 @@ export function UXSection({ bucketSize }: Props) { () => { if (serviceName && bucketSize) { return getDataHandler('ux')?.fetchData({ - absoluteTime: { start: absStart, end: absEnd }, - relativeTime: { start: rangeFrom, end: rangeTo }, + absoluteTime: { start: absoluteStart, end: absoluteEnd }, + relativeTime: { start: relativeStart, end: relativeEnd }, serviceName, bucketSize, }); @@ -37,7 +37,7 @@ export function UXSection({ bucketSize }: Props) { }, // Absolute times shouldn't be used here, since it would refetch on every render // eslint-disable-next-line react-hooks/exhaustive-deps - [bucketSize, rangeFrom, rangeTo, forceUpdate, serviceName] + [bucketSize, relativeStart, relativeEnd, forceUpdate, serviceName] ); if (!uxHasDataResponse?.hasData) { diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index beee1061c0b8d..79d58056af73c 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -36,7 +36,7 @@ const apps: DataContextApps[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics', export function HasDataContextProvider({ children }: { children: React.ReactNode }) { const { core } = usePluginContext(); const [forceUpdate, setForceUpdate] = useState(''); - const { absStart, absEnd } = useTimeRange(); + const { absoluteStart, absoluteEnd } = useTimeRange(); const [hasData, setHasData] = useState({}); @@ -46,7 +46,9 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode try { if (app !== 'alert') { const params = - app === 'ux' ? { absoluteTime: { start: absStart, end: absEnd } } : undefined; + app === 'ux' + ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } + : undefined; const result = await getDataHandler(app)?.hasData(params); setHasData((prevState) => ({ diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts index 993b6834573d3..c89d52f904a96 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -6,13 +6,14 @@ import { useTimeRange } from './use_time_range'; import * as pluginContext from './use_plugin_context'; -import { CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import { ObservabilityPluginSetupDeps } from '../plugin'; import * as kibanaUISettings from './use_kibana_ui_settings'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/observability/overview/', + search: '', }), })); @@ -20,6 +21,7 @@ describe('useTimeRange', () => { beforeAll(() => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, + appMountParameters: {} as AppMountParameters, plugins: ({ data: { query: { @@ -44,14 +46,14 @@ describe('useTimeRange', () => { describe('when range from and to are not provided', () => { describe('when data plugin has time set', () => { it('returns ranges and absolute times from data plugin', () => { - const rangeFrom = '2020-10-08T06:00:00.000Z'; - const rangeTo = '2020-10-08T07:00:00.000Z'; + const relativeStart = '2020-10-08T06:00:00.000Z'; + const relativeEnd = '2020-10-08T07:00:00.000Z'; const timeRange = useTimeRange(); expect(timeRange).toEqual({ - rangeFrom, - rangeTo, - absStart: new Date(rangeFrom).valueOf(), - absEnd: new Date(rangeTo).valueOf(), + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), }); }); }); @@ -59,6 +61,7 @@ describe('useTimeRange', () => { beforeAll(() => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, + appMountParameters: {} as AppMountParameters, plugins: ({ data: { query: { @@ -76,14 +79,14 @@ describe('useTimeRange', () => { })); }); it('returns ranges and absolute times from kibana default settings', () => { - const rangeFrom = '2020-10-08T05:00:00.000Z'; - const rangeTo = '2020-10-08T06:00:00.000Z'; + const relativeStart = '2020-10-08T05:00:00.000Z'; + const relativeEnd = '2020-10-08T06:00:00.000Z'; const timeRange = useTimeRange(); expect(timeRange).toEqual({ - rangeFrom, - rangeTo, - absStart: new Date(rangeFrom).valueOf(), - absEnd: new Date(rangeTo).valueOf(), + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), }); }); }); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.ts b/x-pack/plugins/observability/public/hooks/use_time_range.ts index 7a61b11f9bc04..e8bed12aaa9bd 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.ts @@ -6,13 +6,13 @@ import { parse } from 'query-string'; import { useLocation } from 'react-router-dom'; -import { TimePickerTime } from '../components/shared/data_picker'; +import { TimePickerTime } from '../components/shared/date_picker'; import { getAbsoluteTime } from '../utils/date'; import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings'; import { usePluginContext } from './use_plugin_context'; const getParsedParams = (search: string) => { - return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {}; + return parse(search.slice(1), { sort: false }); }; export function useTimeRange() { @@ -26,15 +26,15 @@ export function useTimeRange() { const { rangeFrom, rangeTo } = getParsedParams(useLocation().search); - const _rangeFrom = (rangeFrom ?? + const relativeStart = (rangeFrom ?? timePickerSharedState.from ?? timePickerTimeDefaults.from) as string; - const _rangeTo = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string; + const relativeEnd = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string; return { - rangeFrom: _rangeFrom, - rangeTo: _rangeTo, - absStart: getAbsoluteTime(_rangeFrom)!, - absEnd: getAbsoluteTime(_rangeTo, { roundUp: true })!, + relativeStart, + relativeEnd, + absoluteStart: getAbsoluteTime(relativeStart)!, + absoluteEnd: getAbsoluteTime(relativeEnd, { roundUp: true })!, }; } diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index b913decf0d93a..87a836b2cb32c 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { default as React, default as React, useContext } from 'react'; +import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; import { useTrackPageview } from '../..'; import { Alert } from '../../../../alerts/common'; @@ -13,7 +13,7 @@ import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { NewsFeed } from '../../components/app/news_feed'; import { Resources } from '../../components/app/resources'; import { AlertsSection } from '../../components/app/section/alerts'; -import { DatePicker } from '../../components/shared/data_picker'; +import { DatePicker } from '../../components/shared/date_picker'; import { useFetcher } from '../../hooks/use_fetcher'; import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -40,10 +40,10 @@ export function OverviewPage({ routeParams }: Props) { const { core } = usePluginContext(); const theme = useContext(ThemeContext); - const { rangeFrom, rangeTo, absStart, absEnd } = useTimeRange(); + const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const relativeTime = { start: rangeFrom, end: rangeTo }; - const absoluteTime = { start: absStart, end: absEnd }; + const relativeTime = { start: relativeStart, end: relativeEnd }; + const absoluteTime = { start: absoluteStart, end: absoluteEnd }; const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); From bf819f88d8029195261aa24c212e1758a9bfeb60 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 20 Nov 2020 14:10:52 +0100 Subject: [PATCH 12/13] fixing TS issues --- .../apm/server/lib/rum_client/has_rum_data.ts | 2 +- .../components/app/section/ux/index.test.tsx | 3 ++- .../public/context/has_data_context.test.tsx | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) 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 14245ce1d6c83..bcd6d10d31987 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 @@ -54,6 +54,6 @@ export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key, }; } catch (e) { - return false; + return { hasData: false, serviceName: undefined }; } } diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index 0c9021204e73c..be6df55166387 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { HasDataContextValue } from '../../../../context/has_data_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; @@ -36,6 +36,7 @@ describe('UXSection', () => { uiSettings: { get: jest.fn() }, http: { basePath: { prepend: jest.fn() } }, } as unknown) as CoreStart, + appMountParameters: {} as AppMountParameters, plugins: ({ data: { query: { diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 41052182f10aa..3369765c68bd1 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -16,8 +16,8 @@ import { HasDataContextProvider } from './has_data_context'; import * as pluginContext from '../hooks/use_plugin_context'; import { PluginContextValue } from './plugin_context'; -const rangeFrom = '2020-10-08T06:00:00.000Z'; -const rangeTo = '2020-10-08T07:00:00.000Z'; +const relativeStart = '2020-10-08T06:00:00.000Z'; +const relativeEnd = '2020-10-08T07:00:00.000Z'; function wrapper({ children }: { children: React.ReactElement }) { return {children}; @@ -47,16 +47,16 @@ describe('HasDataContextProvider', () => { beforeAll(() => { jest.spyOn(routeParams, 'useRouteParams').mockImplementation(() => ({ query: { - from: rangeFrom, - to: rangeTo, + from: relativeStart, + to: relativeEnd, }, path: {}, })); jest.spyOn(timeRange, 'useTimeRange').mockImplementation(() => ({ - rangeFrom, - rangeTo, - absStart: new Date(rangeFrom).valueOf(), - absEnd: new Date(rangeTo).valueOf(), + relativeStart, + relativeEnd, + absoluteStart: new Date(relativeStart).valueOf(), + absoluteEnd: new Date(relativeEnd).valueOf(), })); jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({ core: ({ http: { get: jest.fn() } } as unknown) as CoreStart, From 6d8a5678743c450c362522a117763ce327a64e4e Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Fri, 20 Nov 2020 15:00:02 +0100 Subject: [PATCH 13/13] fixing eslint issue --- x-pack/plugins/observability/public/application/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 7878fbd6b1e83..ea84a417c20eb 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -40,6 +40,7 @@ function App() { const Wrapper = () => { const { core } = usePluginContext(); + // eslint-disable-next-line react-hooks/exhaustive-deps const breadcrumb = [observabilityLabelBreadcrumb, ...route.breadcrumb]; useEffect(() => { core.chrome.setBreadcrumbs(breadcrumb);