Skip to content

Commit

Permalink
[Logs UI] Preserve relative dates of Logs Analysis datepicker (elasti…
Browse files Browse the repository at this point in the history
…c#44706) (elastic#44914)

Backports the following commits to 7.4:
 - [Logs UI] Preserve relative dates of Logs Analysis datepicker (elastic#44706)
  • Loading branch information
weltenwort authored Sep 5, 2019
1 parent 3445844 commit 24ebd2a
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/

import moment from 'moment';
import { useEffect } from 'react';
import * as rt from 'io-ts';
import { useUrlState } from '../../../utils/use_url_state';
import { timeRangeRT } from '../../../../common/http_api/shared/time_range';

const autoRefreshRT = rt.union([rt.boolean, rt.undefined]);
const urlTimeRangeRT = rt.union([timeRangeRT, rt.undefined]);
const autoRefreshRT = rt.union([
rt.type({
interval: rt.number,
isPaused: rt.boolean,
}),
rt.undefined,
]);

export const stringTimeRangeRT = rt.type({
startTime: rt.string,
endTime: rt.string,
});
export type StringTimeRange = rt.TypeOf<typeof stringTimeRangeRT>;

const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]);

const TIME_RANGE_URL_STATE_KEY = 'timeRange';
const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh';

export const useLogAnalysisResultsUrlState = () => {
const [timeRange, setTimeRange] = useUrlState({
defaultState: {
startTime: moment
.utc()
.subtract(2, 'weeks')
.valueOf(),
endTime: moment.utc().valueOf(),
startTime: 'now-2w',
endTime: 'now',
},
decodeUrlState: (value: unknown) => urlTimeRangeRT.decode(value).getOrElse(undefined),
encodeUrlState: urlTimeRangeRT.encode,
Expand All @@ -34,21 +42,24 @@ export const useLogAnalysisResultsUrlState = () => {
setTimeRange(timeRange);
}, []);

const [autoRefreshEnabled, setAutoRefresh] = useUrlState({
defaultState: false,
const [autoRefresh, setAutoRefresh] = useUrlState({
defaultState: {
isPaused: false,
interval: 30000,
},
decodeUrlState: (value: unknown) => autoRefreshRT.decode(value).getOrElse(undefined),
encodeUrlState: autoRefreshRT.encode,
urlStateKey: AUTOREFRESH_URL_STATE_KEY,
});

useEffect(() => {
setAutoRefresh(autoRefreshEnabled);
setAutoRefresh(autoRefresh);
}, []);

return {
timeRange,
setTimeRange,
autoRefreshEnabled,
autoRefresh,
setAutoRefresh,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback, useMemo, useState } from 'react';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import datemath from '@elastic/datemath';
import {
EuiSuperDatePicker,
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPanel,
EuiBadge,
EuiSuperDatePicker,
} from '@elastic/eui';
import dateMath from '@elastic/datemath';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useCallback, useMemo, useState } from 'react';

import euiStyled from '../../../../../../common/eui_styled_components';
import { useTrackPageview } from '../../../hooks/use_track_metric';
import { useInterval } from '../../../hooks/use_interval';
import { useLogAnalysisResults } from '../../../containers/logs/log_analysis';
import { useLogAnalysisResultsUrlState } from '../../../containers/logs/log_analysis';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { bucketSpan } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import { LogRateResults } from './sections/log_rate';
import {
StringTimeRange,
useLogAnalysisResults,
useLogAnalysisResultsUrlState,
} from '../../../containers/logs/log_analysis';
import { useTrackPageview } from '../../../hooks/use_track_metric';
import { FirstUseCallout } from './first_use';

const DATE_PICKER_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';

const getLoadingState = () => {
return (
<LoadingPage
message={i18n.translate('xpack.infra.logs.logsAnalysisResults.loadingMessage', {
defaultMessage: 'Loading results...',
})}
/>
);
};
import { LogRateResults } from './sections/log_rate';

export const AnalysisResultsContent = ({
sourceId,
Expand All @@ -54,26 +45,15 @@ export const AnalysisResultsContent = ({
useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 });

const {
timeRange,
setTimeRange,
autoRefreshEnabled,
timeRange: selectedTimeRange,
setTimeRange: setSelectedTimeRange,
autoRefresh,
setAutoRefresh,
} = useLogAnalysisResultsUrlState();

const [refreshInterval, setRefreshInterval] = useState(300000);

const setTimeRangeToNow = useCallback(() => {
const range = timeRange.endTime - timeRange.startTime;
const nowInMs = moment()
.utc()
.valueOf();
setTimeRange({
startTime: nowInMs - range,
endTime: nowInMs,
});
}, [timeRange.startTime, timeRange.endTime, setTimeRange]);

useInterval(setTimeRangeToNow, autoRefreshEnabled ? refreshInterval : null);
const [queryTimeRange, setQueryTimeRange] = useState<TimeRange>(
stringToNumericTimeRange(selectedTimeRange)
);

const bucketDuration = useMemo(() => {
// This function takes the current time range in ms,
Expand All @@ -83,52 +63,63 @@ export const AnalysisResultsContent = ({
// 900000 (15 minutes) to it, so that we don't end up with
// jaggy bucket boundaries between the ML buckets and our
// aggregation buckets.
const msRange = timeRange.endTime - timeRange.startTime;
const msRange = moment(queryTimeRange.endTime).diff(moment(queryTimeRange.startTime));
const bucketIntervalInMs = msRange / 200;
const bucketSpan = 900000; // TODO: Pull this from 'common' when setup hook PR is merged
const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan);
const roundedResult = parseInt(Number(result).toFixed(0), 10);
return roundedResult < bucketSpan ? bucketSpan : roundedResult;
}, [timeRange]);
}, [queryTimeRange.startTime, queryTimeRange.endTime]);

const { isLoading, logEntryRate } = useLogAnalysisResults({
sourceId,
startTime: timeRange.startTime,
endTime: timeRange.endTime,
startTime: queryTimeRange.startTime,
endTime: queryTimeRange.endTime,
bucketDuration,
});
const hasResults = useMemo(() => logEntryRate && logEntryRate.histogramBuckets.length > 0, [
logEntryRate,
]);
const handleTimeRangeChange = useCallback(
({ start, end }: { start: string; end: string }) => {
const parsedStart = dateMath.parse(start);
const parsedEnd = dateMath.parse(end);
setTimeRange({
startTime:
!parsedStart || !parsedStart.isValid() ? timeRange.startTime : parsedStart.valueOf(),
endTime: !parsedEnd || !parsedEnd.isValid() ? timeRange.endTime : parsedEnd.valueOf(),

const handleQueryTimeRangeChange = useCallback(
({ start: startTime, end: endTime }: { start: string; end: string }) => {
setQueryTimeRange(stringToNumericTimeRange({ startTime, endTime }));
},
[setQueryTimeRange]
);

const handleSelectedTimeRangeChange = useCallback(
(selectedTime: { start: string; end: string; isInvalid: boolean }) => {
if (selectedTime.isInvalid) {
return;
}
setSelectedTimeRange({
startTime: selectedTime.start,
endTime: selectedTime.end,
});
handleQueryTimeRangeChange(selectedTime);
},
[setSelectedTimeRange, handleQueryTimeRangeChange]
);

const handleAutoRefreshChange = useCallback(
({ isPaused, refreshInterval: interval }: { isPaused: boolean; refreshInterval: number }) => {
setAutoRefresh({
isPaused,
interval,
});
},
[setTimeRange, timeRange]
[setAutoRefresh]
);

const anomaliesDetected = useMemo(() => {
if (!logEntryRate) {
return null;
} else {
if (logEntryRate.histogramBuckets && logEntryRate.histogramBuckets.length) {
return logEntryRate.histogramBuckets.reduce((acc: any, bucket) => {
if (bucket.anomalies.length > 0) {
return (
acc +
bucket.anomalies.reduce((anomalyAcc: any, anomaly) => {
return anomalyAcc + 1;
}, 0)
);
} else {
return acc;
}
}, 0);
return logEntryRate.histogramBuckets.reduce(
(acc, bucket) => acc + bucket.anomalies.length,
0
);
} else {
return null;
}
Expand All @@ -138,7 +129,11 @@ export const AnalysisResultsContent = ({
return (
<>
{isLoading && !logEntryRate ? (
<>{getLoadingState()}</>
<LoadingPage
message={i18n.translate('xpack.infra.logs.logsAnalysisResults.loadingMessage', {
defaultMessage: 'Loading results...',
})}
/>
) : (
<>
<EuiPage>
Expand All @@ -148,41 +143,33 @@ export const AnalysisResultsContent = ({
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
{anomaliesDetected !== null ? (
<>
<span>
<FormattedMessage
id="xpack.infra.logs.analysis.anomaliesDetectedText"
defaultMessage="Detected {formattedNumber} {number, plural, one {anomaly} other {anomalies}}"
values={{
formattedNumber: (
<EuiBadge color={anomaliesDetected === 0 ? 'default' : 'warning'}>
{anomaliesDetected}
</EuiBadge>
),
number: anomaliesDetected,
}}
/>
</span>
</>
<span>
<FormattedMessage
id="xpack.infra.logs.analysis.anomaliesDetectedText"
defaultMessage="Detected {formattedNumber} {number, plural, one {anomaly} other {anomalies}}"
values={{
formattedNumber: (
<EuiBadge color={anomaliesDetected === 0 ? 'default' : 'warning'}>
{anomaliesDetected}
</EuiBadge>
),
number: anomaliesDetected,
}}
/>
</span>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperDatePicker
start={moment.utc(timeRange.startTime).format(DATE_PICKER_FORMAT)}
end={moment.utc(timeRange.endTime).format(DATE_PICKER_FORMAT)}
onTimeChange={handleTimeRangeChange}
isPaused={!autoRefreshEnabled}
refreshInterval={refreshInterval}
onRefreshChange={({ isPaused, refreshInterval: interval }) => {
if (isPaused) {
setAutoRefresh(false);
} else {
setRefreshInterval(interval);
setAutoRefresh(true);
}
}}
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
onRefresh={handleQueryTimeRangeChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -196,7 +183,7 @@ export const AnalysisResultsContent = ({
<LogRateResults
isLoading={isLoading}
results={logEntryRate}
timeRange={timeRange}
timeRange={queryTimeRange}
/>
</EuiPageContentBody>
</EuiPageContent>
Expand All @@ -208,6 +195,20 @@ export const AnalysisResultsContent = ({
);
};

const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({
startTime: moment(
datemath.parse(timeRange.startTime, {
momentInstance: moment,
})
).valueOf(),
endTime: moment(
datemath.parse(timeRange.endTime, {
momentInstance: moment,
roundUp: true,
})
).valueOf(),
});

const ExpandingPage = euiStyled(EuiPage)`
flex: 1 0 0%;
`;

0 comments on commit 24ebd2a

Please sign in to comment.