[ML] Move DatePickerWrapper and related code to package (#148063)

- Moves duplicates of `DatePickerWrapper` and related code to package
`@kbn/ml-date-picker`. The duplicated components across the code base
have been consolidated and diverging features combined. Each duplicate
has been checked with a diff against the package before deletion.
- Moves duplicates of `query_utils.ts` to a package
`@kbn/ml-query_utils`.
- Some jest test were migrated from enzyme to react-testing-lib.
- `i18n` strings and data-test-subjects have been updated to be prefixes
in line with package names.
- Replaces custom code related to the `compact` flag with EUI's
breakpoints.
This commit is contained in:
Walter Rafelsberger 2023-01-12 11:04:49 +01:00 committed by GitHub
parent a275262b8a
commit ae5594849c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 1305 additions and 2304 deletions

2
.github/CODEOWNERS vendored
View file

@ -1078,9 +1078,11 @@ src/plugins/chart_expressions/common @elastic/kibana-visualizations
x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/agg_utils @elastic/ml-ui
x-pack/packages/ml/aiops_components @elastic/ml-ui x-pack/packages/ml/aiops_components @elastic/ml-ui
x-pack/packages/ml/aiops_utils @elastic/ml-ui x-pack/packages/ml/aiops_utils @elastic/ml-ui
x-pack/packages/ml/date_picker @elastic/ml-ui
x-pack/packages/ml/is_defined @elastic/ml-ui x-pack/packages/ml/is_defined @elastic/ml-ui
x-pack/packages/ml/is_populated_object @elastic/ml-ui x-pack/packages/ml/is_populated_object @elastic/ml-ui
x-pack/packages/ml/local_storage @elastic/ml-ui x-pack/packages/ml/local_storage @elastic/ml-ui
x-pack/packages/ml/nested_property @elastic/ml-ui x-pack/packages/ml/nested_property @elastic/ml-ui
x-pack/packages/ml/query_utils @elastic/ml-ui
x-pack/packages/ml/string_hash @elastic/ml-ui x-pack/packages/ml/string_hash @elastic/ml-ui
x-pack/packages/ml/url_state @elastic/ml-ui x-pack/packages/ml/url_state @elastic/ml-ui

View file

@ -356,10 +356,12 @@
"@kbn/logging-mocks": "link:packages/kbn-logging-mocks", "@kbn/logging-mocks": "link:packages/kbn-logging-mocks",
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl", "@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
"@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils", "@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils",
"@kbn/ml-date-picker": "link:x-pack/packages/ml/date_picker",
"@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined", "@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined",
"@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object", "@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object",
"@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage", "@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage",
"@kbn/ml-nested-property": "link:x-pack/packages/ml/nested_property", "@kbn/ml-nested-property": "link:x-pack/packages/ml/nested_property",
"@kbn/ml-query-utils": "link:x-pack/packages/ml/query_utils",
"@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash", "@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash",
"@kbn/ml-url-state": "link:x-pack/packages/ml/url_state", "@kbn/ml-url-state": "link:x-pack/packages/ml/url_state",
"@kbn/monaco": "link:packages/kbn-monaco", "@kbn/monaco": "link:packages/kbn-monaco",

View file

@ -838,6 +838,8 @@
"@kbn/maps-plugin/*": ["x-pack/plugins/maps/*"], "@kbn/maps-plugin/*": ["x-pack/plugins/maps/*"],
"@kbn/ml-agg-utils": ["x-pack/packages/ml/agg_utils"], "@kbn/ml-agg-utils": ["x-pack/packages/ml/agg_utils"],
"@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"], "@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"],
"@kbn/ml-date-picker": ["x-pack/packages/ml/date_picker"],
"@kbn/ml-date-picker/*": ["x-pack/packages/ml/date_picker/*"],
"@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"], "@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"],
"@kbn/ml-is-defined/*": ["x-pack/packages/ml/is_defined/*"], "@kbn/ml-is-defined/*": ["x-pack/packages/ml/is_defined/*"],
"@kbn/ml-is-populated-object": ["x-pack/packages/ml/is_populated_object"], "@kbn/ml-is-populated-object": ["x-pack/packages/ml/is_populated_object"],
@ -848,6 +850,8 @@
"@kbn/ml-nested-property/*": ["x-pack/packages/ml/nested_property/*"], "@kbn/ml-nested-property/*": ["x-pack/packages/ml/nested_property/*"],
"@kbn/ml-plugin": ["x-pack/plugins/ml"], "@kbn/ml-plugin": ["x-pack/plugins/ml"],
"@kbn/ml-plugin/*": ["x-pack/plugins/ml/*"], "@kbn/ml-plugin/*": ["x-pack/plugins/ml/*"],
"@kbn/ml-query-utils": ["x-pack/packages/ml/query_utils"],
"@kbn/ml-query-utils/*": ["x-pack/packages/ml/query_utils/*"],
"@kbn/ml-string-hash": ["x-pack/packages/ml/string_hash"], "@kbn/ml-string-hash": ["x-pack/packages/ml/string_hash"],
"@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"], "@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"],
"@kbn/ml-url-state": ["x-pack/packages/ml/url_state"], "@kbn/ml-url-state": ["x-pack/packages/ml/url_state"],

View file

@ -44,7 +44,7 @@
"xpack.main": "legacy/plugins/xpack_main", "xpack.main": "legacy/plugins/xpack_main",
"xpack.maps": ["plugins/maps"], "xpack.maps": ["plugins/maps"],
"xpack.aiops": ["packages/ml/aiops_components", "plugins/aiops"], "xpack.aiops": ["packages/ml/aiops_components", "plugins/aiops"],
"xpack.ml": ["plugins/ml"], "xpack.ml": ["packages/ml/date_picker", "plugins/ml"],
"xpack.monitoring": ["plugins/monitoring"], "xpack.monitoring": ["plugins/monitoring"],
"xpack.osquery": ["plugins/osquery"], "xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab", "xpack.painlessLab": "plugins/painless_lab",

View file

@ -0,0 +1,3 @@
# @kbn/ml-date-picker
Date picker related components to be used in UIs maintained by the @elastic/ml-ui team.

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
DatePickerContextProvider,
type DatePickerDependencies,
} from './src/hooks/use_date_picker_context';
export {
useRefreshIntervalUpdates,
useTimefilter,
useTimeRangeUpdates,
} from './src/hooks/use_timefilter';
export { DatePickerWrapper } from './src/components/date_picker_wrapper';
export {
FullTimeRangeSelector,
type FullTimeRangeSelectorProps,
} from './src/components/full_time_range_selector';
export {
getTimeFilterRange,
type TimeRange,
} from './src/services/full_time_range_selector_service';
export { type GetTimeFieldRangeResponse } from './src/services/types';
export { mlTimefilterRefresh$, type Refresh } from './src/services/timefilter_refresh_service';
export { type FrozenTierPreference, FROZEN_TIER_PREFERENCE } from './src/storage';

View file

