diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js
index 52a9af505b..723660dc15 100644
--- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js
+++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js
@@ -14,9 +14,11 @@
import * as React from 'react';
import { Form, Input, Button, Popover, Select } from 'antd';
+import _get from 'lodash/get';
import logfmtParser from 'logfmt/lib/logfmt_parser';
import { stringify as logfmtStringify } from 'logfmt/lib/stringify';
import moment from 'moment';
+import memoizeOne from 'memoize-one';
import PropTypes from 'prop-types';
import queryString from 'query-string';
import IoHelp from 'react-icons/lib/io/help';
@@ -71,6 +73,95 @@ export function convTagsLogfmt(tags) {
return JSON.stringify(data);
}
+export function lookbackToTimestamp(lookback, from) {
+ const unit = lookback.substr(-1);
+ return (
+ moment(from)
+ .subtract(parseInt(lookback, 10), unit)
+ .valueOf() * 1000
+ );
+}
+
+const lookbackOptions = [
+ {
+ label: 'Hour',
+ value: '1h',
+ },
+ {
+ label: '2 Hours',
+ value: '2h',
+ },
+ {
+ label: '3 Hours',
+ value: '3h',
+ },
+ {
+ label: '6 Hours',
+ value: '6h',
+ },
+ {
+ label: '12 Hours',
+ value: '12h',
+ },
+ {
+ label: '24 Hours',
+ value: '24h',
+ },
+ {
+ label: '2 Days',
+ value: '2d',
+ },
+ {
+ label: '3 Days',
+ value: '3d',
+ },
+ {
+ label: '5 Days',
+ value: '5d',
+ },
+ {
+ label: '7 Days',
+ value: '7d',
+ },
+ {
+ label: '2 Weeks',
+ value: '2w',
+ },
+ {
+ label: '3 Weeks',
+ value: '3w',
+ },
+ {
+ label: '4 Weeks',
+ value: '4w',
+ },
+];
+
+export const optionsWithinMaxLookback = memoizeOne(maxLookback => {
+ const now = new Date();
+ const minTimestamp = lookbackToTimestamp(maxLookback.value, now);
+ const lookbackToTimestampMap = new Map();
+ const options = lookbackOptions.filter(({ value }) => {
+ const lookbackTimestamp = lookbackToTimestamp(value, now);
+ lookbackToTimestampMap.set(value, lookbackTimestamp);
+ return lookbackTimestamp >= minTimestamp;
+ });
+ const lastInRangeIndex = options.length - 1;
+ const lastInRangeOption = options[lastInRangeIndex];
+ if (lastInRangeOption.label !== maxLookback.label) {
+ if (lookbackToTimestampMap.get(lastInRangeOption.value) !== minTimestamp) {
+ options.push(maxLookback);
+ } else {
+ options.splice(lastInRangeIndex, 1, maxLookback);
+ }
+ }
+ return options.map(({ label, value }) => (
+
+ ));
+});
+
export function traceIDsToQuery(traceIDs) {
if (!traceIDs) {
return null;
@@ -133,13 +224,9 @@ export function submitForm(fields, searchTraces) {
let start;
let end;
if (lookback !== 'custom') {
- const unit = lookback.substr(-1);
const now = new Date();
- start =
- moment(now)
- .subtract(parseInt(lookback, 10), unit)
- .valueOf() * 1000;
- end = moment(now).valueOf() * 1000;
+ start = lookbackToTimestamp(lookback, now);
+ end = now * 1000;
} else {
const times = getUnixTimeStampInMSFromForm({
startDate,
@@ -171,6 +258,7 @@ export class SearchFormImpl extends React.PureComponent {
const {
handleSubmit,
invalid,
+ searchMaxLookback,
selectedLookback,
selectedService = '-',
services,
@@ -264,13 +352,7 @@ export class SearchFormImpl extends React.PureComponent {
-
-
-
-
-
-
-
+ {optionsWithinMaxLookback(searchMaxLookback)}
@@ -381,6 +463,10 @@ SearchFormImpl.propTypes = {
handleSubmit: PropTypes.func.isRequired,
invalid: PropTypes.bool,
submitting: PropTypes.bool,
+ searchMaxLookback: PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ }).isRequired,
services: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
@@ -512,6 +598,7 @@ export function mapStateToProps(state) {
maxDuration: maxDuration || null,
traceIDs: traceIDs || null,
},
+ searchMaxLookback: _get(state, 'config.search.maxLookback'),
selectedService: searchSideBarFormSelector(state, 'service'),
selectedLookback: searchSideBarFormSelector(state, 'lookback'),
};
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js
index 3f2edecc03..c333dadfcc 100644
--- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js
+++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js
@@ -25,7 +25,9 @@ import {
convertQueryParamsToFormDates,
convTagsLogfmt,
getUnixTimeStampInMSFromForm,
+ lookbackToTimestamp,
mapStateToProps,
+ optionsWithinMaxLookback,
submitForm,
traceIDsToQuery,
SearchFormImpl as SearchForm,
@@ -53,8 +55,12 @@ function makeDateParams(dateOffset = 0) {
const DATE_FORMAT = 'YYYY-MM-DD';
const TIME_FORMAT = 'HH:mm';
const defaultProps = {
- services: [{ name: 'svcA', operations: ['A', 'B'] }, { name: 'svcB', operations: ['A', 'B'] }],
dataCenters: ['dc1'],
+ searchMaxLookback: {
+ label: '2 Days',
+ value: '2d',
+ },
+ services: [{ name: 'svcA', operations: ['A', 'B'] }, { name: 'svcB', operations: ['A', 'B'] }],
};
describe('conversion utils', () => {
@@ -126,6 +132,104 @@ describe('conversion utils', () => {
});
});
+describe('lookback utils', () => {
+ describe('lookbackToTimestamp', () => {
+ const hourInMicroseconds = 60 * 60 * 1000 * 1000;
+ const now = new Date();
+ const nowInMicroseconds = now * 1000;
+
+ it('creates timestamp for hours ago', () => {
+ [1, 2, 4, 7].forEach(lookbackNum => {
+ expect(nowInMicroseconds - lookbackToTimestamp(`${lookbackNum}h`, now)).toBe(
+ lookbackNum * hourInMicroseconds
+ );
+ });
+ });
+
+ it('creates timestamp for days ago', () => {
+ [1, 2, 4, 7].forEach(lookbackNum => {
+ expect(nowInMicroseconds - lookbackToTimestamp(`${lookbackNum}d`, now)).toBe(
+ lookbackNum * 24 * hourInMicroseconds
+ );
+ });
+ });
+
+ it('creates timestamp for weeks ago', () => {
+ [1, 2, 4, 7].forEach(lookbackNum => {
+ expect(nowInMicroseconds - lookbackToTimestamp(`${lookbackNum}w`, now)).toBe(
+ lookbackNum * 7 * 24 * hourInMicroseconds
+ );
+ });
+ });
+ });
+
+ describe('optionsWithinMaxLookback', () => {
+ const threeHoursOfExpectedOptions = [
+ {
+ label: 'Hour',
+ value: '1h',
+ },
+ {
+ label: '2 Hours',
+ value: '2h',
+ },
+ {
+ label: '3 Hours',
+ value: '3h',
+ },
+ ];
+
+ it('memoizes correctly', () => {
+ const firstCallOptions = optionsWithinMaxLookback(threeHoursOfExpectedOptions[0]);
+ const secondCallOptions = optionsWithinMaxLookback(threeHoursOfExpectedOptions[0]);
+ const thirdCallOptions = optionsWithinMaxLookback(threeHoursOfExpectedOptions[1]);
+ expect(secondCallOptions).toBe(firstCallOptions);
+ expect(thirdCallOptions).not.toBe(firstCallOptions);
+ });
+
+ it('returns options within config.search.maxLookback', () => {
+ const configValue = threeHoursOfExpectedOptions[2];
+ const options = optionsWithinMaxLookback(configValue);
+
+ expect(options.length).toBe(threeHoursOfExpectedOptions.length);
+ options.forEach(({ props }, i) => {
+ expect(props.value).toBe(threeHoursOfExpectedOptions[i].value);
+ expect(props.children[1]).toBe(threeHoursOfExpectedOptions[i].label);
+ });
+ });
+
+ it("includes config.search.maxLookback if it's not part of standard options", () => {
+ const configValue = {
+ label: '4 Hours - configValue',
+ value: '4h',
+ };
+ const expectedOptions = [...threeHoursOfExpectedOptions, configValue];
+ const options = optionsWithinMaxLookback(configValue);
+
+ expect(options.length).toBe(expectedOptions.length);
+ options.forEach(({ props }, i) => {
+ expect(props.value).toBe(expectedOptions[i].value);
+ expect(props.children[1]).toBe(expectedOptions[i].label);
+ });
+ });
+
+ it('uses config.search.maxLookback in place of standard option it is not equal to but is equivalent to', () => {
+ const configValue = {
+ label: '180 minutes is equivalent to 3 hours',
+ value: '180m',
+ };
+ const expectedOptions = [threeHoursOfExpectedOptions[0], threeHoursOfExpectedOptions[1], configValue];
+ const options = optionsWithinMaxLookback(configValue);
+
+ expect(options.length).toBe(expectedOptions.length);
+ options.forEach(({ props }, i) => {
+ expect(props.value).toBe(expectedOptions[i].value);
+ expect(props.children[1]).toBe(expectedOptions[i].label);
+ });
+ });
+ });
+});
+
describe('submitForm()', () => {
const LOOKBACK_VALUE = 2;
const LOOKBACK_UNIT = 's';
diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx
index 250d133761..a7b332cb89 100644
--- a/packages/jaeger-ui/src/constants/default-config.tsx
+++ b/packages/jaeger-ui/src/constants/default-config.tsx
@@ -25,10 +25,6 @@ export default deepFreeze(
menuEnabled: true,
},
linkPatterns: [],
- tracking: {
- gaID: null,
- trackErrors: true,
- },
menu: [
{
label: 'About Jaeger',
@@ -60,10 +56,20 @@ export default deepFreeze(
],
},
],
+ search: {
+ maxLookback: {
+ label: '2 Days',
+ value: '2d',
+ },
+ },
+ tracking: {
+ gaID: null,
+ trackErrors: true,
+ },
},
// fields that should be individually merged vs wholesale replaced
'__mergeFields',
- { value: ['tracking', 'dependencies'] }
+ { value: ['dependencies', 'search', 'tracking'] }
)
);
diff --git a/packages/jaeger-ui/src/setupTests.js b/packages/jaeger-ui/src/setupTests.js
index 03e035d9b4..9117c6f686 100644
--- a/packages/jaeger-ui/src/setupTests.js
+++ b/packages/jaeger-ui/src/setupTests.js
@@ -25,3 +25,7 @@ const createSerializer = require('enzyme-to-json').createSerializer;
Enzyme.configure({ adapter: new EnzymeAdapter() });
expect.addSnapshotSerializer(createSerializer({ mode: 'deep' }));
+
+// Calls to get-config.tsx warn if this global is not a function
+// This file is executed before each test file, so this value may be overridden safely
+window.getJaegerUiConfig = () => ({});
diff --git a/packages/jaeger-ui/src/types/config.tsx b/packages/jaeger-ui/src/types/config.tsx
index fadf8bc6ce..95b015b2c1 100644
--- a/packages/jaeger-ui/src/types/config.tsx
+++ b/packages/jaeger-ui/src/types/config.tsx
@@ -26,11 +26,12 @@ export type ConfigMenuGroup = {
};
export type Config = {
- archiveEnabled: boolean | TNil;
+ archiveEnabled?: boolean;
dependencies?: { dagMaxServicesLen?: number; menuEnabled?: boolean };
+ menu: (ConfigMenuGroup | ConfigMenuItem)[];
+ search?: { maxLookback: { label: string; value: string } };
tracking?: {
gaID: string | TNil;
trackErrors: boolean | TNil;
};
- menu: (ConfigMenuGroup | ConfigMenuItem)[];
};
diff --git a/packages/jaeger-ui/src/utils/config/get-config.test.js b/packages/jaeger-ui/src/utils/config/get-config.test.js
index a2ed3f8854..d152469344 100644
--- a/packages/jaeger-ui/src/utils/config/get-config.test.js
+++ b/packages/jaeger-ui/src/utils/config/get-config.test.js
@@ -21,20 +21,27 @@ import processDeprecation from './process-deprecation';
import defaultConfig, { deprecations } from '../../constants/default-config';
describe('getConfig()', () => {
+ const warnFn = jest.fn();
let oldWarn;
- let warnFn;
- beforeEach(() => {
+ beforeAll(() => {
oldWarn = console.warn;
- warnFn = jest.fn();
console.warn = warnFn;
});
- afterEach(() => {
+ beforeEach(() => {
+ warnFn.mockClear();
+ });
+
+ afterAll(() => {
console.warn = oldWarn;
});
describe('`window.getJaegerUiConfig` is not a function', () => {
+ beforeAll(() => {
+ window.getJaegerUiConfig = undefined;
+ });
+
it('warns once', () => {
getConfig();
expect(warnFn.mock.calls.length).toBe(1);