diff --git a/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.test.tsx b/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.test.tsx index 9fc22e490ae2..c79b0acbe19b 100644 --- a/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.test.tsx +++ b/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.test.tsx @@ -15,14 +15,17 @@ * . */ import React from 'react'; +import Immutable from 'immutable'; import { render, screen } from 'wrappedTestingLibrary'; import { MockStore } from 'helpers/mocking'; import asMock from 'helpers/mocking/AsMock'; +import Search from 'views/logic/search/Search'; import Widget from 'views/logic/widgets/Widget'; import TimeLocalizeContext from 'contexts/TimeLocalizeContext'; import { GlobalOverrideStore, GlobalOverrideStoreState } from 'views/stores/GlobalOverrideStore'; import GlobalOverride from 'views/logic/search/GlobalOverride'; +import { SearchStore } from 'views/stores/SearchStore'; import TimerangeInfo from './TimerangeInfo'; @@ -34,30 +37,31 @@ jest.mock('views/stores/GlobalOverrideStore', () => ({ ), })); -jest.mock('views/stores/SearchStore', () => ({ - SearchStore: MockStore( - ['listen', () => jest.fn()], - 'get', - ['getInitialState', () => ({ - result: { - results: { - 'active-query-id': { - searchTypes: { - 'search-type-id': { - effective_timerange: { - type: 'absolute', from: '2021-03-27T14:32:31.894Z', to: '2021-04-26T14:32:48.000Z', - }, - }, +const mockSearchStoreState = (storeState = {}) => ({ + result: { + results: { + 'active-query-id': { + searchTypes: { + 'search-type-id': { + effective_timerange: { + type: 'absolute', from: '2021-03-27T14:32:31.894Z', to: '2021-04-26T14:32:48.000Z', }, }, }, }, - widgetMapping: { - get: jest.fn(() => ({ - first: jest.fn(() => 'search-type-id'), - })), - }, - })], + }, + }, + widgetMapping: Immutable.Map({ 'widget-id': Immutable.Set(['search-type-id']) }), + search: Search.create(), + widgetsToSearch: undefined, + ...storeState, +}); + +jest.mock('views/stores/SearchStore', () => ({ + SearchStore: MockStore( + ['listen', () => jest.fn()], + 'get', + ['getInitialState', jest.fn(() => mockSearchStoreState())], ), })); @@ -118,7 +122,7 @@ describe('TimerangeInfo', () => { it('should display global override', () => { const state: GlobalOverrideStoreState = GlobalOverride.empty().toBuilder().timerange({ type: 'relative', range: 3000 }).build(); - asMock(GlobalOverrideStore.getInitialState).mockReturnValue(state); + asMock(GlobalOverrideStore.getInitialState).mockReturnValueOnce(state); const keywordWidget = widget.toBuilder() .timerange({ type: 'keyword', keyword: '5 minutes ago' }) @@ -128,4 +132,32 @@ describe('TimerangeInfo', () => { expect(screen.getByText('Global Override: an hour ago - Now')).toBeInTheDocument(); }); + + it('should not throw error when related search type is empty', () => { + const relativeWidget = widget.toBuilder().timerange({ type: 'relative', range: 3000 }).build(); + + asMock(SearchStore.getInitialState).mockReturnValueOnce(mockSearchStoreState({ + result: { + results: { + 'active-query-id': { + searchTypes: {}, + }, + }, + }, + })); + + render(); + + expect(screen.getByText('an hour ago - Now')).toBeInTheDocument(); + }); + + it('should not throw error and display default time range when widget id does not exist in search widget mapping', () => { + asMock(SearchStore.getInitialState).mockReturnValueOnce(mockSearchStoreState({ + widgetMapping: Immutable.Map(), + })); + + render(); + + expect(screen.getByText('5 minutes ago - Now')).toBeInTheDocument(); + }); }); diff --git a/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.tsx b/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.tsx index 76154c29f3a6..b7beb0c53cd4 100644 --- a/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.tsx +++ b/graylog2-web-interface/src/views/components/widgets/TimerangeInfo.tsx @@ -40,6 +40,9 @@ const Wrapper = styled.div(({ theme }) => css` width: max-content; `); +const getEffectiveWidgetTimerange = (result, activeQuery, searchTypeId) => result?.results[activeQuery] + .searchTypes[searchTypeId]?.effective_timerange; + const TimerangeInfo = ({ className, widget, activeQuery, widgetId }: Props) => { const { localizeTime } = useContext(TimeLocalizeContext); const { result, widgetMapping } = useStore(SearchStore); @@ -50,10 +53,10 @@ const TimerangeInfo = ({ className, widget, activeQuery, widgetId }: Props) => { const configuredTimerange = timerangeToString(widget.timerange || DEFAULT_TIMERANGE, localizeTime); - const searchTypeId = widgetId ? widgetMapping.get(widgetId).first() : undefined; - const effectiveTimerange = activeQuery && searchTypeId ? result?.results[activeQuery] - .searchTypes[searchTypeId].effective_timerange : {}; - const effectiveTimerangeString = timerangeToString(effectiveTimerange, localizeTime); + const searchTypeId = widgetId ? widgetMapping.get(widgetId)?.first() : undefined; + + const effectiveTimerange = (activeQuery && searchTypeId) ? getEffectiveWidgetTimerange(result, activeQuery, searchTypeId) : undefined; + const effectiveTimerangeString = effectiveTimerange ? timerangeToString(effectiveTimerange, localizeTime) : 'Effective widget time range is currently not available.'; return (