@ -5,4 +5,8 @@
* 2.0. * 2.0.
*/ */
export { DatePickerWrapper } from './date_picker_wrapper'; module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/date_picker'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-date-picker",
"owner": "@elastic/ml-ui"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/ml-date-picker",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -5,20 +5,24 @@
* 2.0. * 2.0.
*/ */
import { mount } from 'enzyme'; import '@testing-library/jest-dom';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { EuiSuperDatePicker } from '@elastic/eui'; import { EuiSuperDatePicker } from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import type { UI_SETTINGS } from '@kbn/data-plugin/common';
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { useDatePickerContext } from '../hooks/use_date_picker_context';
import { useToastNotificationService } from '../../../services/toast_notification_service'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
import { DatePickerWrapper } from './date_picker_wrapper'; import { DatePickerWrapper } from './date_picker_wrapper';
jest.mock('@elastic/eui', () => { jest.mock('@elastic/eui', () => {
const EuiButtonMock = jest.fn(() => {
return null;
});
const EuiSuperDatePickerMock = jest.fn(() => { const EuiSuperDatePickerMock = jest.fn(() => {
return null; return null;
}); });
@ -29,6 +33,9 @@ jest.mock('@elastic/eui', () => {
return <>{children}</>; return <>{children}</>;
}); });
return { return {
useEuiBreakpoint: jest.fn(() => 'mediaQuery @media only screen and (max-width: 1199px)'),
useIsWithinMaxBreakpoint: jest.fn(() => false),
EuiButton: EuiButtonMock,
EuiSuperDatePicker: EuiSuperDatePickerMock, EuiSuperDatePicker: EuiSuperDatePickerMock,
EuiFlexGroup: EuiFlexGroupMock, EuiFlexGroup: EuiFlexGroupMock,
EuiFlexItem: EuiFlexItemMock, EuiFlexItem: EuiFlexItemMock,
@ -43,67 +50,82 @@ jest.mock('@kbn/ml-url-state', () => {
}; };
}); });
jest.mock('../../../contexts/kibana/use_timefilter'); jest.mock('../hooks/use_timefilter', () => ({
useRefreshIntervalUpdates: jest.fn(),
jest.mock('../../../services/toast_notification_service'); useTimefilter: () => {
jest.mock('../../../contexts/kibana', () => ({
useMlKibana: () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { of } = require('rxjs'); const { of } = require('rxjs');
return { return {
services: { getRefreshIntervalUpdate$: of(),
uiSettings: {
get: jest.fn().mockReturnValue([
{
from: 'now/d',
to: 'now/d',
display: 'Today',
},
{
from: 'now/w',
to: 'now/w',
display: 'This week',
},
]),
},
data: {
query: {
timefilter: {
timefilter: {
getRefreshInterval: jest.fn(),
setRefreshInterval: jest.fn(),
getTime: jest.fn(() => {
return { from: '', to: '' };
}),
isAutoRefreshSelectorEnabled: jest.fn(() => true),
isTimeRangeSelectorEnabled: jest.fn(() => true),
getRefreshIntervalUpdate$: jest.fn(),
getTimeUpdate$: jest.fn(),
getEnabledUpdated$: jest.fn(),
},
history: { get: jest.fn() },
},
},
},
mlServices: {
httpService: {
getLoadingCount$: of(0),
},
},
theme: {
theme$: of(),
},
},
}; };
}, },
useTimeRangeUpdates: jest.fn(() => {
return { from: '', to: '' };
}),
})); }));
jest.mock('../hooks/use_date_picker_context', () => ({
useDatePickerContext: jest.fn(),
}));
const mockContextFactory = (addWarning: jest.Mock<void, []>) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { of } = require('rxjs');
const mockedUiSettingsKeys = {} as typeof UI_SETTINGS;
const mockedToMountPoint = jest.fn();
const mockedWrapWithTheme = jest.fn();
return () => ({
notifications: {
toasts: { addWarning },
},
uiSettings: {
get: jest.fn().mockReturnValue([
{
from: 'now/d',
to: 'now/d',
display: 'Today',
},
{
from: 'now/w',
to: 'now/w',
display: 'This week',
},
]),
},
data: {
query: {
timefilter: {
timefilter: {
getRefreshInterval: jest.fn(),
setRefreshInterval: jest.fn(),
getTime: jest.fn(() => {
return { from: '', to: '' };
}),
isAutoRefreshSelectorEnabled: jest.fn(() => true),
isTimeRangeSelectorEnabled: jest.fn(() => true),
getRefreshIntervalUpdate$: jest.fn(),
getTimeUpdate$: jest.fn(),
getEnabledUpdated$: jest.fn(),
},
history: { get: jest.fn() },
},
},
},
theme: {
theme$: of(),
},
uiSettingsKeys: mockedUiSettingsKeys,
toMountPoint: mockedToMountPoint,
wrapWithTheme: mockedWrapWithTheme,
});
};
const MockedEuiSuperDatePicker = EuiSuperDatePicker as jest.MockedFunction< const MockedEuiSuperDatePicker = EuiSuperDatePicker as jest.MockedFunction<
typeof EuiSuperDatePicker typeof EuiSuperDatePicker
>; >;
describe('Navigation Menu: <DatePickerWrapper />', () => { describe('<DatePickerWrapper />', () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers({ legacyFakeTimers: true }); jest.useFakeTimers({ legacyFakeTimers: true });
MockedEuiSuperDatePicker.mockClear(); MockedEuiSuperDatePicker.mockClear();
@ -113,12 +135,16 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
test('Minimal initialization.', () => { test('Minimal initialization.', async () => {
const refreshListener = jest.fn(); const refreshListener = jest.fn();
const refreshSubscription = mlTimefilterRefresh$.subscribe(refreshListener); const refreshSubscription = mlTimefilterRefresh$.subscribe(refreshListener);
const wrapper = mount(<DatePickerWrapper />); const displayWarningSpy = jest.fn(() => {});
expect(wrapper.find(DatePickerWrapper)).toHaveLength(1);
(useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
render(<DatePickerWrapper />);
expect(refreshListener).toBeCalledTimes(0); expect(refreshListener).toBeCalledTimes(0);
refreshSubscription.unsubscribe(); refreshSubscription.unsubscribe();
@ -130,9 +156,7 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
const displayWarningSpy = jest.fn(() => {}); const displayWarningSpy = jest.fn(() => {});
(useToastNotificationService as jest.Mock).mockReturnValueOnce({ (useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
displayWarningToast: displayWarningSpy,
});
// act // act
render(<DatePickerWrapper />); render(<DatePickerWrapper />);
@ -151,9 +175,7 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
const displayWarningSpy = jest.fn(() => {}); const displayWarningSpy = jest.fn(() => {});
(useToastNotificationService as jest.Mock).mockReturnValueOnce({ (useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
displayWarningToast: displayWarningSpy,
});
// act // act
render(<DatePickerWrapper />); render(<DatePickerWrapper />);

View file

@ -5,13 +5,14 @@
* 2.0. * 2.0.
*/ */
import { css } from '@emotion/react';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { import {
useEuiBreakpoint,
useIsWithinMaxBreakpoint,
EuiButton, EuiButton,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
@ -19,19 +20,19 @@ import {
OnRefreshProps, OnRefreshProps,
OnTimeChangeProps, OnTimeChangeProps,
} from '@elastic/eui'; } from '@elastic/eui';
import type { TimeRange } from '@kbn/es-query';
import { TimeHistoryContract } from '@kbn/data-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { wrapWithTheme, toMountPoint } from '@kbn/kibana-react-plugin/public'; import type { TimeRange } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { TimeHistoryContract } from '@kbn/data-plugin/public';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
import { useMlKibana } from '../../../contexts/kibana'; import { useRefreshIntervalUpdates, useTimeRangeUpdates } from '../hooks/use_timefilter';
import { import { useDatePickerContext } from '../hooks/use_date_picker_context';
useRefreshIntervalUpdates, import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
useTimeRangeUpdates,
} from '../../../contexts/kibana/use_timefilter'; const DEFAULT_REFRESH_INTERVAL_MS = 5000;
import { useToastNotificationService } from '../../../services/toast_notification_service'; const DATE_PICKER_MAX_WIDTH = '540px';
interface TimePickerQuickRange { interface TimePickerQuickRange {
from: string; from: string;
@ -66,16 +67,46 @@ function updateLastRefresh(timeRange?: OnRefreshProps) {
mlTimefilterRefresh$.next({ lastRefresh: Date.now(), ...(timeRange ? { timeRange } : {}) }); mlTimefilterRefresh$.next({ lastRefresh: Date.now(), ...(timeRange ? { timeRange } : {}) });
} }
const DEFAULT_REFRESH_INTERVAL_MS = 5000; /**
* DatePickerWrapper React Component props interface
*/
interface DatePickerWrapperProps {
/**
* Boolean flag to be passed on to `EuiSuperDatePicker`.
*/
isAutoRefreshOnly?: boolean;
/**
* Boolean flag to indicate loading state.
*/
isLoading?: boolean;
/**
* Boolean flag to enforce showing/hiding the refresh button.
*/
showRefresh?: boolean;
}
export const DatePickerWrapper: FC = () => { /**
const { services } = useMlKibana(); * DatePickerWrapper React Component
const config = services.uiSettings; *
const theme$ = services.theme.theme$; * @type {FC<DatePickerWrapperProps>}
* @param props - `DatePickerWrapper` component props
* @returns {React.ReactElement} The DatePickerWrapper component.
*/
export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
const { isAutoRefreshOnly, isLoading = false, showRefresh } = props;
const {
data,
notifications: { toasts },
theme: { theme$ },
uiSettings: config,
uiSettingsKeys,
wrapWithTheme,
toMountPoint,
} = useDatePickerContext();
const { httpService } = services.mlServices; const isWithinLBreakpoint = useIsWithinMaxBreakpoint('l');
const { timefilter, history } = services.data.query.timefilter;
const { displayWarningToast } = useToastNotificationService(); const { timefilter, history } = data.query.timefilter;
const [globalState, setGlobalState] = useUrlState('_g'); const [globalState, setGlobalState] = useUrlState('_g');
const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history); const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history);
@ -117,7 +148,6 @@ export const DatePickerWrapper: FC = () => {
[setGlobalState] [setGlobalState]
); );
const [isLoading, setIsLoading] = useState(false);
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges()); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges());
const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState( const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState(
timefilter.isAutoRefreshSelectorEnabled() timefilter.isAutoRefreshSelectorEnabled()
@ -140,16 +170,16 @@ export const DatePickerWrapper: FC = () => {
// Only warn about short interval with enabled auto-refresh. // Only warn about short interval with enabled auto-refresh.
if (!isTooShort || refreshInterval.pause) return; if (!isTooShort || refreshInterval.pause) return;
displayWarningToast( toasts.addWarning(
{ {
title: isResolvedFromUrlState title: isResolvedFromUrlState
? i18n.translate('xpack.ml.datePicker.shortRefreshIntervalURLWarningMessage', { ? i18n.translate('xpack.ml.datePicker.shortRefreshIntervalURLWarningMessage', {
defaultMessage: defaultMessage:
'The refresh interval in the URL is shorter than the minimum supported by Machine Learning.', 'The refresh interval in the URL is shorter than the minimum supported interval.',
}) })
: i18n.translate('xpack.ml.datePicker.shortRefreshIntervalTimeFilterWarningMessage', { : i18n.translate('xpack.ml.datePicker.shortRefreshIntervalTimeFilterWarningMessage', {
defaultMessage: defaultMessage:
'The refresh interval in Advanced Settings is shorter than the minimum supported by Machine Learning.', 'The refresh interval in Advanced Settings is shorter than the minimum supported interval.',
}), }),
text: toMountPoint( text: toMountPoint(
wrapWithTheme( wrapWithTheme(
@ -160,7 +190,7 @@ export const DatePickerWrapper: FC = () => {
})} })}
> >
<FormattedMessage <FormattedMessage
id="xpack.ml.pageRefreshResetButton" id="xpack.ml.datePicker.pageRefreshResetButton"
defaultMessage="Set to {defaultInterval}" defaultMessage="Set to {defaultInterval}"
values={{ values={{
defaultInterval: `${DEFAULT_REFRESH_INTERVAL_MS / 1000}s`, defaultInterval: `${DEFAULT_REFRESH_INTERVAL_MS / 1000}s`,
@ -186,7 +216,7 @@ export const DatePickerWrapper: FC = () => {
const dateFormat = config.get('dateFormat'); const dateFormat = config.get('dateFormat');
const timePickerQuickRanges = config.get<TimePickerQuickRange[]>( const timePickerQuickRanges = config.get<TimePickerQuickRange[]>(
UI_SETTINGS.TIMEPICKER_QUICK_RANGES uiSettingsKeys.TIMEPICKER_QUICK_RANGES
); );
const commonlyUsedRanges = useMemo( const commonlyUsedRanges = useMemo(
@ -202,12 +232,6 @@ export const DatePickerWrapper: FC = () => {
useEffect(() => { useEffect(() => {
const subscriptions = new Subscription(); const subscriptions = new Subscription();
subscriptions.add(
httpService.getLoadingCount$.subscribe((v) => {
setIsLoading(v !== 0);
})
);
const enabledUpdated$ = timefilter.getEnabledUpdated$(); const enabledUpdated$ = timefilter.getEnabledUpdated$();
if (enabledUpdated$ !== undefined) { if (enabledUpdated$ !== undefined) {
subscriptions.add( subscriptions.add(
@ -250,19 +274,21 @@ export const DatePickerWrapper: FC = () => {
setRefreshInterval({ pause, value }); setRefreshInterval({ pause, value });
} }
const datePickerWidth = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l'])]: {
maxWidth: DATE_PICKER_MAX_WIDTH,
},
});
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? ( return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
<EuiFlexGroup <EuiFlexGroup gutterSize="s" alignItems="center">
gutterSize="s" <EuiFlexItem grow={false} css={datePickerWidth}>
alignItems="center"
className="mlNavigationMenu__datePickerWrapper"
>
<EuiFlexItem grow={false}>
<EuiSuperDatePicker <EuiSuperDatePicker
isLoading={isLoading} isLoading={isLoading}
start={time.from} start={time.from}
end={time.to} end={time.to}
isPaused={refreshInterval.pause} isPaused={refreshInterval.pause}
isAutoRefreshOnly={!isTimeRangeSelectorEnabled} isAutoRefreshOnly={!isTimeRangeSelectorEnabled || isAutoRefreshOnly}
refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS} refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS}
onTimeChange={updateTimeFilter} onTimeChange={updateTimeFilter}
onRefresh={updateLastRefresh} onRefresh={updateLastRefresh}
@ -270,23 +296,24 @@ export const DatePickerWrapper: FC = () => {
recentlyUsedRanges={recentlyUsedRanges} recentlyUsedRanges={recentlyUsedRanges}
dateFormat={dateFormat} dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges} commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={{ iconOnly: isWithinLBreakpoint }}
/> />
</EuiFlexItem> </EuiFlexItem>
{isTimeRangeSelectorEnabled ? null : ( {showRefresh === true || !isTimeRangeSelectorEnabled ? (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiButton <EuiButton
fill fill
color="primary" color="primary"
iconType={'refresh'} iconType={'refresh'}
onClick={() => updateLastRefresh()} onClick={() => updateLastRefresh()}
data-test-subj={`mlRefreshPageButton${isLoading ? ' loading' : ' loaded'}`} data-test-subj={`mlDatePickerRefreshPageButton${isLoading ? ' loading' : ' loaded'}`}
isLoading={isLoading} isLoading={isLoading}
> >
<FormattedMessage id="xpack.ml.pageRefreshButton" defaultMessage="Refresh" /> <FormattedMessage id="xpack.ml.datePicker.pageRefreshButton" defaultMessage="Refresh" />
</EuiButton> </EuiButton>
</EuiFlexItem> </EuiFlexItem>
)} ) : null}
</EuiFlexGroup> </EuiFlexGroup>
) : null; ) : null;
}; };

View file

@ -6,16 +6,30 @@
*/ */
import React from 'react'; import React from 'react';
import { shallowWithIntl } from '@kbn/test-jest-helpers'; import { render, fireEvent } from '@testing-library/react';
import { FullTimeRangeSelector } from '.';
import type { Query } from '@kbn/es-query'; import type { Query } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import {
DatePickerContextProvider,
type DatePickerDependencies,
} from '../hooks/use_date_picker_context';
import { FROZEN_TIER_PREFERENCE } from '../storage';
import { FullTimeRangeSelector } from './full_time_range_selector';
const mockDependencies = {
notifications: {},
} as DatePickerDependencies;
// Create a mock for the setFullTimeRange function in the service. // Create a mock for the setFullTimeRange function in the service.
// The mock is hoisted to the top, so need to prefix the mock function // The mock is hoisted to the top, so need to prefix the mock function
// with 'mock' so it can be used lazily. // with 'mock' so it can be used lazily.
const mockSetFullTimeRange = jest.fn((indexPattern: DataView, query: Query) => true); const mockSetFullTimeRange = jest.fn((indexPattern: DataView, query: Query) => true);
jest.mock('./full_time_range_selector_service', () => ({ jest.mock('../services/full_time_range_selector_service', () => ({
setFullTimeRange: (indexPattern: DataView, query: Query) => setFullTimeRange: (indexPattern: DataView, query: Query) =>
mockSetFullTimeRange(indexPattern, query), mockSetFullTimeRange(indexPattern, query),
})); }));
@ -44,24 +58,25 @@ describe('FullTimeRangeSelector', () => {
query, query,
}; };
test('renders the selector', () => {
const props = {
...requiredProps,
disabled: false,
};
const wrapper = shallowWithIntl(<FullTimeRangeSelector {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('calls setFullTimeRange on clicking button', () => { test('calls setFullTimeRange on clicking button', () => {
const props = { const props = {
...requiredProps, ...requiredProps,
disabled: false, disabled: false,
frozenDataPreference: FROZEN_TIER_PREFERENCE.EXCLUDE,
setFrozenDataPreference: jest.fn(),
timefilter: {} as TimefilterContract,
}; };
const wrapper = shallowWithIntl(<FullTimeRangeSelector {...props} />); const { getByText } = render(
wrapper.find('EuiButton').simulate('click'); <IntlProvider locale="en">
<DatePickerContextProvider {...mockDependencies}>
<FullTimeRangeSelector {...props} />
</DatePickerContextProvider>
</IntlProvider>
);
fireEvent.click(getByText(/use full data/i));
expect(mockSetFullTimeRange).toHaveBeenCalled(); expect(mockSetFullTimeRange).toHaveBeenCalled();
}); });
}); });

View file

@ -5,14 +5,10 @@
* 2.0. * 2.0.
*/ */
// TODO Consolidate with near duplicate component `FullTimeRangeSelector` in
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx`
import React, { FC, useCallback, useMemo, useState } from 'react'; import React, { FC, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { TimefilterContract } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-plugin/common';
import { import {
EuiButton, EuiButton,
EuiButtonIcon, EuiButtonIcon,
@ -24,40 +20,74 @@ import {
EuiRadioGroupOption, EuiRadioGroupOption,
EuiToolTip, EuiToolTip,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useStorage } from '@kbn/ml-local-storage';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
type GetTimeFieldRangeResponse,
setFullTimeRange,
} from './full_time_range_selector_service';
import {
AIOPS_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type AiOpsKey,
type AiOpsStorageMapped,
type FrozenTierPreference,
} from '../../types/storage';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-plugin/common';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDatePickerContext } from '../hooks/use_date_picker_context';
import { setFullTimeRange } from '../services/full_time_range_selector_service';
import type { GetTimeFieldRangeResponse } from '../services/types';
import { FROZEN_TIER_PREFERENCE, type FrozenTierPreference } from '../storage';
/**
* FullTimeRangeSelectorProps React Component props interface
*/
export interface FullTimeRangeSelectorProps { export interface FullTimeRangeSelectorProps {
/**
* Frozen data preference ('exclude-frozen' | 'include-frozen')
*/
frozenDataPreference: FrozenTierPreference;
/**
* Callback to set frozen data preference.
* @param value - The updated frozen data preference.
*/
setFrozenDataPreference: (value: FrozenTierPreference | undefined) => void;
/**
* timefilter service.
*/
timefilter: TimefilterContract; timefilter: TimefilterContract;
/**
* Current data view.
*/
dataView: DataView; dataView: DataView;
/**
* Boolean flag to enable/disable the full time range button.
*/
disabled: boolean; disabled: boolean;
/**
* Optional DSL query.
*/
query?: QueryDslQueryContainer; query?: QueryDslQueryContainer;
callback?: (a: GetTimeFieldRangeResponse) => void; /**
* Optional callback.
* @param value - The time field range response.
*/
callback?: (value: GetTimeFieldRangeResponse) => void;
} }
export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({ /**
timefilter, * Component for rendering a button which automatically sets the range of the time filter
dataView, * to the time range of data in the index(es) mapped to the supplied Kibana data view or query.
query, *
disabled, * @type {FC<FullTimeRangeSelectorProps>}
callback, * @param props - `FullTimeRangeSelectorProps` component props
}) => { * @returns {React.ReactElement} The FullTimeRangeSelector component.
*/
export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = (props) => {
const {
frozenDataPreference,
setFrozenDataPreference,
timefilter,
dataView,
query,
disabled,
callback,
} = props;
const { const {
http, http,
notifications: { toasts }, notifications: { toasts },
} = useAiopsAppContext(); } = useDatePickerContext();
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
const setRange = useCallback( const setRange = useCallback(
@ -77,7 +107,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
} catch (e) { } catch (e) {
toasts.addDanger( toasts.addDanger(
i18n.translate( i18n.translate(
'xpack.aiops.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', 'xpack.ml.datePicker.fullTimeRangeSelector.errorSettingTimeRangeNotification',
{ {
defaultMessage: 'An error occurred setting the time range.', defaultMessage: 'An error occurred setting the time range.',
} }
@ -90,15 +120,6 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
const [isPopoverOpen, setPopover] = useState(false); const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_FROZEN_TIER_PREFERENCE>
>(
AIOPS_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE
);
const setPreference = useCallback( const setPreference = useCallback(
(id: string) => { (id: string) => {
setFrozenDataPreference(id as FrozenTierPreference); setFrozenDataPreference(id as FrozenTierPreference);
@ -121,7 +142,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
{ {
id: FROZEN_TIER_PREFERENCE.EXCLUDE, id: FROZEN_TIER_PREFERENCE.EXCLUDE,
label: i18n.translate( label: i18n.translate(
'xpack.aiops.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', 'xpack.ml.datePicker.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
{ {
defaultMessage: 'Exclude frozen data tier', defaultMessage: 'Exclude frozen data tier',
} }
@ -130,7 +151,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
{ {
id: FROZEN_TIER_PREFERENCE.INCLUDE, id: FROZEN_TIER_PREFERENCE.INCLUDE,
label: i18n.translate( label: i18n.translate(
'xpack.aiops.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', 'xpack.ml.datePicker.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
{ {
defaultMessage: 'Include frozen data tier', defaultMessage: 'Include frozen data tier',
} }
@ -157,12 +178,12 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
() => () =>
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
<FormattedMessage <FormattedMessage
id="xpack.aiops.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip" id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
defaultMessage="Use full range of data excluding frozen data tier." defaultMessage="Use full range of data excluding frozen data tier."
/> />
) : ( ) : (
<FormattedMessage <FormattedMessage
id="xpack.aiops.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip" id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
defaultMessage="Use full range of data including frozen data tier, which might have slower search results." defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
/> />
), ),
@ -175,10 +196,10 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
<EuiButton <EuiButton
isDisabled={disabled} isDisabled={disabled}
onClick={() => setRange(dataView, query, true)} onClick={() => setRange(dataView, query, true)}
data-test-subj="aiopsExplainLogRatesSpikeButtonUseFullData" data-test-subj="mlDatePickerButtonUseFullData"
> >
<FormattedMessage <FormattedMessage
id="xpack.aiops.index.fullTimeRangeSelector.useFullDataButtonLabel" id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataButtonLabel"
defaultMessage="Use full data" defaultMessage="Use full data"
/> />
</EuiButton> </EuiButton>
@ -192,7 +213,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
size="m" size="m"
iconType="boxesVertical" iconType="boxesVertical"
aria-label={i18n.translate( aria-label={i18n.translate(
'xpack.aiops.index.fullTimeRangeSelector.moreOptionsButtonAriaLabel', 'xpack.ml.datePicker.fullTimeRangeSelector.moreOptionsButtonAriaLabel',
{ {
defaultMessage: 'More options', defaultMessage: 'More options',
} }

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { createContext, useContext, type FC } from 'react';
import type { UI_SETTINGS } from '@kbn/data-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { CoreSetup, IUiSettingsClient, ThemeServiceStart } from '@kbn/core/public';
import type { HttpStart } from '@kbn/core/public';
import type { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
/**
* Date Picker Dependencies to be passed on via `DatePickerContextProvider`.
*/
export interface DatePickerDependencies {
/**
* data plugin
*/
data: DataPublicPluginStart;
/**
* http service
*/
http: HttpStart;
/**
* notifications service
*/
notifications: CoreSetup['notifications'];
/**
* EUI theme
*/
theme: ThemeServiceStart;
/**
* Kibana UI advanced settings
*/
uiSettings: IUiSettingsClient;
/**
* Kibana UI advanced settings keys.
*/
uiSettingsKeys: typeof UI_SETTINGS;
/**
* helper to be used with notifications.
*/
wrapWithTheme: typeof wrapWithTheme;
/**
* helper to be used with notifications.
*/
toMountPoint: typeof toMountPoint;
}
/**
* The context holding the date picker dependencies.
*/
export const DatePickerContext = createContext<DatePickerDependencies | undefined>(undefined);
/**
* Custom hook to return date picker dependencies.
* @returns `DatePickerDependencies`
*/
export const useDatePickerContext = (): DatePickerDependencies => {
const datePickerContext = useContext(DatePickerContext);
// if `undefined`, throw an error
if (datePickerContext === undefined) {
throw new Error('datePickerContext was used outside of its Provider');
}
return datePickerContext;
};
/**
* React Component that acts as a wrapper for DatePickerContext.
*
* @type {FC<DatePickerDependencies>}
* @param props - The component props
* @returns {React.ReactElement} The DatePickerContextProvider component.
*/
export const DatePickerContextProvider: FC<DatePickerDependencies> = (props) => {
const { children, ...deps } = props;
return <DatePickerContext.Provider value={deps}>{children}</DatePickerContext.Provider>;
};

View file

@ -6,31 +6,35 @@
*/ */
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks';
import { useDatePickerContext } from './use_date_picker_context';
import { useTimefilter } from './use_timefilter'; import { useTimefilter } from './use_timefilter';
jest.mock('./kibana_context', () => ({ jest.mock('./use_date_picker_context');
useMlKibana: () => {
return { const mockContextFactory = (
services: { isAutoRefreshSelectorEnabled: boolean = true,
data: { isTimeRangeSelectorEnabled: boolean = true
query: { ) => ({
timefilter: { data: {
timefilter: { query: {
disableTimeRangeSelector: jest.fn(), timefilter: {
disableAutoRefreshSelector: jest.fn(), timefilter: {
enableTimeRangeSelector: jest.fn(), disableTimeRangeSelector: jest.fn(),
enableAutoRefreshSelector: jest.fn(), disableAutoRefreshSelector: jest.fn(),
}, enableTimeRangeSelector: jest.fn(),
}, enableAutoRefreshSelector: jest.fn(),
}, isAutoRefreshSelectorEnabled: jest.fn(() => isAutoRefreshSelectorEnabled),
isTimeRangeSelectorEnabled: jest.fn(() => isTimeRangeSelectorEnabled),
}, },
}, },
}; },
}, },
})); });
describe('useTimefilter', () => { describe('useTimefilter', () => {
test('will not trigger any date picker settings by default', () => { test('will not trigger any date picker settings by default', () => {
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory());
const { result } = renderHook(() => useTimefilter()); const { result } = renderHook(() => useTimefilter());
const timefilter = result.current; const timefilter = result.current;
@ -41,6 +45,8 @@ describe('useTimefilter', () => {
}); });
test('custom disabled overrides', () => { test('custom disabled overrides', () => {
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory());
const { result } = renderHook(() => const { result } = renderHook(() =>
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }) useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false })
); );
@ -53,6 +59,8 @@ describe('useTimefilter', () => {
}); });
test('custom enabled overrides', () => { test('custom enabled overrides', () => {
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory(false, false));
const { result } = renderHook(() => const { result } = renderHook(() =>
useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }) useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true })
); );

View file

@ -9,24 +9,41 @@ import { useEffect, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { useAiopsAppContext } from './use_aiops_app_context';
import type { TimeRange } from '@kbn/es-query';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import { useDatePickerContext } from './use_date_picker_context';
/**
* Options interface for the `useTimefilter` custom hook.
*/
interface UseTimefilterOptions { interface UseTimefilterOptions {
/**
* Boolean flag to enable/disable the time range selector
*/
timeRangeSelector?: boolean; timeRangeSelector?: boolean;
/**
* Boolean flag to enable/disable the auto refresh selector
*/
autoRefreshSelector?: boolean; autoRefreshSelector?: boolean;
} }
export const useTimefilter = ({ /**
timeRangeSelector, * Custom hook to get the timefilter from runtime dependencies.
autoRefreshSelector, *
}: UseTimefilterOptions = {}) => { * @param {UseTimefilterOptions} options - time filter options
* @returns {TimefilterContract} timefilter
*/
export const useTimefilter = (options: UseTimefilterOptions = {}): TimefilterContract => {
const { timeRangeSelector, autoRefreshSelector } = options;
const { const {
data: { data: {
query: { query: {
timefilter: { timefilter }, timefilter: { timefilter },
}, },
}, },
} = useAiopsAppContext(); } = useDatePickerContext();
useEffect(() => { useEffect(() => {
if (timeRangeSelector === true && !timefilter.isTimeRangeSelectorEnabled()) { if (timeRangeSelector === true && !timefilter.isTimeRangeSelectorEnabled()) {
@ -45,6 +62,11 @@ export const useTimefilter = ({
return timefilter; return timefilter;
}; };
/**
* Custom hook to return refresh interval updates from the `refreshIntervalObservable$` observable.
*
* @returns refresh interval update
*/
export const useRefreshIntervalUpdates = () => { export const useRefreshIntervalUpdates = () => {
const timefilter = useTimefilter(); const timefilter = useTimefilter();
@ -56,7 +78,13 @@ export const useRefreshIntervalUpdates = () => {
return useObservable(refreshIntervalObservable$, timefilter.getRefreshInterval()); return useObservable(refreshIntervalObservable$, timefilter.getRefreshInterval());
}; };
export const useTimeRangeUpdates = (absolute = false) => { /**
* Custom hook to return time range updates from the `timeChangeObservable$` observable.
*
* @param absolute - flag to enforce absolute times
* @returns time range update
*/
export const useTimeRangeUpdates = (absolute = false): TimeRange => {
const timefilter = useTimefilter(); const timefilter = useTimefilter();
const getTimeCallback = useMemo(() => { const getTimeCallback = useMemo(() => {

View file

@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import dateMath from '@kbn/datemath';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ToastsStart, HttpStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { addExcludeFrozenToQuery } from '@kbn/ml-query-utils';
import { getTimeFieldRange } from './time_field_range';
import type { GetTimeFieldRangeResponse } from './types';
/**
* Determines the full available time range of the given Data View and updates
* the timefilter accordingly.
*
* @param timefilter - TimefilterContract
* @param dataView - DataView
* @param toasts - ToastsStart
* @param http - HttpStart
* @param query - optional query
* @param excludeFrozenData - optional boolean flag
* @returns {GetTimeFieldRangeResponse}
*/
export async function setFullTimeRange(
timefilter: TimefilterContract,
dataView: DataView,
toasts: ToastsStart,
http: HttpStart,
query?: QueryDslQueryContainer,
excludeFrozenData?: boolean
): Promise<GetTimeFieldRangeResponse> {
try {
const runtimeMappings = dataView.getRuntimeMappings();
const resp = await getTimeFieldRange({
index: dataView.getIndexPattern(),
timeFieldName: dataView.timeFieldName,
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
http,
});
if (resp.start.epoch && resp.end.epoch) {
timefilter.setTime({
from: moment(resp.start.epoch).toISOString(),
to: moment(resp.end.epoch).toISOString(),
});
} else {
toasts.addWarning({
title: i18n.translate('xpack.ml.datePicker.fullTimeRangeSelector.noResults', {
defaultMessage: 'No results match your search criteria',
}),
});
}
return resp;
} catch (resp) {
toasts.addDanger(
i18n.translate(
'xpack.ml.datePicker.fullTimeRangeSelector.errorSettingTimeRangeNotification',
{
defaultMessage: 'An error occurred setting the time range.',
}
)
);
return resp;
}
}
/**
* Return type for the `getTimeFilterRange` function.
*/
export interface TimeRange {
/**
* From timestamp.
*/
from: number;
/**
* To timestamp.
*/
to: number;
}
/**
* Function to get the time filter range as timestamps.
*
* @param timefilter - The timefilter
* @returns TimeRange
*/
export function getTimeFilterRange(timefilter: TimefilterContract): TimeRange {
const fromMoment = dateMath.parse(timefilter.getTime().from);
const toMoment = dateMath.parse(timefilter.getTime().to);
const from = fromMoment !== undefined ? fromMoment.valueOf() : 0;
const to = toMoment !== undefined ? toMoment.valueOf() : 0;
return {
to,
from,
};
}

View file

@ -5,38 +5,50 @@
* 2.0. * 2.0.
*/ */
// TODO Consolidate with near duplicate service in
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts`
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { HttpStart } from '@kbn/core/public'; import type { HttpStart } from '@kbn/core/public';
export interface GetTimeFieldRangeResponse { import type { GetTimeFieldRangeResponse } from './types';
success: boolean;
start: { epoch: number; string: string }; /**
end: { epoch: number; string: string }; * Options definition for the `getTimeFieldRange` function.
*/
interface GetTimeFieldRangeOptions {
/**
* The index to be queried.
*/
index: string;
/**
* Optional time field name.
*/
timeFieldName?: string;
/**
* Optional DSL query.
*/
query?: QueryDslQueryContainer;
/**
* Optional runtime mappings.
*/
runtimeMappings?: estypes.MappingRuntimeFields;
/**
* HTTP client
*/
http: HttpStart;
} }
export async function getTimeFieldRange({ /**
index, *
timeFieldName, * @param options - GetTimeFieldRangeOptions
query, * @returns GetTimeFieldRangeResponse
runtimeMappings, */
http, export async function getTimeFieldRange(options: GetTimeFieldRangeOptions) {
}: { const { http, ...body } = options;
index: string;
timeFieldName?: string;
query?: QueryDslQueryContainer;
runtimeMappings?: estypes.MappingRuntimeFields;
http: HttpStart;
}) {
const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings });
return await http.fetch<GetTimeFieldRangeResponse>({ return await http.fetch<GetTimeFieldRangeResponse>({
path: `/internal/file_upload/time_field_range`, path: `/internal/file_upload/time_field_range`,
method: 'POST', method: 'POST',
body, body: JSON.stringify(body),
}); });
} }

View file

@ -5,14 +5,23 @@
* 2.0. * 2.0.
*/ */
// TODO Consolidate with near duplicate service in
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts`
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
/**
* State definition of `mlTimefilterRefresh$` observable.
*/
export interface Refresh { export interface Refresh {
/**
* Timestamp of the last time a refresh got triggered.
*/
lastRefresh: number; lastRefresh: number;
/**
* The time range triggered by the refresh.
*/
timeRange?: { start: string; end: string }; timeRange?: { start: string; end: string };
} }
export const aiopsRefresh$ = new Subject<Refresh>(); /**
* Observable to hold `Refresh` state.
*/
export const mlTimefilterRefresh$ = new Subject<Refresh>();

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Time definition for `GetTimeFieldRangeResponse` start/end attributes.
*/
interface GetTimeFieldRangeResponseTime {
/**
* Timestamp
*/
epoch: number;
/**
* String representation
*/
string: string;
}
/**
* Response interface for the `setFullTimeRange` function.
*/
export interface GetTimeFieldRangeResponse {
/**
* Success boolean flag.
*/
success: boolean;
/**
* Start time of the time range.
*/
start: GetTimeFieldRangeResponseTime;
/**
* End time of the time range.
*/
end: GetTimeFieldRangeResponseTime;
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Local storage options to include/exclude frozen tier.
*/
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
/**
* Union type of `FROZEN_TIER_PREFERENCE` options.
*/
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];

View file

@ -0,0 +1,31 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/es-query",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/ml-url-state",
"@kbn/data-plugin",
"@kbn/datemath",
"@kbn/core",
"@kbn/data-views-plugin",
"@kbn/ml-is-populated-object",
"@kbn/kibana-react-plugin",
"@kbn/ml-query-utils",
]
}

View file

@ -8,7 +8,7 @@
/** /**
* Checks whether the supplied argument is not `undefined` and not `null`. * Checks whether the supplied argument is not `undefined` and not `null`.
* *
* @param argument * @param argument - argument to check whether it is defined.
* @returns boolean * @returns boolean
*/ */
export function isDefined<T>(argument: T | undefined | null): argument is T { export function isDefined<T>(argument: T | undefined | null): argument is T {

View file

@ -0,0 +1,3 @@
# @kbn/ml-query-utils
Query utilities.

View file

@ -5,4 +5,4 @@
* 2.0. * 2.0.
*/ */
export { FullTimeRangeSelector } from './full_time_range_selector'; export { addExcludeFrozenToQuery } from './src/add_exclude_frozen_to_query';

View file

@ -5,4 +5,8 @@
* 2.0. * 2.0.
*/ */
export { DatePickerWrapper } from './date_picker_wrapper'; module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/query_utils'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-query-utils",
"owner": "@elastic/ml-ui"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/ml-query-utils",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { addExcludeFrozenToQuery } from './query_utils'; import { addExcludeFrozenToQuery } from './add_exclude_frozen_to_query';
describe('Util: addExcludeFrozenToQuery()', () => { describe('Util: addExcludeFrozenToQuery()', () => {
test('Validation checks.', () => { test('Validation checks.', () => {

View file

@ -9,6 +9,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isPopulatedObject } from '@kbn/ml-is-populated-object';
/**
* Extends an existing query with a clause to exclude the frozen tier.
*
* @param originalQuery - the original query
* @returns the original query exluding the frozen tier
*/
export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => {
const FROZEN_TIER_TERM = { const FROZEN_TIER_TERM = {
term: { term: {
@ -31,11 +37,11 @@ export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer |
delete query.match_all; delete query.match_all;
if (isPopulatedObject(query.bool)) { if (isPopulatedObject(query.bool)) {
// Must_not can be both arrays or singular object // `must_not` can be both arrays or singular object
if (Array.isArray(query.bool.must_not)) { if (Array.isArray(query.bool.must_not)) {
query.bool.must_not.push(FROZEN_TIER_TERM); query.bool.must_not.push(FROZEN_TIER_TERM);
} else { } else {
// If there's already a must_not condition // If there's already a `must_not` condition
if (isPopulatedObject(query.bool.must_not)) { if (isPopulatedObject(query.bool.must_not)) {
query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM];
} }

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/ml-is-populated-object",
]
}

View file

@ -8,13 +8,9 @@
// TODO Consolidate with duplicate query utils in // TODO Consolidate with duplicate query utils in
// `x-pack/plugins/data_visualizer/common/utils/query_utils.ts` // `x-pack/plugins/data_visualizer/common/utils/query_utils.ts`
import { cloneDeep } from 'lodash';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Query } from '@kbn/es-query'; import type { Query } from '@kbn/es-query';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils'; import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils';
import type { GroupTableItem } from '../../components/spike_analysis_table/types'; import type { GroupTableItem } from '../../components/spike_analysis_table/types';
@ -93,46 +89,3 @@ export function buildBaseFilterCriteria(
return filterCriteria; return filterCriteria;
} }
export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => {
const FROZEN_TIER_TERM = {
term: {
_tier: {
value: 'data_frozen',
},
},
};
if (!originalQuery) {
return {
bool: {
must_not: [FROZEN_TIER_TERM],
},
};
}
const query = cloneDeep(originalQuery);
delete query.match_all;
if (isPopulatedObject(query.bool)) {
// Must_not can be both arrays or singular object
if (Array.isArray(query.bool.must_not)) {
query.bool.must_not.push(FROZEN_TIER_TERM);
} else {
// If there's already a must_not condition
if (isPopulatedObject(query.bool.must_not)) {
query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM];
}
if (query.bool.must_not === undefined) {
query.bool.must_not = [FROZEN_TIER_TERM];
}
}
} else {
query.bool = {
must_not: [FROZEN_TIER_TERM],
};
}
return query;
};

View file

@ -18,12 +18,12 @@ import { type DataViewField } from '@kbn/data-views-plugin/public';
import { startWith } from 'rxjs'; import { startWith } from 'rxjs';
import type { Query, Filter } from '@kbn/es-query'; import type { Query, Filter } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state'; import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { import {
createMergedEsQuery, createMergedEsQuery,
getEsQueryFromSavedSearch, getEsQueryFromSavedSearch,
} from '../../application/utils/search_utils'; } from '../../application/utils/search_utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useTimefilter, useTimeRangeUpdates } from '../../hooks/use_time_filter';
import { useChangePointResults } from './use_change_point_agg_request'; import { useChangePointResults } from './use_change_point_agg_request';
import { type TimeBuckets, TimeBucketsInterval } from '../../../common/time_buckets'; import { type TimeBuckets, TimeBucketsInterval } from '../../../common/time_buckets';
import { useDataSource } from '../../hooks/use_data_source'; import { useDataSource } from '../../hooks/use_data_source';

View file

@ -6,12 +6,18 @@
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { pick } from 'lodash';
import { EuiSpacer } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common'; import { DataView } from '@kbn/data-views-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage'; import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state'; import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { DataSourceContext } from '../../hooks/use_data_source'; import { DataSourceContext } from '../../hooks/use_data_source';
import { SavedSearchSavedObject } from '../../application/utils/search_utils'; import { SavedSearchSavedObject } from '../../application/utils/search_utils';
@ -36,15 +42,25 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
savedSearch, savedSearch,
appDependencies, appDependencies,
}) => { }) => {
const datePickerDeps = {
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
return ( return (
<AiopsAppContext.Provider value={appDependencies}> <AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider> <UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch }}> <DataSourceContext.Provider value={{ dataView, savedSearch }}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}> <StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<PageHeader /> <DatePickerContextProvider {...datePickerDeps}>
<ChangePointDetectionContextProvider> <PageHeader />
<ChangePointDetectionPage /> <EuiSpacer />
</ChangePointDetectionContextProvider> <ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionContextProvider>
</DatePickerContextProvider>
</StorageContextProvider> </StorageContextProvider>
</DataSourceContext.Provider> </DataSourceContext.Provider>
</UrlStateProvider> </UrlStateProvider>

View file

@ -6,12 +6,15 @@
*/ */
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { FilterStateStore } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query';
import { useChangePointDetectionContext } from './change_point_detection_context'; import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { useDataSource } from '../../hooks/use_data_source'; import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useTimeRangeUpdates } from '../../hooks/use_time_filter';
import { useChangePointDetectionContext } from './change_point_detection_context';
import { fnOperationTypeMapping } from './constants'; import { fnOperationTypeMapping } from './constants';
export interface ChartComponentProps { export interface ChartComponentProps {

View file

@ -1,278 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// TODO Consolidate with duplicate component `DatePickerWrapper` in
// `x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx`
// `x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx`
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { debounce } from 'lodash';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import type { TimeRange } from '@kbn/es-query';
import { TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { useUrlState } from '@kbn/ml-url-state';
import { useRefreshIntervalUpdates, useTimeRangeUpdates } from '../../hooks/use_time_filter';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { aiopsRefresh$ } from '../../application/services/timefilter_refresh_service';
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
interface TimePickerQuickRange {
from: string;
to: string;
display: string;
}
interface Duration {
start: string;
end: string;
}
interface RefreshInterval {
pause: boolean;
value: number;
}
function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) {
return function (): Duration[] {
return (
timeHistory.get()?.map(({ from, to }: TimeRange) => {
return {
start: from,
end: to,
};
}) ?? []
);
};
}
function updateLastRefresh(timeRange?: OnRefreshProps) {
aiopsRefresh$.next({ lastRefresh: Date.now(), timeRange });
}
export const DatePickerWrapper: FC = () => {
const services = useAiopsAppContext();
const { toasts } = services.notifications;
const config = services.uiSettings;
const { timefilter, history } = services.data.query.timefilter;
const theme$ = services.theme.theme$;
const [globalState, setGlobalState] = useUrlState('_g');
const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history);
const timeFilterRefreshInterval = useRefreshIntervalUpdates();
const time = useTimeRangeUpdates();
useEffect(
function syncTimRangeFromUrlState() {
if (globalState?.time !== undefined) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[globalState?.time?.from, globalState?.time?.to, globalState?.time?.ts]
);
useEffect(
function syncRefreshIntervalFromUrlState() {
if (globalState?.refreshInterval !== undefined) {
timefilter.setRefreshInterval({
pause: !!globalState?.refreshInterval?.pause,
value: globalState?.refreshInterval?.value,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[globalState?.refreshInterval]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const setRefreshInterval = useCallback(
debounce((refreshIntervalUpdate: RefreshInterval) => {
setGlobalState('refreshInterval', refreshIntervalUpdate, true);
}, 200),
[setGlobalState]
);
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges());
const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState(
timefilter.isAutoRefreshSelectorEnabled()
);
const [isTimeRangeSelectorEnabled, setIsTimeRangeSelectorEnabled] = useState(
timefilter.isTimeRangeSelectorEnabled()
);
const refreshInterval = useMemo(
(): RefreshInterval => globalState?.refreshInterval ?? timeFilterRefreshInterval,
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(globalState?.refreshInterval), timeFilterRefreshInterval]
);
useEffect(
function warnAboutShortRefreshInterval() {
const isResolvedFromUrlState = !!globalState?.refreshInterval;
const isTooShort = refreshInterval.value < DEFAULT_REFRESH_INTERVAL_MS;
// Only warn about short interval with enabled auto-refresh.
if (!isTooShort || refreshInterval.pause) return;
toasts.addWarning(
{
title: isResolvedFromUrlState
? i18n.translate('xpack.aiops.datePicker.shortRefreshIntervalURLWarningMessage', {
defaultMessage:
'The refresh interval in the URL is shorter than the minimum supported by Machine Learning.',
})
: i18n.translate(
'xpack.aiops.datePicker.shortRefreshIntervalTimeFilterWarningMessage',
{
defaultMessage:
'The refresh interval in Advanced Settings is shorter than the minimum supported by Machine Learning.',
}
),
text: toMountPoint(
wrapWithTheme(
<EuiButton
onClick={setRefreshInterval.bind(null, {
pause: refreshInterval.pause,
value: DEFAULT_REFRESH_INTERVAL_MS,
})}
>
<FormattedMessage
id="xpack.aiops.index.pageRefreshResetButton"
defaultMessage="Set to {defaultInterval}"
values={{
defaultInterval: `${DEFAULT_REFRESH_INTERVAL_MS / 1000}s`,
}}
/>
</EuiButton>,
theme$
)
),
},
{ toastLifeTimeMs: 30000 }
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(refreshInterval),
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(globalState?.refreshInterval),
setRefreshInterval,
]
);
const dateFormat = config.get('dateFormat');
const timePickerQuickRanges = config.get<TimePickerQuickRange[]>(
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
);
const commonlyUsedRanges = useMemo(
() =>
timePickerQuickRanges.map(({ from, to, display }) => ({
start: from,
end: to,
label: display,
})),
[timePickerQuickRanges]
);
useEffect(() => {
const subscriptions = new Subscription();
const enabledUpdated$ = timefilter.getEnabledUpdated$();
if (enabledUpdated$ !== undefined) {
subscriptions.add(
enabledUpdated$.subscribe((w) => {
setIsAutoRefreshSelectorEnabled(timefilter.isAutoRefreshSelectorEnabled());
setIsTimeRangeSelectorEnabled(timefilter.isTimeRangeSelectorEnabled());
})
);
}
return function cleanup() {
subscriptions.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateTimeFilter = useCallback(
({ start, end }: OnTimeChangeProps) => {
setRecentlyUsedRanges(getRecentlyUsedRanges());
setGlobalState('time', {
from: start,
to: end,
...(start === 'now' || end === 'now' ? { ts: Date.now() } : {}),
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setGlobalState]
);
function updateInterval({
isPaused: pause,
refreshInterval: value,
}: {
isPaused: boolean;
refreshInterval: number;
}) {
setRefreshInterval({ pause, value });
}
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
<EuiFlexGroup gutterSize="s" alignItems="center" css={{ width: '100%', minWidth: 0 }}>
<EuiFlexItem grow={true} css={{ width: '100%' }}>
<EuiSuperDatePicker
css={{ width: '100%', minWidth: 0 }}
start={time.from}
end={time.to}
isPaused={refreshInterval.pause}
isAutoRefreshOnly={!isTimeRangeSelectorEnabled}
refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS}
onTimeChange={updateTimeFilter}
onRefresh={updateLastRefresh}
onRefreshChange={updateInterval}
recentlyUsedRanges={recentlyUsedRanges}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
/>
</EuiFlexItem>
{isTimeRangeSelectorEnabled ? null : (
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
iconType={'refresh'}
onClick={() => updateLastRefresh()}
data-test-subj="aiOpsRefreshPageButton"
>
<FormattedMessage id="xpack.aiops.pageRefreshButton" defaultMessage="Refresh" />
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
) : null;
};

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { DatePickerWrapper } from './date_picker_wrapper';

View file

@ -6,6 +6,7 @@
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { pick } from 'lodash';
import { EuiCallOut } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui';
@ -16,6 +17,9 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage'; import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state'; import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { import {
SEARCH_QUERY_LANGUAGE, SEARCH_QUERY_LANGUAGE,
@ -24,6 +28,7 @@ import {
} from '../../application/utils/search_utils'; } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context'; import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { DataSourceContext } from '../../hooks/use_data_source';
import { AIOPS_STORAGE_KEYS } from '../../types/storage'; import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { SpikeAnalysisTableRowStateProvider } from '../spike_analysis_table/spike_analysis_table_row_provider'; import { SpikeAnalysisTableRowStateProvider } from '../spike_analysis_table/spike_analysis_table_row_provider';
@ -95,14 +100,25 @@ export const ExplainLogRateSpikesAppState: FC<ExplainLogRateSpikesAppStateProps>
); );
} }
const datePickerDeps = {
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
return ( return (
<AiopsAppContext.Provider value={appDependencies}> <AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider> <UrlStateProvider>
<SpikeAnalysisTableRowStateProvider> <DataSourceContext.Provider value={{ dataView, savedSearch }}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}> <SpikeAnalysisTableRowStateProvider>
<ExplainLogRateSpikesPage dataView={dataView} savedSearch={savedSearch} /> <StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
</StorageContextProvider> <DatePickerContextProvider {...datePickerDeps}>
</SpikeAnalysisTableRowStateProvider> <ExplainLogRateSpikesPage />
</DatePickerContextProvider>
</StorageContextProvider>
</SpikeAnalysisTableRowStateProvider>
</DataSourceContext.Provider>
</UrlStateProvider> </UrlStateProvider>
</AiopsAppContext.Provider> </AiopsAppContext.Provider>
); );

View file

@ -6,6 +6,7 @@
*/ */
import React, { useCallback, useEffect, useState, FC } from 'react'; import React, { useCallback, useEffect, useState, FC } from 'react';
import { import {
EuiEmptyPrompt, EuiEmptyPrompt,
EuiFlexGroup, EuiFlexGroup,
@ -13,34 +14,29 @@ import {
EuiHorizontalRule, EuiHorizontalRule,
EuiPageBody, EuiPageBody,
EuiPageContentBody_Deprecated as EuiPageContentBody, EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
EuiPanel, EuiPanel,
EuiTitle,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { WindowParameters } from '@kbn/aiops-utils'; import type { WindowParameters } from '@kbn/aiops-utils';
import type { ChangePoint } from '@kbn/ml-agg-utils'; import type { ChangePoint } from '@kbn/ml-agg-utils';
import { Filter, FilterStateStore, Query } from '@kbn/es-query'; import { Filter, FilterStateStore, Query } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { SavedSearch } from '@kbn/discover-plugin/public';
import { useUrlState, usePageUrlState } from '@kbn/ml-url-state'; import { useUrlState, usePageUrlState } from '@kbn/ml-url-state';
import { useCss } from '../../hooks/use_css';
import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { SearchQueryLanguage, SavedSearchSavedObject } from '../../application/utils/search_utils'; import { SearchQueryLanguage } from '../../application/utils/search_utils';
import { useData } from '../../hooks/use_data'; import { useData } from '../../hooks/use_data';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { DocumentCountContent } from '../document_count_content/document_count_content'; import { DocumentCountContent } from '../document_count_content/document_count_content';
import { DatePickerWrapper } from '../date_picker_wrapper';
import { SearchPanel } from '../search_panel'; import { SearchPanel } from '../search_panel';
import type { GroupTableItem } from '../spike_analysis_table/types';
import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider';
import { PageHeader } from '../page_header';
import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state'; import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state';
import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis'; import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis';
import type { GroupTableItem } from '../spike_analysis_table/types';
import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider';
function getDocumentCountStatsSplitLabel(changePoint?: ChangePoint, group?: GroupTableItem) { function getDocumentCountStatsSplitLabel(changePoint?: ChangePoint, group?: GroupTableItem) {
if (changePoint) { if (changePoint) {
@ -52,22 +48,9 @@ function getDocumentCountStatsSplitLabel(changePoint?: ChangePoint, group?: Grou
} }
} }
/** export const ExplainLogRateSpikesPage: FC = () => {
* ExplainLogRateSpikes props require a data view.
*/
interface ExplainLogRateSpikesPageProps {
/** The data view to analyze. */
dataView: DataView;
/** The saved search to analyze. */
savedSearch: SavedSearch | SavedSearchSavedObject | null;
}
export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
dataView,
savedSearch,
}) => {
const { aiopsPageHeader, dataViewTitleHeader } = useCss();
const { data: dataService } = useAiopsAppContext(); const { data: dataService } = useAiopsAppContext();
const { dataView, savedSearch } = useDataSource();
const { const {
currentSelectedChangePoint, currentSelectedChangePoint,
@ -186,40 +169,7 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
return ( return (
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}> <EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
<EuiFlexGroup gutterSize="none"> <PageHeader />
<EuiFlexItem>
<EuiPageContentHeader css={aiopsPageHeader}>
<EuiPageContentHeaderSection>
<div css={dataViewTitleHeader}>
<EuiTitle size={'s'}>
<h2>{dataView.getName()}</h2>
</EuiTitle>
</div>
</EuiPageContentHeaderSection>
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="s"
data-test-subj="aiopsTimeRangeSelectorSection"
>
{dataView.timeFieldName !== undefined && (
<EuiFlexItem grow={false}>
<FullTimeRangeSelector
dataView={dataView}
query={undefined}
disabled={false}
timefilter={timefilter}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<DatePickerWrapper />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeader>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule /> <EuiHorizontalRule />
<EuiPageContentBody> <EuiPageContentBody>
<EuiFlexGroup gutterSize="m" direction="column"> <EuiFlexGroup gutterSize="m" direction="column">

View file

@ -1,75 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// TODO Consolidate with duplicate service in
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts`
import moment from 'moment';
import { TimefilterContract } from '@kbn/data-plugin/public';
import dateMath from '@kbn/datemath';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ToastsStart, HttpStart } from '@kbn/core/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { getTimeFieldRange } from '../../application/services/time_field_range';
import { addExcludeFrozenToQuery } from '../../application/utils/query_utils';
export interface GetTimeFieldRangeResponse {
success: boolean;
start: { epoch: number; string: string };
end: { epoch: number; string: string };
}
export interface TimeRange {
from: number;
to: number;
}
export async function setFullTimeRange(
timefilter: TimefilterContract,
dataView: DataView,
toasts: ToastsStart,
http: HttpStart,
query?: QueryDslQueryContainer,
excludeFrozenData?: boolean
): Promise<GetTimeFieldRangeResponse> {
const runtimeMappings = dataView.getRuntimeMappings();
const resp = await getTimeFieldRange({
index: dataView.getIndexPattern(),
timeFieldName: dataView.timeFieldName,
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
http,
});
if (resp.start.epoch && resp.end.epoch) {
timefilter.setTime({
from: moment(resp.start.epoch).toISOString(),
to: moment(resp.end.epoch).toISOString(),
});
} else {
toasts.addWarning({
title: i18n.translate('xpack.aiops.index.fullTimeRangeSelector.noResults', {
defaultMessage: 'No results match your search criteria',
}),
});
}
return resp;
}
export function getTimeFilterRange(timefilter: TimefilterContract): TimeRange {
const fromMoment = dateMath.parse(timefilter.getTime().from);
const toMoment = dateMath.parse(timefilter.getTime().to);
const from = fromMoment !== undefined ? fromMoment.valueOf() : 0;
const to = toMoment !== undefined ? toMoment.valueOf() : 0;
return {
to,
from,
};
}

View file

@ -5,12 +5,18 @@
* 2.0. * 2.0.
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { pick } from 'lodash';
import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage'; import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state'; import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { DataSourceContext } from '../../hooks/use_data_source';
import { SavedSearchSavedObject } from '../../application/utils/search_utils'; import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage'; import { AIOPS_STORAGE_KEYS } from '../../types/storage';
@ -31,12 +37,23 @@ export const LogCategorizationAppState: FC<LogCategorizationAppStateProps> = ({
savedSearch, savedSearch,
appDependencies, appDependencies,
}) => { }) => {
const datePickerDeps = {
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
return ( return (
<AiopsAppContext.Provider value={appDependencies}> <AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider> <UrlStateProvider>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}> <DataSourceContext.Provider value={{ dataView, savedSearch }}>
<LogCategorizationPage dataView={dataView} savedSearch={savedSearch} /> <StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
</StorageContextProvider> <DatePickerContextProvider {...datePickerDeps}>
<LogCategorizationPage />
</DatePickerContextProvider>
</StorageContextProvider>
</DataSourceContext.Provider>
</UrlStateProvider> </UrlStateProvider>
</AiopsAppContext.Provider> </AiopsAppContext.Provider>
); );

View file

@ -5,59 +5,46 @@
* 2.0. * 2.0.
*/ */
import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; import React, { FC, useState, useEffect, useCallback, useMemo } from 'react';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { Filter, Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { import {
EuiButton, EuiButton,
EuiSpacer, EuiSpacer,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiPageBody, EuiPageBody,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
EuiTitle,
EuiComboBox, EuiComboBox,
EuiComboBoxOptionOption, EuiComboBoxOptionOption,
EuiFormRow, EuiFormRow,
EuiLoadingContent, EuiLoadingContent,
} from '@elastic/eui'; } from '@elastic/eui';
import { Filter, Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { DatePickerWrapper } from '../date_picker_wrapper'; import { useDataSource } from '../../hooks/use_data_source';
import { useCss } from '../../hooks/use_css';
import { useData } from '../../hooks/use_data'; import { useData } from '../../hooks/use_data';
import { SearchPanel } from '../search_panel'; import type { SearchQueryLanguage } from '../../application/utils/search_utils';
import type {
SearchQueryLanguage,
SavedSearchSavedObject,
} from '../../application/utils/search_utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
import { useCategorizeRequest } from './use_categorize_request'; import { SearchPanel } from '../search_panel';
import { PageHeader } from '../page_header';
import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request'; import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request';
import { useCategorizeRequest } from './use_categorize_request';
import { CategoryTable } from './category_table'; import { CategoryTable } from './category_table';
import { DocumentCountChart } from './document_count_chart'; import { DocumentCountChart } from './document_count_chart';
import { InformationText } from './information_text'; import { InformationText } from './information_text';
export interface LogCategorizationPageProps {
dataView: DataView;
savedSearch: SavedSearch | SavedSearchSavedObject | null;
}
const BAR_TARGET = 20; const BAR_TARGET = 20;
export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ export const LogCategorizationPage: FC = () => {
dataView,
savedSearch,
}) => {
const { aiopsPageHeader, dataViewTitleHeader } = useCss();
const { const {
notifications: { toasts }, notifications: { toasts },
} = useAiopsAppContext(); } = useAiopsAppContext();
const { dataView, savedSearch } = useDataSource();
const { runCategorizeRequest, cancelRequest } = useCategorizeRequest(); const { runCategorizeRequest, cancelRequest } = useCategorizeRequest();
const [aiopsListState, setAiopsListState] = useState(restorableDefaults); const [aiopsListState, setAiopsListState] = useState(restorableDefaults);
@ -219,41 +206,8 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({
}; };
return ( return (
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}> <EuiPageBody data-test-subj="aiopsLogCategorizationPage" paddingSize="none" panelled={false}>
<EuiFlexGroup gutterSize="none"> <PageHeader />
<EuiFlexItem>
<EuiPageContentHeader css={aiopsPageHeader}>
<EuiPageContentHeaderSection>
<div css={dataViewTitleHeader}>
<EuiTitle size="s">
<h2>{dataView.getName()}</h2>
</EuiTitle>
</div>
</EuiPageContentHeaderSection>
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="s"
data-test-subj="aiopsTimeRangeSelectorSection"
>
{dataView.timeFieldName !== undefined && (
<EuiFlexItem grow={false}>
<FullTimeRangeSelector
dataView={dataView}
query={undefined}
disabled={false}
timefilter={timefilter}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<DatePickerWrapper />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeader>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer /> <EuiSpacer />
<EuiFlexGroup gutterSize="none"> <EuiFlexGroup gutterSize="none">
<EuiFlexItem> <EuiFlexItem>

View file

@ -5,8 +5,10 @@
* 2.0. * 2.0.
*/ */
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
import { import {
useIsWithinMaxBreakpoint,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiSpacer, EuiSpacer,
@ -14,19 +16,40 @@ import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader, EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection, EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
} from '@elastic/eui'; } from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import { FullTimeRangeSelectorProps } from '../full_time_range_selector/full_time_range_selector'; import { useStorage } from '@kbn/ml-local-storage';
import {
useTimefilter,
DatePickerWrapper,
FullTimeRangeSelector,
type FullTimeRangeSelectorProps,
FROZEN_TIER_PREFERENCE,
} from '@kbn/ml-date-picker';
import { useCss } from '../../hooks/use_css'; import { useCss } from '../../hooks/use_css';
import { useDataSource } from '../../hooks/use_data_source'; import { useDataSource } from '../../hooks/use_data_source';
import { useTimefilter } from '../../hooks/use_time_filter'; import {
import { FullTimeRangeSelector } from '../full_time_range_selector'; AIOPS_FROZEN_TIER_PREFERENCE,
import { DatePickerWrapper } from '../date_picker_wrapper'; type AiOpsKey,
type AiOpsStorageMapped,
} from '../../types/storage';
export const PageHeader: FC = () => { export const PageHeader: FC = () => {
const { aiopsPageHeader, dataViewTitleHeader } = useCss(); const { aiopsPageHeader, dataViewTitleHeader } = useCss();
const [, setGlobalState] = useUrlState('_g'); const [, setGlobalState] = useUrlState('_g');
const { dataView } = useDataSource(); const { dataView } = useDataSource();
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_FROZEN_TIER_PREFERENCE>
>(
AIOPS_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE
);
const timefilter = useTimefilter({ const timefilter = useTimefilter({
timeRangeSelector: dataView.timeFieldName !== undefined, timeRangeSelector: dataView.timeFieldName !== undefined,
autoRefreshSelector: true, autoRefreshSelector: true,
@ -39,44 +62,54 @@ export const PageHeader: FC = () => {
[setGlobalState] [setGlobalState]
); );
return ( const hasValidTimeField = useMemo(
<> () => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '',
<EuiFlexGroup gutterSize="none"> [dataView.timeFieldName]
<EuiFlexItem> );
<EuiPageContentHeader css={aiopsPageHeader}>
<EuiPageContentHeaderSection>
<div css={dataViewTitleHeader}>
<EuiTitle size="s">
<h2>{dataView.getName()}</h2>
</EuiTitle>
</div>
</EuiPageContentHeaderSection>
<EuiFlexGroup const isWithinLBreakpoint = useIsWithinMaxBreakpoint('l');
alignItems="center"
justifyContent="flexEnd" return (
gutterSize="s" <EuiFlexGroup gutterSize="none">
data-test-subj="aiopsTimeRangeSelectorSection" <EuiFlexItem>
> <EuiPageContentHeader css={aiopsPageHeader}>
{dataView.timeFieldName !== undefined && ( <EuiPageContentHeaderSection>
<EuiFlexItem grow={false}> <div css={dataViewTitleHeader}>
<FullTimeRangeSelector <EuiTitle size="s">
dataView={dataView} <h2>{dataView.getName()}</h2>
query={undefined} </EuiTitle>
disabled={false} </div>
timefilter={timefilter} </EuiPageContentHeaderSection>
callback={updateTimeState}
/> {isWithinLBreakpoint ? <EuiSpacer size="m" /> : null}
</EuiFlexItem> <EuiFlexGroup
)} alignItems="center"
justifyContent="flexEnd"
gutterSize="s"
data-test-subj="aiopsTimeRangeSelectorSection"
>
{hasValidTimeField ? (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<DatePickerWrapper /> <FullTimeRangeSelector
frozenDataPreference={frozenDataPreference}
setFrozenDataPreference={setFrozenDataPreference}
dataView={dataView}
query={undefined}
disabled={false}
timefilter={timefilter}
callback={updateTimeState}
/>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> ) : null}
</EuiPageContentHeader> <EuiFlexItem grow={false}>
</EuiFlexItem> <DatePickerWrapper
</EuiFlexGroup> isAutoRefreshOnly={!hasValidTimeField}
<EuiSpacer /> showRefresh={!hasValidTimeField}
</> />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeader>
</EuiFlexItem>
</EuiFlexGroup>
); );
}; };

View file

@ -14,8 +14,13 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { CoreStart, CoreSetup, HttpStart, IUiSettingsClient } from '@kbn/core/public'; import type {
import type { ThemeServiceStart } from '@kbn/core/public'; CoreStart,
CoreSetup,
HttpStart,
IUiSettingsClient,
ThemeServiceStart,
} from '@kbn/core/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public';
export interface AiopsAppDependencies { export interface AiopsAppDependencies {

View file

@ -10,25 +10,23 @@ import { merge } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import type { ChangePoint } from '@kbn/ml-agg-utils'; import type { ChangePoint } from '@kbn/ml-agg-utils';
import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { Dictionary } from '@kbn/ml-url-state'; import type { Dictionary } from '@kbn/ml-url-state';
import { useTimeBuckets } from './use_time_buckets'; import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { useAiopsAppContext } from './use_aiops_app_context';
import { aiopsRefresh$ } from '../application/services/timefilter_refresh_service';
import type { DocumentStatsSearchStrategyParams } from '../get_document_stats'; import type { DocumentStatsSearchStrategyParams } from '../get_document_stats';
import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state'; import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state';
import { import {
getEsQueryFromSavedSearch, getEsQueryFromSavedSearch,
SavedSearchSavedObject, SavedSearchSavedObject,
} from '../application/utils/search_utils'; } from '../application/utils/search_utils';
import { useTimefilter } from './use_time_filter';
import { useDocumentCountStats } from './use_document_count_stats';
import type { GroupTableItem } from '../components/spike_analysis_table/types'; import type { GroupTableItem } from '../components/spike_analysis_table/types';
import { useTimeBuckets } from './use_time_buckets';
import { useAiopsAppContext } from './use_aiops_app_context';
import { useDocumentCountStats } from './use_document_count_stats';
const DEFAULT_BAR_TARGET = 75; const DEFAULT_BAR_TARGET = 75;
export const useData = ( export const useData = (
@ -153,7 +151,7 @@ export const useData = (
const timeUpdateSubscription = merge( const timeUpdateSubscription = merge(
timefilter.getAutoRefreshFetch$(), timefilter.getAutoRefreshFetch$(),
timefilter.getTimeUpdate$(), timefilter.getTimeUpdate$(),
aiopsRefresh$ mlTimefilterRefresh$
).subscribe(() => { ).subscribe(() => {
if (onUpdate) { if (onUpdate) {
onUpdate({ onUpdate({

View file

@ -15,7 +15,7 @@ export const DataSourceContext = createContext<{
savedSearch: SavedSearch | SavedSearchSavedObject | null; savedSearch: SavedSearch | SavedSearchSavedObject | null;
}>({ }>({
get dataView(): never { get dataView(): never {
throw new Error('Context is not implemented'); throw new Error('DataSourceContext is not implemented');
}, },
savedSearch: null, savedSearch: null,
}); });

View file

@ -5,16 +5,10 @@
* 2.0. * 2.0.
*/ */
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
export const AIOPS_FROZEN_TIER_PREFERENCE = 'aiops.frozenDataTierPreference'; export const AIOPS_FROZEN_TIER_PREFERENCE = 'aiops.frozenDataTierPreference';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type AiOps = Partial<{ export type AiOps = Partial<{
[AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; [AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
}> | null; }> | null;

View file

@ -44,6 +44,8 @@
"@kbn/es-types", "@kbn/es-types",
"@kbn/ml-url-state", "@kbn/ml-url-state",
"@kbn/ml-local-storage", "@kbn/ml-local-storage",
"@kbn/ml-date-picker",
"@kbn/ml-local-storage",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -1,295 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// TODO Consolidate with duplicate component `DatePickerWrapper` in
// `x-pack/plugins/aiops/public/components/date_picker_wrapper/date_picker_wrapper.tsx`
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { debounce } from 'lodash';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import type { TimeRange } from '@kbn/es-query';
import { TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import {
useRefreshIntervalUpdates,
useTimeRangeUpdates,
} from '../../../index_data_visualizer/hooks/use_time_filter';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { dataVisualizerRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service';
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
const DATE_PICKER_MAX_WIDTH = 540;
interface TimePickerQuickRange {
from: string;
to: string;
display: string;
}
interface Duration {
start: string;
end: string;
}
interface RefreshInterval {
pause: boolean;
value: number;
}
function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) {
return function (): Duration[] {
return (
timeHistory.get()?.map(({ from, to }: TimeRange) => {
return {
start: from,
end: to,
};
}) ?? []
);
};
}
function updateLastRefresh(timeRange?: OnRefreshProps) {
dataVisualizerRefresh$.next({ lastRefresh: Date.now(), timeRange });
}
// FIXME: Consolidate this component with ML and AIOps's component
export const DatePickerWrapper: FC<{
isAutoRefreshOnly?: boolean;
showRefresh?: boolean;
compact?: boolean;
}> = ({ isAutoRefreshOnly, showRefresh, compact = false }) => {
const {
services,
notifications: { toasts },
} = useDataVisualizerKibana();
const config = services.uiSettings;
const theme$ = services.theme.theme$;
const { timefilter, history } = services.data.query.timefilter;
const [globalState, setGlobalState] = useUrlState('_g');
const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history);
const timeFilterRefreshInterval = useRefreshIntervalUpdates();
const time = useTimeRangeUpdates();
useEffect(
function syncTimRangeFromUrlState() {
if (globalState?.time !== undefined) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[globalState?.time?.from, globalState?.time?.to, globalState?.time?.ts]
);
useEffect(
function syncRefreshIntervalFromUrlState() {
if (globalState?.refreshInterval !== undefined) {
timefilter.setRefreshInterval({
pause: !!globalState?.refreshInterval?.pause,
value: globalState?.refreshInterval?.value,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[globalState?.refreshInterval]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const setRefreshInterval = useCallback(
debounce((refreshIntervalUpdate: RefreshInterval) => {
setGlobalState('refreshInterval', refreshIntervalUpdate, true);
}, 200),
[setGlobalState]
);
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges());
const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState(
timefilter.isAutoRefreshSelectorEnabled()
);
const [isTimeRangeSelectorEnabled, setIsTimeRangeSelectorEnabled] = useState(
timefilter.isTimeRangeSelectorEnabled()
);
const refreshInterval = useMemo(
(): RefreshInterval => globalState?.refreshInterval ?? timeFilterRefreshInterval,
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(globalState?.refreshInterval), timeFilterRefreshInterval]
);
useEffect(
function warnAboutShortRefreshInterval() {
const isTooShort = refreshInterval.value < DEFAULT_REFRESH_INTERVAL_MS;
// Only warn about short interval with enabled auto-refresh.
if (!isTooShort || refreshInterval.pause) return;
toasts.warning({
title: i18n.translate(
'xpack.dataVisualizer.index.datePicker.shortRefreshIntervalURLWarningMessage',
{
defaultMessage:
'The refresh interval in the URL is shorter than the minimum supported by Machine Learning.',
}
),
body: wrapWithTheme(
<EuiButton
onClick={setRefreshInterval.bind(null, {
pause: refreshInterval.pause,
value: DEFAULT_REFRESH_INTERVAL_MS,
})}
>
<FormattedMessage
id="xpack.dataVisualizer.index.pageRefreshResetButton"
defaultMessage="Set to {defaultInterval}"
values={{
defaultInterval: `${DEFAULT_REFRESH_INTERVAL_MS / 1000}s`,
}}
/>
</EuiButton>,
theme$
),
toastLifeTimeMs: 30000,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(refreshInterval),
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(globalState?.refreshInterval),
setRefreshInterval,
]
);
const dateFormat = config.get('dateFormat');
const timePickerQuickRanges = config.get<TimePickerQuickRange[]>(
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
);
const commonlyUsedRanges = useMemo(
() =>
timePickerQuickRanges.map(({ from, to, display }) => ({
start: from,
end: to,
label: display,
})),
[timePickerQuickRanges]
);
useEffect(() => {
const subscriptions = new Subscription();
const enabledUpdated$ = timefilter.getEnabledUpdated$();
if (enabledUpdated$ !== undefined) {
subscriptions.add(
enabledUpdated$.subscribe((w) => {
setIsAutoRefreshSelectorEnabled(timefilter.isAutoRefreshSelectorEnabled());
setIsTimeRangeSelectorEnabled(timefilter.isTimeRangeSelectorEnabled());
})
);
}
return function cleanup() {
subscriptions.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateTimeFilter = useCallback(
({ start, end }: OnTimeChangeProps) => {
setRecentlyUsedRanges(getRecentlyUsedRanges());
setGlobalState('time', {
from: start,
to: end,
...(start === 'now' || end === 'now' ? { ts: Date.now() } : {}),
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setGlobalState]
);
function updateInterval({
isPaused: pause,
refreshInterval: value,
}: {
isPaused: boolean;
refreshInterval: number;
}) {
setRefreshInterval({ pause, value });
}
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
data-test-subj="mlNavigationMenuDatePickerWrapper"
>
<EuiFlexItem
grow={false}
css={
compact
? {
maxWidth: DATE_PICKER_MAX_WIDTH,
}
: null
}
>
<EuiSuperDatePicker
start={time.from}
end={time.to}
isPaused={refreshInterval.pause}
isAutoRefreshOnly={!isTimeRangeSelectorEnabled || isAutoRefreshOnly}
refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS}
onTimeChange={updateTimeFilter}
onRefresh={updateLastRefresh}
onRefreshChange={updateInterval}
recentlyUsedRanges={recentlyUsedRanges}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={{ iconOnly: compact }}
/>
</EuiFlexItem>
{showRefresh === true || !isTimeRangeSelectorEnabled ? (
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
iconType={'refresh'}
onClick={() => updateLastRefresh()}
data-test-subj="dataVisualizerRefreshPageButton"
>
<FormattedMessage
id="xpack.dataVisualizer.index.pageRefreshButton"
defaultMessage="Refresh"
/>
</EuiButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
) : null;
};

View file

@ -10,14 +10,11 @@ import { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { MutableRefObject } from 'react'; import { MutableRefObject } from 'react';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { mlTimefilterRefresh$, Refresh } from '@kbn/ml-date-picker';
import { getCompatibleLensDataType, getLensAttributes } from './lens_utils'; import { getCompatibleLensDataType, getLensAttributes } from './lens_utils';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query'; import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { FieldVisConfig } from '../../stats_table/types'; import { FieldVisConfig } from '../../stats_table/types';
import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context'; import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context';
import {
dataVisualizerRefresh$,
Refresh,
} from '../../../../index_data_visualizer/services/timefilter_refresh_service';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants'; import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { APP_ID } from '../../../../../../common/constants'; import { APP_ID } from '../../../../../../common/constants';
@ -36,7 +33,7 @@ export function getActions(
const refresh: Refresh = { const refresh: Refresh = {
lastRefresh: Date.now(), lastRefresh: Date.now(),
}; };
dataVisualizerRefresh$.next(refresh); mlTimefilterRefresh$.next(refresh);
}; };
// Navigate to Lens with prefilled chart for data field // Navigate to Lens with prefilled chart for data field
if (services.application?.capabilities?.visualize?.show === true && lensPlugin !== undefined) { if (services.application?.capabilities?.visualize?.show === true && lensPlugin !== undefined) {

View file

@ -11,7 +11,7 @@ import React, { FC, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { useEuiBreakpoint, EuiSpacer, EuiTitle } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import { isDefined } from '@kbn/ml-is-defined'; import { isDefined } from '@kbn/ml-is-defined';
@ -26,7 +26,6 @@ interface Props {
searchString?: string | { [key: string]: any }; searchString?: string | { [key: string]: any };
searchQueryLanguage?: string; searchQueryLanguage?: string;
getAdditionalLinks?: GetAdditionalLinks; getAdditionalLinks?: GetAdditionalLinks;
compact?: boolean;
} }
const ACTIONS_PANEL_WIDTH = '240px'; const ACTIONS_PANEL_WIDTH = '240px';
@ -36,7 +35,6 @@ export const ActionsPanel: FC<Props> = ({
searchString, searchString,
searchQueryLanguage, searchQueryLanguage,
getAdditionalLinks, getAdditionalLinks,
compact,
}) => { }) => {
const [globalState] = useUrlState('_g'); const [globalState] = useUrlState('_g');
@ -121,19 +119,17 @@ export const ActionsPanel: FC<Props> = ({
const showActionsPanel = const showActionsPanel =
discoverLink || (Array.isArray(asyncHrefCards) && asyncHrefCards.length > 0); discoverLink || (Array.isArray(asyncHrefCards) && asyncHrefCards.length > 0);
const dvActionsPanel = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l', 'xl'])]: {
width: ACTIONS_PANEL_WIDTH,
},
});
// Note we use display:none for the DataRecognizer section as it needs to be // Note we use display:none for the DataRecognizer section as it needs to be
// passed the recognizerResults object, and then run the recognizer check which // passed the recognizerResults object, and then run the recognizer check which
// controls whether the recognizer section is ultimately displayed. // controls whether the recognizer section is ultimately displayed.
return showActionsPanel ? ( return showActionsPanel ? (
<div <div data-test-subj="dataVisualizerActionsPanel" css={dvActionsPanel}>
data-test-subj="dataVisualizerActionsPanel"
css={
!compact &&
css`
width: ${ACTIONS_PANEL_WIDTH};
`
}
>
<EuiTitle size="s"> <EuiTitle size="s">
<h2> <h2>
<FormattedMessage <FormattedMessage

View file

@ -9,8 +9,8 @@ import React, { useEffect, useRef, useState } from 'react';
import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { mlTimefilterRefresh$, Refresh } from '@kbn/ml-date-picker';
import { useDataVisualizerKibana } from '../../../kibana_context'; import { useDataVisualizerKibana } from '../../../kibana_context';
import { dataVisualizerRefresh$, Refresh } from '../../services/timefilter_refresh_service';
export interface DataVisualizerDataViewManagementProps { export interface DataVisualizerDataViewManagementProps {
/** /**
@ -56,7 +56,7 @@ export function DataVisualizerDataViewManagement(props: DataVisualizerDataViewMa
const refresh: Refresh = { const refresh: Refresh = {
lastRefresh: Date.now(), lastRefresh: Date.now(),
}; };
dataVisualizerRefresh$.next(refresh); mlTimefilterRefresh$.next(refresh);
}, },
}); });
}; };

View file

@ -1,206 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { TimefilterContract } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-plugin/common';
import {
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiPopover,
EuiRadioGroup,
EuiRadioGroupOption,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { useStorage } from '@kbn/ml-local-storage';
import { setFullTimeRange } from './full_time_range_selector_service';
import { useDataVisualizerKibana } from '../../../kibana_context';
import {
DV_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type DVKey,
type DVStorageMapped,
type FrozenTierPreference,
} from '../../types/storage';
export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
interface Props {
timefilter: TimefilterContract;
dataView: DataView;
disabled: boolean;
query?: QueryDslQueryContainer;
callback?: (a: any) => void;
}
// Component for rendering a button which automatically sets the range of the time filter
// to the time range of data in the index(es) mapped to the supplied Kibana data view or query.
export const FullTimeRangeSelector: FC<Props> = ({
timefilter,
dataView,
query,
disabled,
callback,
}) => {
const {
services: {
notifications: { toasts },
},
} = useDataVisualizerKibana();
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
const setRange = useCallback(
async (i: DataView, q?: QueryDslQueryContainer, excludeFrozenData?: boolean) => {
try {
const fullTimeRange = await setFullTimeRange(timefilter, i, q, excludeFrozenData, toasts);
if (typeof callback === 'function') {
callback(fullTimeRange);
}
} catch (e) {
toasts.addDanger(
i18n.translate(
'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification',
{
defaultMessage: 'An error occurred setting the time range.',
}
)
);
}
},
[callback, timefilter, toasts]
);
const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
DVKey,
DVStorageMapped<typeof DV_FROZEN_TIER_PREFERENCE>
>(
DV_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE
);
const setPreference = useCallback(
(id: string) => {
setFrozenDataPreference(id as FrozenTierPreference);
setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE);
closePopover();
},
[dataView, query, setFrozenDataPreference, setRange]
);
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
const sortOptions: EuiRadioGroupOption[] = useMemo(() => {
return [
{
id: FROZEN_TIER_PREFERENCE.EXCLUDE,
label: i18n.translate(
'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
{
defaultMessage: 'Exclude frozen data tier',
}
),
},
{
id: FROZEN_TIER_PREFERENCE.INCLUDE,
label: i18n.translate(
'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
{
defaultMessage: 'Include frozen data tier',
}
),
},
];
}, []);
const popoverContent = useMemo(
() => (
<EuiPanel>
<EuiRadioGroup
options={sortOptions}
idSelected={frozenDataPreference}
onChange={setPreference}
compressed
/>
</EuiPanel>
),
[sortOptions, frozenDataPreference, setPreference]
);
const buttonTooltip = useMemo(
() =>
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
<FormattedMessage
id="xpack.dataVisualizer.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
defaultMessage="Use full range of data excluding frozen data tier."
/>
) : (
<FormattedMessage
id="xpack.dataVisualizer.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
/>
),
[frozenDataPreference]
);
return (
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
<EuiToolTip content={buttonTooltip}>
<EuiButton
isDisabled={disabled}
onClick={() => setRange(dataView, query, true)}
data-test-subj="dataVisualizerButtonUseFullData"
>
<FormattedMessage
id="xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel"
defaultMessage="Use full data"
/>
</EuiButton>
</EuiToolTip>
<EuiFlexItem grow={false}>
<EuiPopover
id={'mlFullTimeRangeSelectorOption'}
button={
<EuiButtonIcon
display="base"
size="m"
iconType="boxesVertical"
aria-label={i18n.translate(
'xpack.dataVisualizer.index.fullTimeRangeSelector.moreOptionsButtonAriaLabel',
{
defaultMessage: 'More options',
}
)}
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downRight"
>
{popoverContent}
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { TimefilterContract } from '@kbn/data-plugin/public';
import dateMath from '@kbn/datemath';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ToastsStart } from '@kbn/core/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { getTimeFieldRange } from '../../services/time_field_range';
import type { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request';
import { addExcludeFrozenToQuery } from '../../utils/query_utils';
export interface TimeRange {
from: number;
to: number;
}
export async function setFullTimeRange(
timefilter: TimefilterContract,
dataView: DataView,
query?: QueryDslQueryContainer,
excludeFrozenData?: boolean,
toasts?: ToastsStart
): Promise<GetTimeFieldRangeResponse> {
const runtimeMappings = dataView.getRuntimeMappings();
const resp = await getTimeFieldRange({
index: dataView.getIndexPattern(),
timeFieldName: dataView.timeFieldName,
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
});
if (resp.start.epoch && resp.end.epoch) {
timefilter.setTime({
from: moment(resp.start.epoch).toISOString(),
to: moment(resp.end.epoch).toISOString(),
});
} else {
toasts?.addWarning({
title: i18n.translate('xpack.dataVisualizer.index.fullTimeRangeSelector.noResults', {
defaultMessage: 'No results match your search criteria',
}),
});
}
return resp;
}
export function getTimeFilterRange(timefilter: TimefilterContract): TimeRange {
const fromMoment = dateMath.parse(timefilter.getTime().from);
const toMoment = dateMath.parse(timefilter.getTime().to);
const from = fromMoment !== undefined ? fromMoment.valueOf() : 0;
const to = toMoment !== undefined ? toMoment.valueOf() : 0;
return {
to,
from,
};
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { FullTimeRangeSelector } from './full_time_range_selector';
export type { TimeRange } from './full_time_range_selector_service';
export { getTimeFilterRange } from './full_time_range_selector_service';

View file

@ -5,10 +5,13 @@
* 2.0. * 2.0.
*/ */
import { css } from '@emotion/react';
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react'; import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import type { Required } from 'utility-types'; import type { Required } from 'utility-types';
import { import {
useEuiBreakpoint,
useIsWithinMaxBreakpoint,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiPageBody, EuiPageBody,
@ -26,15 +29,20 @@ import { Filter, FilterStateStore, Query } from '@kbn/es-query';
import { generateFilters } from '@kbn/data-plugin/public'; import { generateFilters } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
import {
DatePickerWrapper,
FullTimeRangeSelector,
FROZEN_TIER_PREFERENCE,
} from '@kbn/ml-date-picker';
import { useStorage } from '@kbn/ml-local-storage'; import { useStorage } from '@kbn/ml-local-storage';
import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
import { import {
DV_FROZEN_TIER_PREFERENCE,
DV_RANDOM_SAMPLER_PREFERENCE, DV_RANDOM_SAMPLER_PREFERENCE,
type DVKey, type DVKey,
type DVStorageMapped, type DVStorageMapped,
} from '../../types/storage'; } from '../../types/storage';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { import {
DataVisualizerTable, DataVisualizerTable,
ItemIdToExpandedRowMap, ItemIdToExpandedRowMap,
@ -57,7 +65,6 @@ import { OMIT_FIELDS } from '../../../../../common/constants';
import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
import { SearchPanel } from '../search_panel'; import { SearchPanel } from '../search_panel';
import { ActionsPanel } from '../actions_panel'; import { ActionsPanel } from '../actions_panel';
import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper';
import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { createMergedEsQuery } from '../../utils/saved_search_utils';
import { DataVisualizerDataViewManagement } from '../data_view_management'; import { DataVisualizerDataViewManagement } from '../data_view_management';
import { GetAdditionalLinks } from '../../../common/components/results_links'; import { GetAdditionalLinks } from '../../../common/components/results_links';
@ -125,7 +132,6 @@ export interface IndexDataVisualizerViewProps {
currentSavedSearch: SavedSearchSavedObject | null; currentSavedSearch: SavedSearchSavedObject | null;
currentSessionId?: string; currentSessionId?: string;
getAdditionalLinks?: GetAdditionalLinks; getAdditionalLinks?: GetAdditionalLinks;
compact?: boolean;
} }
export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => { export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => {
@ -136,6 +142,15 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
DVStorageMapped<typeof DV_RANDOM_SAMPLER_PREFERENCE> DVStorageMapped<typeof DV_RANDOM_SAMPLER_PREFERENCE>
>(DV_RANDOM_SAMPLER_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC); >(DV_RANDOM_SAMPLER_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
DVKey,
DVStorageMapped<typeof DV_FROZEN_TIER_PREFERENCE>
>(
DV_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE
);
const restorableDefaults = useMemo( const restorableDefaults = useMemo(
() => () =>
getDefaultDataVisualizerListState({ getDefaultDataVisualizerListState({
@ -161,7 +176,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
dataVisualizerProps.currentSavedSearch dataVisualizerProps.currentSavedSearch
); );
const { currentDataView, currentSessionId, getAdditionalLinks, compact } = dataVisualizerProps; const { currentDataView, currentSessionId, getAdditionalLinks } = dataVisualizerProps;
useEffect(() => { useEffect(() => {
if (dataVisualizerProps?.currentSavedSearch !== undefined) { if (dataVisualizerProps?.currentSavedSearch !== undefined) {
@ -458,14 +473,21 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
() => currentDataView.timeFieldName !== undefined && currentDataView.timeFieldName !== '', () => currentDataView.timeFieldName !== undefined && currentDataView.timeFieldName !== '',
[currentDataView.timeFieldName] [currentDataView.timeFieldName]
); );
const dvPageHeader = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l', 'xl'])]: {
flexDirection: 'column',
alignItems: 'flex-start',
},
});
const isWithinXl = useIsWithinMaxBreakpoint('xl');
return ( return (
<EuiPageBody data-test-subj="dataVisualizerIndexPage" paddingSize="none" panelled={false}> <EuiPageBody data-test-subj="dataVisualizerIndexPage" paddingSize="none" panelled={false}>
<EuiFlexGroup gutterSize="m"> <EuiFlexGroup gutterSize="m">
<EuiFlexItem> <EuiFlexItem>
<EuiPageContentHeader <EuiPageContentHeader data-test-subj="dataVisualizerPageHeader" css={dvPageHeader}>
data-test-subj="dataVisualizerPageHeader"
css={compact ? { flexDirection: 'column', alignItems: 'flex-start' } : null}
>
<EuiPageContentHeaderSection> <EuiPageContentHeaderSection>
<EuiFlexGroup <EuiFlexGroup
data-test-subj="dataViewTitleHeader" data-test-subj="dataViewTitleHeader"
@ -483,7 +505,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
</EuiFlexGroup> </EuiFlexGroup>
</EuiPageContentHeaderSection> </EuiPageContentHeaderSection>
{compact ? <EuiSpacer size="m" /> : null} {isWithinXl ? <EuiSpacer size="m" /> : null}
<EuiFlexGroup <EuiFlexGroup
alignItems="center" alignItems="center"
justifyContent="flexEnd" justifyContent="flexEnd"
@ -493,6 +515,8 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
{hasValidTimeField ? ( {hasValidTimeField ? (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<FullTimeRangeSelector <FullTimeRangeSelector
frozenDataPreference={frozenDataPreference}
setFrozenDataPreference={setFrozenDataPreference}
dataView={currentDataView} dataView={currentDataView}
query={undefined} query={undefined}
disabled={false} disabled={false}
@ -504,7 +528,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
<DatePickerWrapper <DatePickerWrapper
isAutoRefreshOnly={!hasValidTimeField} isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField} showRefresh={!hasValidTimeField}
compact={compact}
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
@ -513,7 +536,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
</EuiFlexGroup> </EuiFlexGroup>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<EuiPageContentBody> <EuiPageContentBody>
<EuiFlexGroup gutterSize="m" direction={compact ? 'column' : 'row'}> <EuiFlexGroup gutterSize="m" direction={isWithinXl ? 'column' : 'row'}>
<EuiFlexItem> <EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder> <EuiPanel hasShadow={false} hasBorder>
<SearchPanel <SearchPanel
@ -530,7 +553,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
setVisibleFieldNames={setVisibleFieldNames} setVisibleFieldNames={setVisibleFieldNames}
showEmptyFields={showEmptyFields} showEmptyFields={showEmptyFields}
onAddFilter={onAddFilter} onAddFilter={onAddFilter}
compact={compact}
/> />
{overallStats?.totalCount !== undefined && ( {overallStats?.totalCount !== undefined && (
@ -576,14 +598,13 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
/> />
</EuiPanel> </EuiPanel>
</EuiFlexItem> </EuiFlexItem>
{compact ? <EuiSpacer size="m" /> : null} {isWithinXl ? <EuiSpacer size="m" /> : null}
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<ActionsPanel <ActionsPanel
dataView={currentDataView} dataView={currentDataView}
searchQueryLanguage={searchQueryLanguage} searchQueryLanguage={searchQueryLanguage}
searchString={searchString} searchString={searchString}
getAdditionalLinks={getAdditionalLinks} getAdditionalLinks={getAdditionalLinks}
compact={compact}
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -7,7 +7,13 @@
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { useEuiBreakpoint, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import {
useEuiBreakpoint,
useIsWithinMaxBreakpoint,
EuiFlexItem,
EuiFlexGroup,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Query, Filter } from '@kbn/es-query'; import { Query, Filter } from '@kbn/es-query';
import type { TimeRange } from '@kbn/es-query'; import type { TimeRange } from '@kbn/es-query';
@ -45,7 +51,6 @@ interface Props {
}): void; }): void;
showEmptyFields: boolean; showEmptyFields: boolean;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
compact?: boolean;
} }
export const SearchPanel: FC<Props> = ({ export const SearchPanel: FC<Props> = ({
@ -60,7 +65,6 @@ export const SearchPanel: FC<Props> = ({
visibleFieldNames, visibleFieldNames,
setSearchParams, setSearchParams,
showEmptyFields, showEmptyFields,
compact,
}) => { }) => {
const { const {
services: { services: {
@ -139,6 +143,8 @@ export const SearchPanel: FC<Props> = ({
}, },
}); });
const isWithinXl = useIsWithinMaxBreakpoint('xl');
return ( return (
<EuiFlexGroup <EuiFlexGroup
gutterSize="none" gutterSize="none"
@ -168,7 +174,7 @@ export const SearchPanel: FC<Props> = ({
/> />
</EuiFlexItem> </EuiFlexItem>
{compact ? <EuiSpacer size="s" /> : null} {isWithinXl ? <EuiSpacer size="s" /> : null}
<EuiFlexItem grow={2} css={dvSearchPanelControls}> <EuiFlexItem grow={2} css={dvSearchPanelControls}>
<DataVisualizerFieldNamesFilter <DataVisualizerFieldNamesFilter
overallStats={overallStats} overallStats={overallStats}

View file

@ -5,6 +5,7 @@
* 2.0. * 2.0.
*/ */
import { pick } from 'lodash';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { CoreStart } from '@kbn/core/public'; import { CoreStart } from '@kbn/core/public';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -20,9 +21,12 @@ import {
EmbeddableOutput, EmbeddableOutput,
IContainer, IContainer,
} from '@kbn/embeddable-plugin/public'; } from '@kbn/embeddable-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { Query } from '@kbn/es-query'; import type { Query } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { SavedSearch } from '@kbn/discover-plugin/public'; import { SavedSearch } from '@kbn/discover-plugin/public';
import { SamplingOption } from '../../../../../common/types/field_stats'; import { SamplingOption } from '../../../../../common/types/field_stats';
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants'; import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
@ -215,18 +219,28 @@ export class DataVisualizerGridEmbeddable extends Embeddable<
const I18nContext = this.services[0].i18n.Context; const I18nContext = this.services[0].i18n.Context;
const services = { ...this.services[0], ...this.services[1] };
const datePickerDeps = {
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
ReactDOM.render( ReactDOM.render(
<I18nContext> <I18nContext>
<KibanaThemeProvider theme$={this.services[0].theme.theme$}> <KibanaThemeProvider theme$={this.services[0].theme.theme$}>
<KibanaContextProvider services={{ ...this.services[0], ...this.services[1] }}> <KibanaContextProvider services={services}>
<Suspense fallback={<EmbeddableLoading />}> <DatePickerContextProvider {...datePickerDeps}>
<IndexDataVisualizerViewWrapper <Suspense fallback={<EmbeddableLoading />}>
id={this.input.id} <IndexDataVisualizerViewWrapper
embeddableContext={this} id={this.input.id}
embeddableInput={this.getInput$()} embeddableContext={this}
onOutputChange={(output) => this.updateOutput(output)} embeddableInput={this.getInput$()}
/> onOutputChange={(output) => this.updateOutput(output)}
</Suspense> />
</Suspense>
</DatePickerContextProvider>
</KibanaContextProvider> </KibanaContextProvider>
</KibanaThemeProvider> </KibanaThemeProvider>
</I18nContext>, </I18nContext>,

View file

@ -15,13 +15,12 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import seedrandom from 'seedrandom'; import seedrandom from 'seedrandom';
import type { SamplingOption } from '@kbn/discover-plugin/public/application/main/components/field_stats_table/field_stats_table'; import type { SamplingOption } from '@kbn/discover-plugin/public/application/main/components/field_stats_table/field_stats_table';
import type { Dictionary } from '@kbn/ml-url-state'; import type { Dictionary } from '@kbn/ml-url-state';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import type { RandomSamplerOption } from '../constants/random_sampler'; import type { RandomSamplerOption } from '../constants/random_sampler';
import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state';
import { useDataVisualizerKibana } from '../../kibana_context'; import { useDataVisualizerKibana } from '../../kibana_context';
import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils'; import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils';
import type { MetricFieldsStats } from '../../common/components/stats_table/components/field_count_stats'; import type { MetricFieldsStats } from '../../common/components/stats_table/components/field_count_stats';
import { useTimefilter } from './use_time_filter';
import { dataVisualizerRefresh$ } from '../services/timefilter_refresh_service';
import { TimeBuckets } from '../../../../common/services/time_buckets'; import { TimeBuckets } from '../../../../common/services/time_buckets';
import type { FieldVisConfig } from '../../common/components/stats_table/types'; import type { FieldVisConfig } from '../../common/components/stats_table/types';
import { import {
@ -313,7 +312,7 @@ export const useDataVisualizerGridData = (
const timeUpdateSubscription = merge( const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(), timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(), timefilter.getAutoRefreshFetch$(),
dataVisualizerRefresh$ mlTimefilterRefresh$
).subscribe(() => { ).subscribe(() => {
if (onUpdate) { if (onUpdate) {
onUpdate({ onUpdate({

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { map } from 'rxjs/operators';
import { useDataVisualizerKibana } from '../../kibana_context';
interface UseTimefilterOptions {
timeRangeSelector?: boolean;
autoRefreshSelector?: boolean;
}
export const useTimefilter = ({
timeRangeSelector,
autoRefreshSelector,
}: UseTimefilterOptions = {}) => {
const { services } = useDataVisualizerKibana();
const { timefilter } = services.data.query.timefilter;
useEffect(() => {
if (timeRangeSelector === true) {
timefilter.enableTimeRangeSelector();
} else if (timeRangeSelector === false) {
timefilter.disableTimeRangeSelector();
}
if (autoRefreshSelector === true) {
timefilter.enableAutoRefreshSelector();
} else if (autoRefreshSelector === false) {
timefilter.disableAutoRefreshSelector();
}
}, [timeRangeSelector, autoRefreshSelector, timefilter]);
return timefilter;
};
export const useRefreshIntervalUpdates = () => {
const timefilter = useTimefilter();
return useObservable(
timefilter.getRefreshIntervalUpdate$().pipe(map(timefilter.getRefreshInterval)),
timefilter.getRefreshInterval()
);
};
export const useTimeRangeUpdates = (absolute = false) => {
const timefilter = useTimefilter();
const getTimeCallback = absolute
? timefilter.getAbsoluteTime.bind(timefilter)
: timefilter.getTime.bind(timefilter);
return useObservable(timefilter.getTimeUpdate$().pipe(map(getTimeCallback)), getTimeCallback());
};

View file

@ -5,19 +5,26 @@
* 2.0. * 2.0.
*/ */
import '../_index.scss'; import '../_index.scss';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { pick } from 'lodash';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { parse, stringify } from 'query-string'; import { parse, stringify } from 'query-string';
import { isEqual, throttle } from 'lodash'; import { isEqual } from 'lodash';
import { EuiResizeObserver } from '@elastic/eui';
import { encode } from '@kbn/rison'; import { encode } from '@kbn/rison';
import { SimpleSavedObject } from '@kbn/core/public'; import { SimpleSavedObject } from '@kbn/core/public';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import {
KibanaContextProvider,
KibanaThemeProvider,
toMountPoint,
wrapWithTheme,
} from '@kbn/kibana-react-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage'; import { StorageContextProvider } from '@kbn/ml-local-storage';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { getNestedProperty } from '@kbn/ml-nested-property'; import { getNestedProperty } from '@kbn/ml-nested-property';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { import {
Provider as UrlStateContextProvider, Provider as UrlStateContextProvider,
parseUrlState, parseUrlState,
@ -38,6 +45,8 @@ import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants'; import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
import { DV_STORAGE_KEYS } from './types/storage'; import { DV_STORAGE_KEYS } from './types/storage';
const XXL_BREAKPOINT = 1400;
const localStorage = new Storage(window.localStorage); const localStorage = new Storage(window.localStorage);
export interface DataVisualizerStateContextProviderProps { export interface DataVisualizerStateContextProviderProps {
@ -247,36 +256,15 @@ export const DataVisualizerStateContextProvider: FC<DataVisualizerStateContextPr
[history, urlSearchString] [history, urlSearchString]
); );
const [panelWidth, setPanelWidth] = useState(1600);
// eslint-disable-next-line react-hooks/exhaustive-deps
const resizeHandler = useCallback(
throttle((e: { width: number; height: number }) => {
// When window or table is resized,
// update the page body width
setPanelWidth(e.width);
}, 500),
[]
);
const compact = useMemo(() => panelWidth <= 1024, [panelWidth]);
return ( return (
<UrlStateContextProvider value={{ searchString: urlSearchString, setUrlState }}> <UrlStateContextProvider value={{ searchString: urlSearchString, setUrlState }}>
{currentDataView ? ( {currentDataView ? (
// Needs ResizeObserver to measure window width - side bar navigation <IndexDataVisualizerComponent
<EuiResizeObserver onResize={resizeHandler}> currentDataView={currentDataView}
{(resizeRef) => ( currentSavedSearch={currentSavedSearch}
<div ref={resizeRef}> currentSessionId={currentSessionId}
<IndexDataVisualizerComponent getAdditionalLinks={getAdditionalLinks}
currentDataView={currentDataView} />
currentSavedSearch={currentSavedSearch}
currentSessionId={currentSessionId}
getAdditionalLinks={getAdditionalLinks}
compact={compact}
/>
</div>
)}
</EuiResizeObserver>
) : ( ) : (
<div /> <div />
)} )}
@ -317,15 +305,30 @@ export const IndexDataVisualizer: FC<{
unifiedSearch, unifiedSearch,
...coreStart, ...coreStart,
}; };
const datePickerDeps = {
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
return ( return (
<KibanaThemeProvider theme$={coreStart.theme.theme$}> <KibanaThemeProvider
theme$={coreStart.theme.theme$}
modify={{
breakpoint: {
xxl: XXL_BREAKPOINT,
},
}}
>
<KibanaContextProvider services={{ ...services }}> <KibanaContextProvider services={{ ...services }}>
<StorageContextProvider storage={localStorage} storageKeys={DV_STORAGE_KEYS}> <StorageContextProvider storage={localStorage} storageKeys={DV_STORAGE_KEYS}>
<DataVisualizerStateContextProvider <DatePickerContextProvider {...datePickerDeps}>
IndexDataVisualizerComponent={IndexDataVisualizerView} <DataVisualizerStateContextProvider
getAdditionalLinks={getAdditionalLinks} IndexDataVisualizerComponent={IndexDataVisualizerView}
/> getAdditionalLinks={getAdditionalLinks}
/>
</DatePickerContextProvider>
</StorageContextProvider> </StorageContextProvider>
</KibanaContextProvider> </KibanaContextProvider>
</KibanaThemeProvider> </KibanaThemeProvider>

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Subject } from 'rxjs';
export interface Refresh {
lastRefresh: number;
timeRange?: { start: string; end: string };
}
export const dataVisualizerRefresh$ = new Subject<Refresh>();

View file

@ -5,20 +5,14 @@
* 2.0. * 2.0.
*/ */
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
import { RandomSamplerOption } from '../constants/random_sampler'; import { RandomSamplerOption } from '../constants/random_sampler';
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference'; export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';
export const DV_RANDOM_SAMPLER_PREFERENCE = 'dataVisualizer.randomSamplerPreference'; export const DV_RANDOM_SAMPLER_PREFERENCE = 'dataVisualizer.randomSamplerPreference';
export const DV_RANDOM_SAMPLER_P_VALUE = 'dataVisualizer.randomSamplerPValue'; export const DV_RANDOM_SAMPLER_P_VALUE = 'dataVisualizer.randomSamplerPValue';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type DV = Partial<{ export type DV = Partial<{
[DV_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; [DV_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
[DV_RANDOM_SAMPLER_PREFERENCE]: RandomSamplerOption; [DV_RANDOM_SAMPLER_PREFERENCE]: RandomSamplerOption;

View file

@ -1,76 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { addExcludeFrozenToQuery } from './query_utils';
describe('Util: addExcludeFrozenToQuery()', () => {
test('Validation checks.', () => {
expect(
addExcludeFrozenToQuery({
match_all: {},
bool: {
must: [
{
match_all: {},
},
],
},
})
).toMatchObject({
bool: {
must: [{ match_all: {} }],
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
},
});
expect(
addExcludeFrozenToQuery({
bool: {
must: [],
must_not: {
term: {
category: {
value: 'clothing',
},
},
},
},
})
).toMatchObject({
bool: {
must: [],
must_not: [
{ term: { category: { value: 'clothing' } } },
{ term: { _tier: { value: 'data_frozen' } } },
],
},
});
expect(
addExcludeFrozenToQuery({
bool: {
must: [],
must_not: [{ term: { category: { value: 'clothing' } } }],
},
})
).toMatchObject({
bool: {
must: [],
must_not: [
{ term: { category: { value: 'clothing' } } },
{ term: { _tier: { value: 'data_frozen' } } },
],
},
});
expect(addExcludeFrozenToQuery(undefined)).toMatchObject({
bool: {
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
},
});
});
});

View file

@ -53,6 +53,7 @@
"@kbn/ml-nested-property", "@kbn/ml-nested-property",
"@kbn/ml-url-state", "@kbn/ml-url-state",
"@kbn/ml-local-storage", "@kbn/ml-local-storage",
"@kbn/ml-date-picker",
"@kbn/ml-is-defined", "@kbn/ml-is-defined",
], ],
"exclude": [ "exclude": [

View file

@ -5,6 +5,8 @@
* 2.0. * 2.0.
*/ */
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
import { EntityFieldType } from './anomalies'; import { EntityFieldType } from './anomalies';
export const ML_ENTITY_FIELDS_CONFIG = 'ml.singleMetricViewer.partitionFields' as const; export const ML_ENTITY_FIELDS_CONFIG = 'ml.singleMetricViewer.partitionFields' as const;
@ -14,14 +16,6 @@ export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels'; export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels';
export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt'; export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type PartitionFieldConfig = export type PartitionFieldConfig =
| { | {
/** /**

View file

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { cloneDeep } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => {
const FROZEN_TIER_TERM = {
term: {
_tier: {
value: 'data_frozen',
},
},
};
if (!originalQuery) {
return {
bool: {
must_not: [FROZEN_TIER_TERM],
},
};
}
const query = cloneDeep(originalQuery);
delete query.match_all;
if (isPopulatedObject(query.bool)) {
// Must_not can be both arrays or singular object
if (Array.isArray(query.bool.must_not)) {
query.bool.must_not.push(FROZEN_TIER_TERM);
} else {
// If there's already a must_not condition
if (isPopulatedObject(query.bool.must_not)) {
query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM];
}
if (query.bool.must_not === undefined) {
query.bool.must_not = [FROZEN_TIER_TERM];
}
}
} else {
query.bool = {
must_not: [FROZEN_TIER_TERM],
};
}
return query;
};

View file

@ -8,24 +8,28 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import './_index.scss'; import './_index.scss';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { pick } from 'lodash';
import { AppMountParameters, CoreStart, HttpStart } from '@kbn/core/public'; import { AppMountParameters, CoreStart, HttpStart } from '@kbn/core/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage'; import { StorageContextProvider } from '@kbn/ml-local-storage';
import { ML_STORAGE_KEYS } from '../../common/types/storage'; import { ML_STORAGE_KEYS } from '../../common/types/storage';
import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator';
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setDependencyCache, clearCache } from './util/dependency_cache';
import { setLicenseCache } from './license'; import { setLicenseCache } from './license';
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { mlUsageCollectionProvider } from './services/usage_collection'; import { mlUsageCollectionProvider } from './services/usage_collection';
import { MlRouter } from './routing'; import { MlRouter } from './routing';
import { mlApiServicesProvider } from './services/ml_api_service'; import { mlApiServicesProvider } from './services/ml_api_service';
import { HttpService } from './services/http_service'; import { HttpService } from './services/http_service';
import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator';
export type MlDependencies = Omit< export type MlDependencies = Omit<
MlSetupDependencies, MlSetupDependencies,
@ -98,6 +102,13 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
...coreStart, ...coreStart,
}; };
const datePickerDeps = {
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
const I18nContext = coreStart.i18n.Context; const I18nContext = coreStart.i18n.Context;
const ApplicationUsageTrackingProvider = const ApplicationUsageTrackingProvider =
deps.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; deps.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
@ -113,7 +124,9 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
}} }}
> >
<StorageContextProvider storage={localStorage} storageKeys={ML_STORAGE_KEYS}> <StorageContextProvider storage={localStorage} storageKeys={ML_STORAGE_KEYS}>
<MlRouter pageDeps={pageDeps} /> <DatePickerContextProvider {...datePickerDeps}>
<MlRouter pageDeps={pageDeps} />
</DatePickerContextProvider>
</StorageContextProvider> </StorageContextProvider>
</KibanaContextProvider> </KibanaContextProvider>
</KibanaThemeProvider> </KibanaThemeProvider>

View file

@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FullTimeRangeSelector renders the selector 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
responsive={false}
>
<EuiToolTip
content={
<FormattedMessage
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
id="xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
values={Object {}}
/>
}
delay="regular"
display="inlineBlock"
position="top"
>
<EuiButton
color="primary"
data-test-subj="mlButtonUseFullData"
isDisabled={false}
onClick={[Function]}
size="m"
>
<FormattedMessage
defaultMessage="Use full data"
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiToolTip>
<EuiFlexItem
grow={false}
>
<EuiPopover
anchorPosition="downRight"
button={
<EuiButtonIcon
aria-label="More"
display="base"
iconType="boxesVertical"
onClick={[Function]}
size="m"
/>
}
closePopover={[Function]}
display="inline-block"
hasArrow={true}
id="mlFullTimeRangeSelectorOption"
isOpen={false}
ownFocus={true}
panelPaddingSize="none"
>
<EuiPanel>
<EuiRadioGroup
compressed={true}
idSelected="e"
onChange={[Function]}
options={
Array [
Object {
"id": "exclude-frozen",
"label": "Exclude frozen data tier",
},
Object {
"id": "include-frozen",
"label": "Include frozen data tier",
},
]
}
/>
</EuiPanel>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -1,166 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlexGroup,
EuiButton,
EuiFlexItem,
EuiButtonIcon,
EuiRadioGroup,
EuiPanel,
EuiToolTip,
EuiPopover,
EuiRadioGroupOption,
} from '@elastic/eui';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import { setFullTimeRange } from './full_time_range_selector_service';
import {
ML_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type MlStorageKey,
type TMlStorageMapped,
type FrozenTierPreference,
} from '../../../../common/types/storage';
import { GetTimeFieldRangeResponse } from '../../services/ml_api_service';
interface Props {
dataView: DataView;
query: QueryDslQueryContainer;
disabled: boolean;
callback?: (a: GetTimeFieldRangeResponse) => void;
}
// Component for rendering a button which automatically sets the range of the time filter
// to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query.
export const FullTimeRangeSelector: FC<Props> = ({ dataView, query, disabled, callback }) => {
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
async function setRange(i: DataView, q: QueryDslQueryContainer, excludeFrozenData = true) {
const fullTimeRange = await setFullTimeRange(i, q, excludeFrozenData);
if (typeof callback === 'function') {
callback(fullTimeRange);
}
}
const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_FROZEN_TIER_PREFERENCE>
>(ML_FROZEN_TIER_PREFERENCE, FROZEN_TIER_PREFERENCE.EXCLUDE);
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
const sortOptions: EuiRadioGroupOption[] = useMemo(() => {
return [
{
id: FROZEN_TIER_PREFERENCE.EXCLUDE,
label: i18n.translate(
'xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
{
defaultMessage: 'Exclude frozen data tier',
}
),
},
{
id: FROZEN_TIER_PREFERENCE.INCLUDE,
label: i18n.translate(
'xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
{
defaultMessage: 'Include frozen data tier',
}
),
},
];
}, []);
const setPreference = useCallback((id: string) => {
setFrozenDataPreference(id as FrozenTierPreference);
setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE);
closePopover();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const popoverContent = useMemo(
() => (
<EuiPanel>
<EuiRadioGroup
options={sortOptions}
idSelected={frozenDataPreference}
onChange={setPreference}
compressed
/>
</EuiPanel>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[frozenDataPreference, sortOptions]
);
const buttonTooltip = useMemo(
() =>
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
<FormattedMessage
id="xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
defaultMessage="Use full range of data excluding frozen data tier."
/>
) : (
<FormattedMessage
id="xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
/>
),
[frozenDataPreference]
);
return (
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
<EuiToolTip content={buttonTooltip}>
<EuiButton
isDisabled={disabled}
onClick={() => setRange(dataView, query, true)}
data-test-subj="mlButtonUseFullData"
>
<FormattedMessage
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
defaultMessage="Use full data"
/>
</EuiButton>
</EuiToolTip>
<EuiFlexItem grow={false}>
<EuiPopover
id={'mlFullTimeRangeSelectorOption'}
button={
<EuiButtonIcon
display="base"
size="m"
iconType="boxesVertical"
aria-label="More"
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downRight"
>
{popoverContent}
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import dateMath from '@kbn/datemath';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { DataView } from '@kbn/data-views-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { getTimefilter, getToastNotifications } from '../../util/dependency_cache';
import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service';
import type { RuntimeMappings } from '../../../../common/types/fields';
import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils';
export interface TimeRange {
from: number;
to: number;
}
export async function setFullTimeRange(
indexPattern: DataView,
query: QueryDslQueryContainer,
excludeFrozenData: boolean
): Promise<GetTimeFieldRangeResponse> {
try {
const timefilter = getTimefilter();
const runtimeMappings = indexPattern.getComputedFields().runtimeFields as RuntimeMappings;
const resp = await ml.getTimeFieldRange({
index: indexPattern.title,
timeFieldName: indexPattern.timeFieldName,
// By default we want to use full non-frozen time range
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
});
timefilter.setTime({
from: moment(resp.start).toISOString(),
to: moment(resp.end).toISOString(),
});
return resp;
} catch (resp) {
const toastNotifications = getToastNotifications();
toastNotifications.addDanger(
i18n.translate('xpack.ml.fullTimeRangeSelector.errorSettingTimeRangeNotification', {
defaultMessage: 'An error occurred setting the time range.',
})
);
return resp;
}
}
export function getTimeFilterRange(): TimeRange {
const timefilter = getTimefilter();
const fromMoment = dateMath.parse(timefilter.getTime().from);
const toMoment = dateMath.parse(timefilter.getTime().to);
const from = fromMoment !== undefined ? fromMoment.valueOf() : 0;
const to = toMoment !== undefined ? toMoment.valueOf() : 0;
return {
to,
from,
};
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { FullTimeRangeSelector } from './full_time_range_selector';
export type { TimeRange } from './full_time_range_selector_service';
export { getTimeFilterRange } from './full_time_range_selector_service';

View file

@ -5,20 +5,21 @@
* 2.0. * 2.0.
*/ */
import React, { createContext, FC, useMemo, useState } from 'react'; import React, { createContext, FC, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui'; import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Redirect, Route, Switch } from 'react-router-dom'; import { Redirect, Route, Switch } from 'react-router-dom';
import type { AppMountParameters } from '@kbn/core/public'; import type { AppMountParameters } from '@kbn/core/public';
import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public'; import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import { createHtmlPortalNode, HtmlPortalNode } from 'react-reverse-portal'; import { createHtmlPortalNode, HtmlPortalNode } from 'react-reverse-portal';
import { DatePickerWrapper } from '@kbn/ml-date-picker';
import { MlPageHeaderRenderer } from '../page_header/page_header'; import { MlPageHeaderRenderer } from '../page_header/page_header';
import { useSideNavItems } from './side_nav'; import { useSideNavItems } from './side_nav';
import * as routes from '../../routing/routes'; import * as routes from '../../routing/routes';
import { MlPageWrapper } from '../../routing/ml_page_wrapper'; import { MlPageWrapper } from '../../routing/ml_page_wrapper';
import { useMlKibana, useNavigateToPath } from '../../contexts/kibana'; import { useMlKibana, useNavigateToPath } from '../../contexts/kibana';
import { MlRoute, PageDependencies } from '../../routing/router'; import { MlRoute, PageDependencies } from '../../routing/router';
import { DatePickerWrapper } from '../navigation_menu/date_picker_wrapper';
import { useActiveRoute } from '../../routing/use_active_route'; import { useActiveRoute } from '../../routing/use_active_route';
import { useDocTitle } from '../../routing/use_doc_title'; import { useDocTitle } from '../../routing/use_doc_title';
@ -43,11 +44,28 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
const { const {
services: { services: {
http: { basePath }, http: { basePath },
mlServices: { httpService },
}, },
} = useMlKibana(); } = useMlKibana();
const headerPortalNode = useMemo(() => createHtmlPortalNode(), []); const headerPortalNode = useMemo(() => createHtmlPortalNode(), []);
const [isHeaderMounted, setIsHeaderMounted] = useState(false); const [isHeaderMounted, setIsHeaderMounted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const subscriptions = new Subscription();
subscriptions.add(
httpService.getLoadingCount$.subscribe((v) => {
setIsLoading(v !== 0);
})
);
return function cleanup() {
subscriptions.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const routeList = useMemo( const routeList = useMemo(
() => () =>
@ -61,8 +79,8 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
const activeRoute = useActiveRoute(routeList); const activeRoute = useActiveRoute(routeList);
const rightSideItems = useMemo(() => { const rightSideItems = useMemo(() => {
return [...(activeRoute.enableDatePicker ? [<DatePickerWrapper />] : [])]; return [...(activeRoute.enableDatePicker ? [<DatePickerWrapper isLoading={isLoading} />] : [])];
}, [activeRoute.enableDatePicker]); }, [activeRoute.enableDatePicker, isLoading]);
useDocTitle(activeRoute); useDocTitle(activeRoute);

View file

@ -10,7 +10,6 @@ export { useMlKibana } from './kibana_context';
export type { NavigateToPath } from './use_navigate_to_path'; export type { NavigateToPath } from './use_navigate_to_path';
export { useNavigateToPath } from './use_navigate_to_path'; export { useNavigateToPath } from './use_navigate_to_path';
export { useUiSettings } from './use_ui_settings_context'; export { useUiSettings } from './use_ui_settings_context';
export { useTimefilter } from './use_timefilter';
export { useNotifications } from './use_notifications_context'; export { useNotifications } from './use_notifications_context';
export { useMlLocator, useMlLink } from './use_create_url'; export { useMlLocator, useMlLink } from './use_create_url';
export { useMlApiContext } from './use_ml_api_context'; export { useMlApiContext } from './use_ml_api_context';

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { map } from 'rxjs/operators';
import { useMlKibana } from './kibana_context';
interface UseTimefilterOptions {
timeRangeSelector?: boolean;
autoRefreshSelector?: boolean;
}
export const useTimefilter = ({
timeRangeSelector,
autoRefreshSelector,
}: UseTimefilterOptions = {}) => {
const { services } = useMlKibana();
const { timefilter } = services.data.query.timefilter;
useEffect(() => {
if (timeRangeSelector === true) {
timefilter.enableTimeRangeSelector();
} else if (timeRangeSelector === false) {
timefilter.disableTimeRangeSelector();
}
if (autoRefreshSelector === true) {
timefilter.enableAutoRefreshSelector();
} else if (autoRefreshSelector === false) {
timefilter.disableAutoRefreshSelector();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeRangeSelector, autoRefreshSelector]);
return timefilter;
};
export const useRefreshIntervalUpdates = () => {
const timefilter = useTimefilter();
return useObservable(
timefilter.getRefreshIntervalUpdate$().pipe(map(timefilter.getRefreshInterval)),
timefilter.getRefreshInterval()
);
};
export const useTimeRangeUpdates = (absolute = false) => {
const timefilter = useTimefilter();
const getTimeCallback = absolute
? timefilter.getAbsoluteTime.bind(timefilter)
: timefilter.getTime.bind(timefilter);
return useObservable(timefilter.getTimeUpdate$().pipe(map(getTimeCallback)), getTimeCallback());
};

View file

@ -20,8 +20,9 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { isFullLicense } from '../license'; import { isFullLicense } from '../license';
import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { useMlKibana, useNavigateToPath } from '../contexts/kibana';
import { HelpMenu } from '../components/help_menu'; import { HelpMenu } from '../components/help_menu';
import { MlPageHeader } from '../components/page_header'; import { MlPageHeader } from '../components/page_header';

View file

@ -13,7 +13,7 @@ import type {
GetAdditionalLinksParams, GetAdditionalLinksParams,
GetAdditionalLinks, GetAdditionalLinks,
} from '@kbn/data-visualizer-plugin/public'; } from '@kbn/data-visualizer-plugin/public';
import { useTimefilter } from '../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { HelpMenu } from '../../components/help_menu'; import { HelpMenu } from '../../components/help_menu';
import { useMlKibana, useMlLocator } from '../../contexts/kibana'; import { useMlKibana, useMlLocator } from '../../contexts/kibana';

View file

@ -14,7 +14,8 @@ import type {
GetAdditionalLinks, GetAdditionalLinks,
GetAdditionalLinksParams, GetAdditionalLinksParams,
} from '@kbn/data-visualizer-plugin/public'; } from '@kbn/data-visualizer-plugin/public';
import { useMlKibana, useTimefilter, useMlLocator } from '../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { useMlKibana, useMlLocator } from '../../contexts/kibana';
import { HelpMenu } from '../../components/help_menu'; import { HelpMenu } from '../../components/help_menu';
import { ML_PAGES } from '../../../../common/constants/locator'; import { ML_PAGES } from '../../../../common/constants/locator';
import { isFullLicense } from '../../license'; import { isFullLicense } from '../../license';

View file

@ -14,6 +14,7 @@ import { switchMap, map } from 'rxjs/operators';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { TimefilterContract } from '@kbn/data-plugin/public'; import { TimefilterContract } from '@kbn/data-plugin/public';
import { useTimefilter } from '@kbn/ml-date-picker';
import { import {
getDateFormatTz, getDateFormatTz,
getSelectionInfluencers, getSelectionInfluencers,
@ -28,7 +29,7 @@ import {
ExplorerJob, ExplorerJob,
} from '../explorer_utils'; } from '../explorer_utils';
import { ExplorerState } from '../reducers'; import { ExplorerState } from '../reducers';
import { useMlKibana, useTimefilter } from '../../contexts/kibana'; import { useMlKibana } from '../../contexts/kibana';
import { MlResultsService, mlResultsServiceProvider } from '../../services/results_service'; import { MlResultsService, mlResultsServiceProvider } from '../../services/results_service';
import { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service'; import { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service';
import type { InfluencersFilterQuery } from '../../../../common/types/es_client'; import type { InfluencersFilterQuery } from '../../../../common/types/es_client';

View file

@ -20,13 +20,13 @@ import { i18n } from '@kbn/i18n';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import type { Query, TimeRange } from '@kbn/es-query'; import type { Query, TimeRange } from '@kbn/es-query';
import { isDefined } from '@kbn/ml-is-defined'; import { isDefined } from '@kbn/ml-is-defined';
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { useAnomalyExplorerContext } from './anomaly_explorer_context'; import { useAnomalyExplorerContext } from './anomaly_explorer_context';
import { escapeKueryForFieldValuePair } from '../util/string_utils'; import { escapeKueryForFieldValuePair } from '../util/string_utils';
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
import { useCasesModal } from '../contexts/kibana/use_cases_modal'; import { useCasesModal } from '../contexts/kibana/use_cases_modal';
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service'; import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service';
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables'; import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables';
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
import { useMlKibana } from '../contexts/kibana'; import { useMlKibana } from '../contexts/kibana';
import { import {
AppStateSelectedCells, AppStateSelectedCells,

View file

@ -6,9 +6,10 @@
*/ */
import React, { useContext, useEffect, useMemo, useState, type FC } from 'react'; import React, { useContext, useEffect, useMemo, useState, type FC } from 'react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { AnomalyTimelineStateService } from './anomaly_timeline_state_service'; import { AnomalyTimelineStateService } from './anomaly_timeline_state_service';
import { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state'; import { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state';
import { useMlKibana, useTimefilter } from '../contexts/kibana'; import { useMlKibana } from '../contexts/kibana';
import { mlResultsServiceProvider } from '../services/results_service'; import { mlResultsServiceProvider } from '../services/results_service';
import { AnomalyTimelineService } from '../services/anomaly_timeline_service'; import { AnomalyTimelineService } from '../services/anomaly_timeline_service';
import { useExplorerUrlState } from './hooks/use_explorer_url_state'; import { useExplorerUrlState } from './hooks/use_explorer_url_state';

View file

@ -28,9 +28,9 @@ import useDebounce from 'react-use/lib/useDebounce';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import type { Query } from '@kbn/es-query'; import type { Query } from '@kbn/es-query';
import { isDefined } from '@kbn/ml-is-defined'; import { isDefined } from '@kbn/ml-is-defined';
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
import { useCasesModal } from '../contexts/kibana/use_cases_modal'; import { useCasesModal } from '../contexts/kibana/use_cases_modal';
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..';
import { import {
OVERALL_LABEL, OVERALL_LABEL,

View file

@ -19,6 +19,8 @@ import {
import { isEqual, sortBy, uniq } from 'lodash'; import { isEqual, sortBy, uniq } from 'lodash';
import type { TimefilterContract } from '@kbn/data-plugin/public'; import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { TimeRangeBounds } from '@kbn/data-plugin/common'; import type { TimeRangeBounds } from '@kbn/data-plugin/common';
// FIXME get rid of the static import
import { mlTimefilterRefresh$ } from '@kbn/ml-date-picker';
import { AnomalyTimelineService } from '../services/anomaly_timeline_service'; import { AnomalyTimelineService } from '../services/anomaly_timeline_service';
import type { import type {
AppStateSelectedCells, AppStateSelectedCells,
@ -38,8 +40,6 @@ import { mlJobService } from '../services/job_service';
import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils'; import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils';
import type { TimeBucketsInterval } from '../util/time_buckets'; import type { TimeBucketsInterval } from '../util/time_buckets';
import { InfluencersFilterQuery } from '../../../common/types/es_client'; import { InfluencersFilterQuery } from '../../../common/types/es_client';
// FIXME get rid of the static import
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
import type { Refresh } from '../routing/use_refresh'; import type { Refresh } from '../routing/use_refresh';
import { StateService } from '../services/state_service'; import { StateService } from '../services/state_service';
import type { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state'; import type { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state';

View file

@ -9,6 +9,7 @@ import { BehaviorSubject } from 'rxjs';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { ES_FIELD_TYPES } from '@kbn/field-types'; import { ES_FIELD_TYPES } from '@kbn/field-types';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import { addExcludeFrozenToQuery } from '@kbn/ml-query-utils';
import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { SavedSearchSavedObject } from '../../../../../../common/types/kibana';
import { UrlConfig } from '../../../../../../common/types/custom_urls'; import { UrlConfig } from '../../../../../../common/types/custom_urls';
import { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { IndexPatternTitle } from '../../../../../../common/types/kibana';
@ -28,7 +29,6 @@ import {
} from '../../../../../../common/types/anomaly_detection_jobs'; } from '../../../../../../common/types/anomaly_detection_jobs';
import { Aggregation, Field, RuntimeMappings } from '../../../../../../common/types/fields'; import { Aggregation, Field, RuntimeMappings } from '../../../../../../common/types/fields';
import { combineFieldsAndAggs } from '../../../../../../common/util/fields_utils'; import { combineFieldsAndAggs } from '../../../../../../common/util/fields_utils';
import { addExcludeFrozenToQuery } from '../../../../../../common/util/query_utils';
import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs';
import { mlJobService } from '../../../../services/job_service'; import { mlJobService } from '../../../../services/job_service';
import { JobRunner, ProgressSubscriber } from '../job_runner'; import { JobRunner, ProgressSubscriber } from '../job_runner';

View file

@ -10,19 +10,26 @@ import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import moment from 'moment'; import moment from 'moment';
import { FullTimeRangeSelector, FROZEN_TIER_PREFERENCE } from '@kbn/ml-date-picker';
import { useTimefilter, type GetTimeFieldRangeResponse } from '@kbn/ml-date-picker';
import { useStorage } from '@kbn/ml-local-storage';
import { WizardNav } from '../wizard_nav'; import { WizardNav } from '../wizard_nav';
import { StepProps, WIZARD_STEPS } from '../step_types'; import { StepProps, WIZARD_STEPS } from '../step_types';
import { JobCreatorContext } from '../job_creator_context'; import { JobCreatorContext } from '../job_creator_context';
import { useMlContext } from '../../../../../contexts/ml'; import { useMlContext } from '../../../../../contexts/ml';
import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector';
import { EventRateChart } from '../charts/event_rate_chart'; import { EventRateChart } from '../charts/event_rate_chart';
import { LineChartPoint } from '../../../common/chart_loader'; import { LineChartPoint } from '../../../common/chart_loader';
import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job';
import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service';
import { TimeRangePicker, TimeRange } from '../../../common/components'; import { TimeRangePicker, TimeRange } from '../../../common/components';
import { useMlKibana } from '../../../../../contexts/kibana'; import { useMlKibana } from '../../../../../contexts/kibana';
import {
ML_FROZEN_TIER_PREFERENCE,
type MlStorageKey,
type TMlStorageMapped,
} from '../../../../../../../common/types/storage';
export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) => { export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) => {
const timefilter = useTimefilter();
const { services } = useMlKibana(); const { services } = useMlKibana();
const mlContext = useMlContext(); const mlContext = useMlContext();
@ -36,6 +43,15 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
const [eventRateChartData, setEventRateChartData] = useState<LineChartPoint[]>([]); const [eventRateChartData, setEventRateChartData] = useState<LineChartPoint[]>([]);
const [loadingData, setLoadingData] = useState(false); const [loadingData, setLoadingData] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_FROZEN_TIER_PREFERENCE>
>(
ML_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE
);
async function loadChart() { async function loadChart() {
setLoadingData(true); setLoadingData(true);
try { try {
@ -61,8 +77,8 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
max: moment(end), max: moment(end),
}); });
// update the timefilter, to keep the URL in sync // update the timefilter, to keep the URL in sync
const { timefilter } = services.data.query.timefilter; const { timefilter: timefilterService } = services.data.query.timefilter;
timefilter.setTime({ timefilterService.setTime({
from: moment(start).toISOString(), from: moment(start).toISOString(),
to: moment(end).toISOString(), to: moment(end).toISOString(),
}); });
@ -89,8 +105,8 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
function fullTimeRangeCallback(range: GetTimeFieldRangeResponse) { function fullTimeRangeCallback(range: GetTimeFieldRangeResponse) {
if (range.start !== null && range.end !== null) { if (range.start !== null && range.end !== null) {
setTimeRange({ setTimeRange({
start: range.start, start: range.start.epoch,
end: range.end, end: range.end.epoch,
}); });
} else { } else {
const { toasts } = services.notifications; const { toasts } = services.notifications;
@ -112,10 +128,13 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<FullTimeRangeSelector <FullTimeRangeSelector
frozenDataPreference={frozenDataPreference}
setFrozenDataPreference={setFrozenDataPreference}
dataView={mlContext.currentDataView} dataView={mlContext.currentDataView}
query={mlContext.combinedQuery} query={mlContext.combinedQuery}
disabled={false} disabled={false}
callback={fullTimeRangeCallback} callback={fullTimeRangeCallback}
timefilter={timefilter}
/> />
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem /> <EuiFlexItem />

View file

@ -12,6 +12,7 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker';
import { useTimeBuckets } from '../../../../components/custom_hooks/use_time_buckets'; import { useTimeBuckets } from '../../../../components/custom_hooks/use_time_buckets';
import { Wizard } from './wizard'; import { Wizard } from './wizard';
import { WIZARD_STEPS } from '../components/step_types'; import { WIZARD_STEPS } from '../components/step_types';
@ -34,7 +35,6 @@ import { ResultsLoader } from '../../common/results_loader';
import { JobValidator } from '../../common/job_validator'; import { JobValidator } from '../../common/job_validator';
import { useMlContext } from '../../../../contexts/ml'; import { useMlContext } from '../../../../contexts/ml';
import { useMlKibana } from '../../../../contexts/kibana'; import { useMlKibana } from '../../../../contexts/kibana';
import { getTimeFilterRange } from '../../../../components/full_time_range_selector';
import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service';
import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service';
import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields';
@ -52,6 +52,7 @@ export interface PageProps {
} }
export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => { export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
const timefilter = useTimefilter();
const mlContext = useMlContext(); const mlContext = useMlContext();
const { const {
services: { maps: mapsPlugin }, services: { maps: mapsPlugin },
@ -74,7 +75,7 @@ export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
const { displayErrorToast } = useToastNotificationService(); const { displayErrorToast } = useToastNotificationService();
const { from, to } = getTimeFilterRange(); const { from, to } = getTimeFilterRange(timefilter);
jobCreator.setTimeRange(from, to); jobCreator.setTimeRange(from, to);
let firstWizardStep = let firstWizardStep =

View file

@ -19,8 +19,8 @@ import {
EuiSwitch, EuiSwitch,
EuiTextAlign, EuiTextAlign,
} from '@elastic/eui'; } from '@elastic/eui';
import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker';
import { ModuleJobUI, SAVE_STATE } from '../page'; import { ModuleJobUI, SAVE_STATE } from '../page';
import { getTimeFilterRange } from '../../../../components/full_time_range_selector';
import { useMlContext } from '../../../../contexts/ml'; import { useMlContext } from '../../../../contexts/ml';
import { import {
composeValidators, composeValidators,
@ -51,7 +51,8 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
saveState, saveState,
jobs, jobs,
}) => { }) => {
const { from, to } = getTimeFilterRange(); const timefilter = useTimefilter();
const { from, to } = getTimeFilterRange(timefilter);
const { currentDataView: dataView } = useMlContext(); const { currentDataView: dataView } = useMlContext();
const jobPrefixValidator = useMemo( const jobPrefixValidator = useMemo(

View file

@ -20,6 +20,7 @@ import {
import { isEqual, merge } from 'lodash'; import { isEqual, merge } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { addExcludeFrozenToQuery } from '@kbn/ml-query-utils';
import { useMlKibana, useMlLocator } from '../../../contexts/kibana'; import { useMlKibana, useMlLocator } from '../../../contexts/kibana';
import { useMlContext } from '../../../contexts/ml'; import { useMlContext } from '../../../contexts/ml';
import { import {
@ -41,7 +42,6 @@ import { ML_PAGES } from '../../../../../common/constants/locator';
import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { TIME_FORMAT } from '../../../../../common/constants/time_format';
import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning';
import { RuntimeMappings } from '../../../../../common/types/fields'; import { RuntimeMappings } from '../../../../../common/types/fields';
import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils';
import { MlPageHeader } from '../../../components/page_header'; import { MlPageHeader } from '../../../components/page_header';
export interface ModuleJobUI extends ModuleJob { export interface ModuleJobUI extends ModuleJob {

View file

@ -5,19 +5,65 @@
* 2.0. * 2.0.
*/ */
import React from 'react'; import React, { type FC } from 'react';
import { I18nProvider } from '@kbn/i18n-react'; import { I18nProvider } from '@kbn/i18n-react';
import { render, waitFor } from '@testing-library/react'; import { render, waitFor } from '@testing-library/react';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
import { NotificationsList } from './notifications_list'; import { NotificationsList } from './notifications_list';
import { useMlKibana } from '../../contexts/kibana'; import { useMlKibana } from '../../contexts/kibana';
jest.mock('../../contexts/kibana'); jest.mock('../../contexts/kibana');
jest.mock('../../services/toast_notification_service'); jest.mock('../../services/toast_notification_service');
jest.mock('../../contexts/ml/ml_notifications_context'); jest.mock('../../contexts/ml/ml_notifications_context');
jest.mock('../../contexts/kibana/use_timefilter');
jest.mock('../../contexts/kibana/use_field_formatter'); jest.mock('../../contexts/kibana/use_field_formatter');
jest.mock('../../components/saved_objects_warning'); jest.mock('../../components/saved_objects_warning');
const getMockedTimefilter = () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { of } = require('rxjs');
return {
timefilter: {
disableTimeRangeSelector: jest.fn(),
disableAutoRefreshSelector: jest.fn(),
enableTimeRangeSelector: jest.fn(),
enableAutoRefreshSelector: jest.fn(),
getRefreshInterval: jest.fn(),
setRefreshInterval: jest.fn(),
getTime: jest.fn(() => {
return { from: '', to: '' };
}),
setTime: jest.fn(),
isAutoRefreshSelectorEnabled: jest.fn(),
isTimeRangeSelectorEnabled: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
getTimeUpdate$: jest.fn(() => {
return of();
}),
getEnabledUpdated$: jest.fn(),
},
history: { get: jest.fn() },
};
};
const getMockedDatePickeDependencies = () => {
return {
data: {
query: {
timefilter: getMockedTimefilter(),
},
},
notifications: {},
} as unknown as DatePickerDependencies;
};
const Wrapper: FC = ({ children }) => (
<I18nProvider>
<DatePickerContextProvider {...getMockedDatePickeDependencies()}>
{children}
</DatePickerContextProvider>
</I18nProvider>
);
describe('NotificationsList', () => { describe('NotificationsList', () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
@ -29,7 +75,7 @@ describe('NotificationsList', () => {
}); });
test('starts fetching notification on mount with default params', async () => { test('starts fetching notification on mount with default params', async () => {
const {} = render(<NotificationsList />, { wrapper: I18nProvider }); const {} = render(<NotificationsList />, { wrapper: Wrapper });
jest.advanceTimersByTime(500); jest.advanceTimersByTime(500);

View file

@ -24,11 +24,11 @@ import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import useDebounce from 'react-use/lib/useDebounce'; import useDebounce from 'react-use/lib/useDebounce';
import useMount from 'react-use/lib/useMount'; import useMount from 'react-use/lib/useMount';
import { usePageUrlState } from '@kbn/ml-url-state'; import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { EntityFilter } from './entity_filter'; import { EntityFilter } from './entity_filter';
import { useMlNotifications } from '../../contexts/ml/ml_notifications_context'; import { useMlNotifications } from '../../contexts/ml/ml_notifications_context';
import { ML_NOTIFICATIONS_MESSAGE_LEVEL } from '../../../../common/constants/notifications'; import { ML_NOTIFICATIONS_MESSAGE_LEVEL } from '../../../../common/constants/notifications';
import { SavedObjectsWarning } from '../../components/saved_objects_warning'; import { SavedObjectsWarning } from '../../components/saved_objects_warning';
import { useTimefilter, useTimeRangeUpdates } from '../../contexts/kibana/use_timefilter';
import { useToastNotificationService } from '../../services/toast_notification_service'; import { useToastNotificationService } from '../../services/toast_notification_service';
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { useRefresh } from '../../routing/use_refresh'; import { useRefresh } from '../../routing/use_refresh';

View file

@ -7,8 +7,9 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { NotificationsList } from './components/notifications_list'; import { NotificationsList } from './components/notifications_list';
import { useMlKibana, useTimefilter } from '../contexts/kibana'; import { useMlKibana } from '../contexts/kibana';
import { MlPageHeader } from '../components/page_header'; import { MlPageHeader } from '../components/page_header';
import { NodeAvailableWarning } from '../components/node_available_warning'; import { NodeAvailableWarning } from '../components/node_available_warning';
import { UpgradeWarning } from '../components/upgrade'; import { UpgradeWarning } from '../components/upgrade';

View file

@ -7,7 +7,8 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Action } from '@elastic/eui/src/components/basic_table/action_types'; import { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { useMlLocator, useNavigateToPath, useTimefilter } from '../../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { useMlLocator, useNavigateToPath } from '../../../contexts/kibana';
import { ML_PAGES } from '../../../../../common/constants/locator'; import { ML_PAGES } from '../../../../../common/constants/locator';
import { Group } from './anomaly_detection_panel'; import { Group } from './anomaly_detection_panel';

View file

@ -7,11 +7,12 @@
import React, { useEffect, useState, type FC } from 'react'; import React, { useEffect, useState, type FC } from 'react';
import { EuiSpacer } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui';
import { useTimefilter } from '@kbn/ml-date-picker';
import { AnomalyDetectionPanel } from './anomaly_detection_panel'; import { AnomalyDetectionPanel } from './anomaly_detection_panel';
import { AnalyticsPanel } from './analytics_panel'; import { AnalyticsPanel } from './analytics_panel';
import { AnomalyTimelineService } from '../../services/anomaly_timeline_service'; import { AnomalyTimelineService } from '../../services/anomaly_timeline_service';
import { mlResultsServiceProvider } from '../../services/results_service'; import { mlResultsServiceProvider } from '../../services/results_service';
import { useMlKibana, useTimefilter } from '../../contexts/kibana'; import { useMlKibana } from '../../contexts/kibana';
interface Props { interface Props {
createAnomalyDetectionJobDisabled: boolean; createAnomalyDetectionJobDisabled: boolean;

View file

@ -8,6 +8,7 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { checkPermission } from '../capabilities/check_capabilities'; import { checkPermission } from '../capabilities/check_capabilities';
import { mlNodesAvailable } from '../ml_nodes_check'; import { mlNodesAvailable } from '../ml_nodes_check';
import { GettingStartedCallout } from './components/getting_started_callout'; import { GettingStartedCallout } from './components/getting_started_callout';
@ -17,9 +18,8 @@ import { JobsAwaitingNodeWarning } from '../components/jobs_awaiting_node_warnin
import { SavedObjectsWarning } from '../components/saved_objects_warning'; import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { UpgradeWarning } from '../components/upgrade'; import { UpgradeWarning } from '../components/upgrade';
import { HelpMenu } from '../components/help_menu'; import { HelpMenu } from '../components/help_menu';
import { useMlKibana, useTimefilter } from '../contexts/kibana'; import { useMlKibana } from '../contexts/kibana';
import { NodesList } from '../trained_models/nodes_overview'; import { NodesList } from '../trained_models/nodes_overview';
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
import { MlPageHeader } from '../components/page_header'; import { MlPageHeader } from '../components/page_header';
export const OverviewPage: FC = () => { export const OverviewPage: FC = () => {

View file

@ -14,7 +14,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
import { useUrlState } from '@kbn/ml-url-state'; import { useUrlState } from '@kbn/ml-url-state';
import { NavigateToPath, useMlKibana, useTimefilter } from '../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { NavigateToPath, useMlKibana } from '../../contexts/kibana';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';

View file

@ -8,18 +8,20 @@
import React, { useEffect, FC, useMemo } from 'react'; import React, { useEffect, FC, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import {
mlTimefilterRefresh$,
useRefreshIntervalUpdates,
useTimefilter,
} from '@kbn/ml-date-picker';
import { NavigateToPath } from '../../contexts/kibana'; import { NavigateToPath } from '../../contexts/kibana';
import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list';
import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service';
import { MlRoute, PageLoader, PageProps } from '../router'; import { MlRoute, PageLoader, PageProps } from '../router';
import { useResolver } from '../use_resolver'; import { useResolver } from '../use_resolver';
import { basicResolvers } from '../resolvers'; import { basicResolvers } from '../resolvers';
import { JobsPage } from '../../jobs/jobs_list'; import { JobsPage } from '../../jobs/jobs_list';
import { useTimefilter } from '../../contexts/kibana';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { AnnotationUpdatesService } from '../../services/annotations_service'; import { AnnotationUpdatesService } from '../../services/annotations_service';
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
import { useRefreshIntervalUpdates } from '../../contexts/kibana/use_timefilter';
export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
id: 'anomaly_detection', id: 'anomaly_detection',

View file

@ -7,6 +7,7 @@
import React, { FC, Suspense } from 'react'; import React, { FC, Suspense } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useTimefilter } from '@kbn/ml-date-picker';
import { PageLoader, PageProps } from '../router'; import { PageLoader, PageProps } from '../router';
import { useResolver } from '../use_resolver'; import { useResolver } from '../use_resolver';
import { checkFullLicense } from '../../license'; import { checkFullLicense } from '../../license';
@ -15,7 +16,7 @@ import { getMlNodeCount } from '../../ml_nodes_check';
import { loadMlServerInfo } from '../../services/ml_server_info'; import { loadMlServerInfo } from '../../services/ml_server_info';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import type { MlRoute } from '..'; import type { MlRoute } from '..';
import { NavigateToPath, useTimefilter } from '../../contexts/kibana'; import { NavigateToPath } from '../../contexts/kibana';
const NotificationsPage = React.lazy(() => import('../../notifications/page')); const NotificationsPage = React.lazy(() => import('../../notifications/page'));

View file

@ -10,16 +10,16 @@ import { i18n } from '@kbn/i18n';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { useTimefilter } from '@kbn/ml-date-picker';
import type { NavigateToPath } from '../../contexts/kibana'; import type { NavigateToPath } from '../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../router';
import { useResolver } from '../use_resolver';
import { checkFullLicense } from '../../license'; import { checkFullLicense } from '../../license';
import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities';
import { getMlNodeCount } from '../../ml_nodes_check'; import { getMlNodeCount } from '../../ml_nodes_check';
import { loadMlServerInfo } from '../../services/ml_server_info'; import { loadMlServerInfo } from '../../services/ml_server_info';
import { useTimefilter } from '../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../router';
import { useResolver } from '../use_resolver';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
const OverviewPage = React.lazy(() => import('../../overview/overview_page')); const OverviewPage = React.lazy(() => import('../../overview/overview_page'));

View file

@ -7,7 +7,8 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { NavigateToPath, useTimefilter } from '../../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { NavigateToPath } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router'; import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver'; import { useResolver } from '../../use_resolver';
import { checkFullLicense } from '../../../license'; import { checkFullLicense } from '../../../license';

View file

@ -7,7 +7,8 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { NavigateToPath, useTimefilter } from '../../../contexts/kibana'; import { useTimefilter } from '@kbn/ml-date-picker';
import { NavigateToPath } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router'; import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver'; import { useResolver } from '../../use_resolver';
import { checkFullLicense } from '../../../license'; import { checkFullLicense } from '../../../license';

Some files were not shown because too many files have changed in this diff Show more