mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
a275262b8a
commit
ae5594849c
121 changed files with 1305 additions and 2304 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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/aiops_components @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_populated_object @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/query_utils @elastic/ml-ui
|
||||
x-pack/packages/ml/string_hash @elastic/ml-ui
|
||||
x-pack/packages/ml/url_state @elastic/ml-ui
|
||||
|
|
|
@ -356,10 +356,12 @@
|
|||
"@kbn/logging-mocks": "link:packages/kbn-logging-mocks",
|
||||
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
|
||||
"@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-populated-object": "link:x-pack/packages/ml/is_populated_object",
|
||||
"@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage",
|
||||
"@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-url-state": "link:x-pack/packages/ml/url_state",
|
||||
"@kbn/monaco": "link:packages/kbn-monaco",
|
||||
|
|
|
@ -838,6 +838,8 @@
|
|||
"@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-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-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-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-url-state": ["x-pack/packages/ml/url_state"],
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"xpack.main": "legacy/plugins/xpack_main",
|
||||
"xpack.maps": ["plugins/maps"],
|
||||
"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.osquery": ["plugins/osquery"],
|
||||
"xpack.painlessLab": "plugins/painless_lab",
|
||||
|
|
3
x-pack/packages/ml/date_picker/README.md
Normal file
3
x-pack/packages/ml/date_picker/README.md
Normal 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.
|
28
x-pack/packages/ml/date_picker/index.ts
Normal file
28
x-pack/packages/ml/date_picker/index.ts
Normal 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';
|
|
@ -5,4 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DatePickerWrapper } from './date_picker_wrapper';
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/ml/date_picker'],
|
||||
};
|
5
x-pack/packages/ml/date_picker/kibana.jsonc
Normal file
5
x-pack/packages/ml/date_picker/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/ml-date-picker",
|
||||
"owner": "@elastic/ml-ui"
|
||||
}
|
6
x-pack/packages/ml/date_picker/package.json
Normal file
6
x-pack/packages/ml/date_picker/package.json
Normal 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"
|
||||
}
|
|
@ -5,20 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
|
||||
import { useUrlState } from '@kbn/ml-url-state';
|
||||
import type { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
|
||||
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
|
||||
import { useToastNotificationService } from '../../../services/toast_notification_service';
|
||||
import { useDatePickerContext } from '../hooks/use_date_picker_context';
|
||||
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
|
||||
|
||||
import { DatePickerWrapper } from './date_picker_wrapper';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const EuiButtonMock = jest.fn(() => {
|
||||
return null;
|
||||
});
|
||||
const EuiSuperDatePickerMock = jest.fn(() => {
|
||||
return null;
|
||||
});
|
||||
|
@ -29,6 +33,9 @@ jest.mock('@elastic/eui', () => {
|
|||
return <>{children}</>;
|
||||
});
|
||||
return {
|
||||
useEuiBreakpoint: jest.fn(() => 'mediaQuery @media only screen and (max-width: 1199px)'),
|
||||
useIsWithinMaxBreakpoint: jest.fn(() => false),
|
||||
EuiButton: EuiButtonMock,
|
||||
EuiSuperDatePicker: EuiSuperDatePickerMock,
|
||||
EuiFlexGroup: EuiFlexGroupMock,
|
||||
EuiFlexItem: EuiFlexItemMock,
|
||||
|
@ -43,67 +50,82 @@ jest.mock('@kbn/ml-url-state', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../contexts/kibana/use_timefilter');
|
||||
|
||||
jest.mock('../../../services/toast_notification_service');
|
||||
|
||||
jest.mock('../../../contexts/kibana', () => ({
|
||||
useMlKibana: () => {
|
||||
jest.mock('../hooks/use_timefilter', () => ({
|
||||
useRefreshIntervalUpdates: jest.fn(),
|
||||
useTimefilter: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { of } = require('rxjs');
|
||||
return {
|
||||
services: {
|
||||
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(),
|
||||
},
|
||||
},
|
||||
getRefreshIntervalUpdate$: 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<
|
||||
typeof EuiSuperDatePicker
|
||||
>;
|
||||
|
||||
describe('Navigation Menu: <DatePickerWrapper />', () => {
|
||||
describe('<DatePickerWrapper />', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
MockedEuiSuperDatePicker.mockClear();
|
||||
|
@ -113,12 +135,16 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('Minimal initialization.', () => {
|
||||
test('Minimal initialization.', async () => {
|
||||
const refreshListener = jest.fn();
|
||||
const refreshSubscription = mlTimefilterRefresh$.subscribe(refreshListener);
|
||||
|
||||
const wrapper = mount(<DatePickerWrapper />);
|
||||
expect(wrapper.find(DatePickerWrapper)).toHaveLength(1);
|
||||
const displayWarningSpy = jest.fn(() => {});
|
||||
|
||||
(useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
|
||||
|
||||
render(<DatePickerWrapper />);
|
||||
|
||||
expect(refreshListener).toBeCalledTimes(0);
|
||||
|
||||
refreshSubscription.unsubscribe();
|
||||
|
@ -130,9 +156,7 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
|
|||
|
||||
const displayWarningSpy = jest.fn(() => {});
|
||||
|
||||
(useToastNotificationService as jest.Mock).mockReturnValueOnce({
|
||||
displayWarningToast: displayWarningSpy,
|
||||
});
|
||||
(useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
|
||||
|
||||
// act
|
||||
render(<DatePickerWrapper />);
|
||||
|
@ -151,9 +175,7 @@ describe('Navigation Menu: <DatePickerWrapper />', () => {
|
|||
|
||||
const displayWarningSpy = jest.fn(() => {});
|
||||
|
||||
(useToastNotificationService as jest.Mock).mockReturnValueOnce({
|
||||
displayWarningToast: displayWarningSpy,
|
||||
});
|
||||
(useDatePickerContext as jest.Mock).mockImplementation(mockContextFactory(displayWarningSpy));
|
||||
|
||||
// act
|
||||
render(<DatePickerWrapper />);
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { debounce } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
useEuiBreakpoint,
|
||||
useIsWithinMaxBreakpoint,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -19,19 +20,19 @@ import {
|
|||
OnRefreshProps,
|
||||
OnTimeChangeProps,
|
||||
} 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 { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import {
|
||||
useRefreshIntervalUpdates,
|
||||
useTimeRangeUpdates,
|
||||
} from '../../../contexts/kibana/use_timefilter';
|
||||
import { useToastNotificationService } from '../../../services/toast_notification_service';
|
||||
|
||||
import { useRefreshIntervalUpdates, useTimeRangeUpdates } from '../hooks/use_timefilter';
|
||||
import { useDatePickerContext } from '../hooks/use_date_picker_context';
|
||||
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
|
||||
|
||||
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
|
||||
const DATE_PICKER_MAX_WIDTH = '540px';
|
||||
|
||||
interface TimePickerQuickRange {
|
||||
from: string;
|
||||
|
@ -66,16 +67,46 @@ function updateLastRefresh(timeRange?: OnRefreshProps) {
|
|||
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();
|
||||
const config = services.uiSettings;
|
||||
const theme$ = services.theme.theme$;
|
||||
/**
|
||||
* DatePickerWrapper React Component
|
||||
*
|
||||
* @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 { timefilter, history } = services.data.query.timefilter;
|
||||
const { displayWarningToast } = useToastNotificationService();
|
||||
const isWithinLBreakpoint = useIsWithinMaxBreakpoint('l');
|
||||
|
||||
const { timefilter, history } = data.query.timefilter;
|
||||
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history);
|
||||
|
@ -117,7 +148,6 @@ export const DatePickerWrapper: FC = () => {
|
|||
[setGlobalState]
|
||||
);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges());
|
||||
const [isAutoRefreshSelectorEnabled, setIsAutoRefreshSelectorEnabled] = useState(
|
||||
timefilter.isAutoRefreshSelectorEnabled()
|
||||
|
@ -140,16 +170,16 @@ export const DatePickerWrapper: FC = () => {
|
|||
// Only warn about short interval with enabled auto-refresh.
|
||||
if (!isTooShort || refreshInterval.pause) return;
|
||||
|
||||
displayWarningToast(
|
||||
toasts.addWarning(
|
||||
{
|
||||
title: isResolvedFromUrlState
|
||||
? i18n.translate('xpack.ml.datePicker.shortRefreshIntervalURLWarningMessage', {
|
||||
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', {
|
||||
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(
|
||||
wrapWithTheme(
|
||||
|
@ -160,7 +190,7 @@ export const DatePickerWrapper: FC = () => {
|
|||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.pageRefreshResetButton"
|
||||
id="xpack.ml.datePicker.pageRefreshResetButton"
|
||||
defaultMessage="Set to {defaultInterval}"
|
||||
values={{
|
||||
defaultInterval: `${DEFAULT_REFRESH_INTERVAL_MS / 1000}s`,
|
||||
|
@ -186,7 +216,7 @@ export const DatePickerWrapper: FC = () => {
|
|||
|
||||
const dateFormat = config.get('dateFormat');
|
||||
const timePickerQuickRanges = config.get<TimePickerQuickRange[]>(
|
||||
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
|
||||
uiSettingsKeys.TIMEPICKER_QUICK_RANGES
|
||||
);
|
||||
|
||||
const commonlyUsedRanges = useMemo(
|
||||
|
@ -202,12 +232,6 @@ export const DatePickerWrapper: FC = () => {
|
|||
useEffect(() => {
|
||||
const subscriptions = new Subscription();
|
||||
|
||||
subscriptions.add(
|
||||
httpService.getLoadingCount$.subscribe((v) => {
|
||||
setIsLoading(v !== 0);
|
||||
})
|
||||
);
|
||||
|
||||
const enabledUpdated$ = timefilter.getEnabledUpdated$();
|
||||
if (enabledUpdated$ !== undefined) {
|
||||
subscriptions.add(
|
||||
|
@ -250,19 +274,21 @@ export const DatePickerWrapper: FC = () => {
|
|||
setRefreshInterval({ pause, value });
|
||||
}
|
||||
|
||||
const datePickerWidth = css({
|
||||
[useEuiBreakpoint(['xs', 's', 'm', 'l'])]: {
|
||||
maxWidth: DATE_PICKER_MAX_WIDTH,
|
||||
},
|
||||
});
|
||||
|
||||
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className="mlNavigationMenu__datePickerWrapper"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false} css={datePickerWidth}>
|
||||
<EuiSuperDatePicker
|
||||
isLoading={isLoading}
|
||||
start={time.from}
|
||||
end={time.to}
|
||||
isPaused={refreshInterval.pause}
|
||||
isAutoRefreshOnly={!isTimeRangeSelectorEnabled}
|
||||
isAutoRefreshOnly={!isTimeRangeSelectorEnabled || isAutoRefreshOnly}
|
||||
refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS}
|
||||
onTimeChange={updateTimeFilter}
|
||||
onRefresh={updateLastRefresh}
|
||||
|
@ -270,23 +296,24 @@ export const DatePickerWrapper: FC = () => {
|
|||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
dateFormat={dateFormat}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
updateButtonProps={{ iconOnly: isWithinLBreakpoint }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{isTimeRangeSelectorEnabled ? null : (
|
||||
{showRefresh === true || !isTimeRangeSelectorEnabled ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
iconType={'refresh'}
|
||||
onClick={() => updateLastRefresh()}
|
||||
data-test-subj={`mlRefreshPageButton${isLoading ? ' loading' : ' loaded'}`}
|
||||
data-test-subj={`mlDatePickerRefreshPageButton${isLoading ? ' loading' : ' loaded'}`}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<FormattedMessage id="xpack.ml.pageRefreshButton" defaultMessage="Refresh" />
|
||||
<FormattedMessage id="xpack.ml.datePicker.pageRefreshButton" defaultMessage="Refresh" />
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
};
|
|
@ -6,16 +6,30 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { FullTimeRangeSelector } from '.';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
import type { Query } from '@kbn/es-query';
|
||||
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.
|
||||
// The mock is hoisted to the top, so need to prefix the mock function
|
||||
// with 'mock' so it can be used lazily.
|
||||
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) =>
|
||||
mockSetFullTimeRange(indexPattern, query),
|
||||
}));
|
||||
|
@ -44,24 +58,25 @@ describe('FullTimeRangeSelector', () => {
|
|||
query,
|
||||
};
|
||||
|
||||
test('renders the selector', () => {
|
||||
const props = {
|
||||
...requiredProps,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<FullTimeRangeSelector {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('calls setFullTimeRange on clicking button', () => {
|
||||
const props = {
|
||||
...requiredProps,
|
||||
disabled: false,
|
||||
frozenDataPreference: FROZEN_TIER_PREFERENCE.EXCLUDE,
|
||||
setFrozenDataPreference: jest.fn(),
|
||||
timefilter: {} as TimefilterContract,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<FullTimeRangeSelector {...props} />);
|
||||
wrapper.find('EuiButton').simulate('click');
|
||||
const { getByText } = render(
|
||||
<IntlProvider locale="en">
|
||||
<DatePickerContextProvider {...mockDependencies}>
|
||||
<FullTimeRangeSelector {...props} />
|
||||
</DatePickerContextProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
fireEvent.click(getByText(/use full data/i));
|
||||
|
||||
expect(mockSetFullTimeRange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -5,14 +5,10 @@
|
|||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
|
@ -24,40 +20,74 @@ import {
|
|||
EuiRadioGroupOption,
|
||||
EuiToolTip,
|
||||
} 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 {
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Current data view.
|
||||
*/
|
||||
dataView: DataView;
|
||||
/**
|
||||
* Boolean flag to enable/disable the full time range button.
|
||||
*/
|
||||
disabled: boolean;
|
||||
/**
|
||||
* Optional DSL query.
|
||||
*/
|
||||
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,
|
||||
dataView,
|
||||
query,
|
||||
disabled,
|
||||
callback,
|
||||
}) => {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @type {FC<FullTimeRangeSelectorProps>}
|
||||
* @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 {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useAiopsAppContext();
|
||||
} = useDatePickerContext();
|
||||
|
||||
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
|
||||
const setRange = useCallback(
|
||||
|
@ -77,7 +107,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
} catch (e) {
|
||||
toasts.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.aiops.index.fullTimeRangeSelector.errorSettingTimeRangeNotification',
|
||||
'xpack.ml.datePicker.fullTimeRangeSelector.errorSettingTimeRangeNotification',
|
||||
{
|
||||
defaultMessage: 'An error occurred setting the time range.',
|
||||
}
|
||||
|
@ -90,15 +120,6 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
|
||||
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(
|
||||
(id: string) => {
|
||||
setFrozenDataPreference(id as FrozenTierPreference);
|
||||
|
@ -121,7 +142,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
{
|
||||
id: FROZEN_TIER_PREFERENCE.EXCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.aiops.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
|
||||
'xpack.ml.datePicker.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Exclude frozen data tier',
|
||||
}
|
||||
|
@ -130,7 +151,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
{
|
||||
id: FROZEN_TIER_PREFERENCE.INCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.aiops.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
|
||||
'xpack.ml.datePicker.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Include frozen data tier',
|
||||
}
|
||||
|
@ -157,12 +178,12 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
() =>
|
||||
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
|
||||
id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
|
||||
defaultMessage="Use full range of data excluding frozen data tier."
|
||||
/>
|
||||
) : (
|
||||
<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."
|
||||
/>
|
||||
),
|
||||
|
@ -175,10 +196,10 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
<EuiButton
|
||||
isDisabled={disabled}
|
||||
onClick={() => setRange(dataView, query, true)}
|
||||
data-test-subj="aiopsExplainLogRatesSpikeButtonUseFullData"
|
||||
data-test-subj="mlDatePickerButtonUseFullData"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.index.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
id="xpack.ml.datePicker.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
defaultMessage="Use full data"
|
||||
/>
|
||||
</EuiButton>
|
||||
|
@ -192,7 +213,7 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
|||
size="m"
|
||||
iconType="boxesVertical"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.aiops.index.fullTimeRangeSelector.moreOptionsButtonAriaLabel',
|
||||
'xpack.ml.datePicker.fullTimeRangeSelector.moreOptionsButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'More options',
|
||||
}
|
|
@ -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>;
|
||||
};
|
|
@ -6,31 +6,35 @@
|
|||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useDatePickerContext } from './use_date_picker_context';
|
||||
import { useTimefilter } from './use_timefilter';
|
||||
|
||||
jest.mock('./kibana_context', () => ({
|
||||
useMlKibana: () => {
|
||||
return {
|
||||
services: {
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
disableTimeRangeSelector: jest.fn(),
|
||||
disableAutoRefreshSelector: jest.fn(),
|
||||
enableTimeRangeSelector: jest.fn(),
|
||||
enableAutoRefreshSelector: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
jest.mock('./use_date_picker_context');
|
||||
|
||||
const mockContextFactory = (
|
||||
isAutoRefreshSelectorEnabled: boolean = true,
|
||||
isTimeRangeSelectorEnabled: boolean = true
|
||||
) => ({
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
disableTimeRangeSelector: jest.fn(),
|
||||
disableAutoRefreshSelector: jest.fn(),
|
||||
enableTimeRangeSelector: jest.fn(),
|
||||
enableAutoRefreshSelector: jest.fn(),
|
||||
isAutoRefreshSelectorEnabled: jest.fn(() => isAutoRefreshSelectorEnabled),
|
||||
isTimeRangeSelectorEnabled: jest.fn(() => isTimeRangeSelectorEnabled),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
describe('useTimefilter', () => {
|
||||
test('will not trigger any date picker settings by default', () => {
|
||||
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory());
|
||||
|
||||
const { result } = renderHook(() => useTimefilter());
|
||||
const timefilter = result.current;
|
||||
|
||||
|
@ -41,6 +45,8 @@ describe('useTimefilter', () => {
|
|||
});
|
||||
|
||||
test('custom disabled overrides', () => {
|
||||
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory());
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false })
|
||||
);
|
||||
|
@ -53,6 +59,8 @@ describe('useTimefilter', () => {
|
|||
});
|
||||
|
||||
test('custom enabled overrides', () => {
|
||||
(useDatePickerContext as jest.Mock).mockReturnValueOnce(mockContextFactory(false, false));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true })
|
||||
);
|
|
@ -9,24 +9,41 @@ import { useEffect, useMemo } from 'react';
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
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 {
|
||||
/**
|
||||
* Boolean flag to enable/disable the time range selector
|
||||
*/
|
||||
timeRangeSelector?: boolean;
|
||||
/**
|
||||
* Boolean flag to enable/disable the auto refresh selector
|
||||
*/
|
||||
autoRefreshSelector?: boolean;
|
||||
}
|
||||
|
||||
export const useTimefilter = ({
|
||||
timeRangeSelector,
|
||||
autoRefreshSelector,
|
||||
}: UseTimefilterOptions = {}) => {
|
||||
/**
|
||||
* Custom hook to get the timefilter from runtime dependencies.
|
||||
*
|
||||
* @param {UseTimefilterOptions} options - time filter options
|
||||
* @returns {TimefilterContract} timefilter
|
||||
*/
|
||||
export const useTimefilter = (options: UseTimefilterOptions = {}): TimefilterContract => {
|
||||
const { timeRangeSelector, autoRefreshSelector } = options;
|
||||
const {
|
||||
data: {
|
||||
query: {
|
||||
timefilter: { timefilter },
|
||||
},
|
||||
},
|
||||
} = useAiopsAppContext();
|
||||
} = useDatePickerContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (timeRangeSelector === true && !timefilter.isTimeRangeSelectorEnabled()) {
|
||||
|
@ -45,6 +62,11 @@ export const useTimefilter = ({
|
|||
return timefilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to return refresh interval updates from the `refreshIntervalObservable$` observable.
|
||||
*
|
||||
* @returns refresh interval update
|
||||
*/
|
||||
export const useRefreshIntervalUpdates = () => {
|
||||
const timefilter = useTimefilter();
|
||||
|
||||
|
@ -56,7 +78,13 @@ export const useRefreshIntervalUpdates = () => {
|
|||
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 getTimeCallback = useMemo(() => {
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -5,38 +5,50 @@
|
|||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
|
||||
export interface GetTimeFieldRangeResponse {
|
||||
success: boolean;
|
||||
start: { epoch: number; string: string };
|
||||
end: { epoch: number; string: string };
|
||||
import type { GetTimeFieldRangeResponse } from './types';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
query,
|
||||
runtimeMappings,
|
||||
http,
|
||||
}: {
|
||||
index: string;
|
||||
timeFieldName?: string;
|
||||
query?: QueryDslQueryContainer;
|
||||
runtimeMappings?: estypes.MappingRuntimeFields;
|
||||
http: HttpStart;
|
||||
}) {
|
||||
const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings });
|
||||
/**
|
||||
*
|
||||
* @param options - GetTimeFieldRangeOptions
|
||||
* @returns GetTimeFieldRangeResponse
|
||||
*/
|
||||
export async function getTimeFieldRange(options: GetTimeFieldRangeOptions) {
|
||||
const { http, ...body } = options;
|
||||
|
||||
return await http.fetch<GetTimeFieldRangeResponse>({
|
||||
path: `/internal/file_upload/time_field_range`,
|
||||
method: 'POST',
|
||||
body,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
|
@ -5,14 +5,23 @@
|
|||
* 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';
|
||||
|
||||
/**
|
||||
* State definition of `mlTimefilterRefresh$` observable.
|
||||
*/
|
||||
export interface Refresh {
|
||||
/**
|
||||
* Timestamp of the last time a refresh got triggered.
|
||||
*/
|
||||
lastRefresh: number;
|
||||
/**
|
||||
* The time range triggered by the refresh.
|
||||
*/
|
||||
timeRange?: { start: string; end: string };
|
||||
}
|
||||
|
||||
export const aiopsRefresh$ = new Subject<Refresh>();
|
||||
/**
|
||||
* Observable to hold `Refresh` state.
|
||||
*/
|
||||
export const mlTimefilterRefresh$ = new Subject<Refresh>();
|
38
x-pack/packages/ml/date_picker/src/services/types.ts
Normal file
38
x-pack/packages/ml/date_picker/src/services/types.ts
Normal 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;
|
||||
}
|
20
x-pack/packages/ml/date_picker/src/storage.ts
Normal file
20
x-pack/packages/ml/date_picker/src/storage.ts
Normal 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];
|
31
x-pack/packages/ml/date_picker/tsconfig.json
Normal file
31
x-pack/packages/ml/date_picker/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
/**
|
||||
* Checks whether the supplied argument is not `undefined` and not `null`.
|
||||
*
|
||||
* @param argument
|
||||
* @param argument - argument to check whether it is defined.
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isDefined<T>(argument: T | undefined | null): argument is T {
|
||||
|
|
3
x-pack/packages/ml/query_utils/README.md
Normal file
3
x-pack/packages/ml/query_utils/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/ml-query-utils
|
||||
|
||||
Query utilities.
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { FullTimeRangeSelector } from './full_time_range_selector';
|
||||
export { addExcludeFrozenToQuery } from './src/add_exclude_frozen_to_query';
|
|
@ -5,4 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DatePickerWrapper } from './date_picker_wrapper';
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/ml/query_utils'],
|
||||
};
|
5
x-pack/packages/ml/query_utils/kibana.jsonc
Normal file
5
x-pack/packages/ml/query_utils/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/ml-query-utils",
|
||||
"owner": "@elastic/ml-ui"
|
||||
}
|
6
x-pack/packages/ml/query_utils/package.json
Normal file
6
x-pack/packages/ml/query_utils/package.json
Normal 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"
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { addExcludeFrozenToQuery } from './query_utils';
|
||||
import { addExcludeFrozenToQuery } from './add_exclude_frozen_to_query';
|
||||
|
||||
describe('Util: addExcludeFrozenToQuery()', () => {
|
||||
test('Validation checks.', () => {
|
|
@ -9,6 +9,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
|||
import { cloneDeep } from 'lodash';
|
||||
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) => {
|
||||
const FROZEN_TIER_TERM = {
|
||||
term: {
|
||||
|
@ -31,11 +37,11 @@ export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer |
|
|||
delete query.match_all;
|
||||
|
||||
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)) {
|
||||
query.bool.must_not.push(FROZEN_TIER_TERM);
|
||||
} else {
|
||||
// If there's already a must_not condition
|
||||
// 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];
|
||||
}
|
21
x-pack/packages/ml/query_utils/tsconfig.json
Normal file
21
x-pack/packages/ml/query_utils/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
|
@ -8,13 +8,9 @@
|
|||
// TODO Consolidate with duplicate query utils in
|
||||
// `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 { Query } from '@kbn/es-query';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils';
|
||||
import type { GroupTableItem } from '../../components/spike_analysis_table/types';
|
||||
|
||||
|
@ -93,46 +89,3 @@ export function buildBaseFilterCriteria(
|
|||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -18,12 +18,12 @@ import { type DataViewField } from '@kbn/data-views-plugin/public';
|
|||
import { startWith } from 'rxjs';
|
||||
import type { Query, Filter } from '@kbn/es-query';
|
||||
import { usePageUrlState } from '@kbn/ml-url-state';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import {
|
||||
createMergedEsQuery,
|
||||
getEsQueryFromSavedSearch,
|
||||
} from '../../application/utils/search_utils';
|
||||
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 { type TimeBuckets, TimeBucketsInterval } from '../../../common/time_buckets';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
|
|
|
@ -6,12 +6,18 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { StorageContextProvider } from '@kbn/ml-local-storage';
|
||||
import { UrlStateProvider } from '@kbn/ml-url-state';
|
||||
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';
|
||||
|
@ -36,15 +42,25 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
|
|||
savedSearch,
|
||||
appDependencies,
|
||||
}) => {
|
||||
const datePickerDeps = {
|
||||
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<PageHeader />
|
||||
<ChangePointDetectionContextProvider>
|
||||
<ChangePointDetectionPage />
|
||||
</ChangePointDetectionContextProvider>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<PageHeader />
|
||||
<EuiSpacer />
|
||||
<ChangePointDetectionContextProvider>
|
||||
<ChangePointDetectionPage />
|
||||
</ChangePointDetectionContextProvider>
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
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 { 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';
|
||||
|
||||
export interface ChartComponentProps {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
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 { UrlStateProvider } from '@kbn/ml-url-state';
|
||||
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 {
|
||||
SEARCH_QUERY_LANGUAGE,
|
||||
|
@ -24,6 +28,7 @@ import {
|
|||
} from '../../application/utils/search_utils';
|
||||
import type { AiopsAppDependencies } 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 { 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 (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<SpikeAnalysisTableRowStateProvider>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<ExplainLogRateSpikesPage dataView={dataView} savedSearch={savedSearch} />
|
||||
</StorageContextProvider>
|
||||
</SpikeAnalysisTableRowStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<SpikeAnalysisTableRowStateProvider>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<ExplainLogRateSpikesPage />
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</SpikeAnalysisTableRowStateProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState, FC } from 'react';
|
||||
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
|
@ -13,34 +14,29 @@ import {
|
|||
EuiHorizontalRule,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody_Deprecated as EuiPageContentBody,
|
||||
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { WindowParameters } from '@kbn/aiops-utils';
|
||||
import type { ChangePoint } from '@kbn/ml-agg-utils';
|
||||
import { Filter, FilterStateStore, Query } from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
|
||||
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 { SearchQueryLanguage, SavedSearchSavedObject } from '../../application/utils/search_utils';
|
||||
import { SearchQueryLanguage } from '../../application/utils/search_utils';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
|
||||
import { DocumentCountContent } from '../document_count_content/document_count_content';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
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 { 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) {
|
||||
if (changePoint) {
|
||||
|
@ -52,22 +48,9 @@ function getDocumentCountStatsSplitLabel(changePoint?: ChangePoint, group?: Grou
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
export const ExplainLogRateSpikesPage: FC = () => {
|
||||
const { data: dataService } = useAiopsAppContext();
|
||||
const { dataView, savedSearch } = useDataSource();
|
||||
|
||||
const {
|
||||
currentSelectedChangePoint,
|
||||
|
@ -186,40 +169,7 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
|
|||
|
||||
return (
|
||||
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<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>
|
||||
<PageHeader />
|
||||
<EuiHorizontalRule />
|
||||
<EuiPageContentBody>
|
||||
<EuiFlexGroup gutterSize="m" direction="column">
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -5,12 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import type { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { StorageContextProvider } from '@kbn/ml-local-storage';
|
||||
import { UrlStateProvider } from '@kbn/ml-url-state';
|
||||
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 type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
||||
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
|
||||
|
@ -31,12 +37,23 @@ export const LogCategorizationAppState: FC<LogCategorizationAppStateProps> = ({
|
|||
savedSearch,
|
||||
appDependencies,
|
||||
}) => {
|
||||
const datePickerDeps = {
|
||||
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<LogCategorizationPage dataView={dataView} savedSearch={savedSearch} />
|
||||
</StorageContextProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<LogCategorizationPage />
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
);
|
||||
|
|
|
@ -5,59 +5,46 @@
|
|||
* 2.0.
|
||||
*/
|
||||
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 {
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageBody,
|
||||
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
|
||||
EuiTitle,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormRow,
|
||||
EuiLoadingContent,
|
||||
} 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 { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
import { useCss } from '../../hooks/use_css';
|
||||
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { SearchPanel } from '../search_panel';
|
||||
import type {
|
||||
SearchQueryLanguage,
|
||||
SavedSearchSavedObject,
|
||||
} from '../../application/utils/search_utils';
|
||||
import type { SearchQueryLanguage } from '../../application/utils/search_utils';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
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 { useCategorizeRequest } from './use_categorize_request';
|
||||
import { CategoryTable } from './category_table';
|
||||
import { DocumentCountChart } from './document_count_chart';
|
||||
import { InformationText } from './information_text';
|
||||
|
||||
export interface LogCategorizationPageProps {
|
||||
dataView: DataView;
|
||||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
}
|
||||
|
||||
const BAR_TARGET = 20;
|
||||
|
||||
export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({
|
||||
dataView,
|
||||
savedSearch,
|
||||
}) => {
|
||||
const { aiopsPageHeader, dataViewTitleHeader } = useCss();
|
||||
export const LogCategorizationPage: FC = () => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useAiopsAppContext();
|
||||
const { dataView, savedSearch } = useDataSource();
|
||||
|
||||
const { runCategorizeRequest, cancelRequest } = useCategorizeRequest();
|
||||
const [aiopsListState, setAiopsListState] = useState(restorableDefaults);
|
||||
|
@ -219,41 +206,8 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<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>
|
||||
<EuiPageBody data-test-subj="aiopsLogCategorizationPage" paddingSize="none" panelled={false}>
|
||||
<PageHeader />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
useIsWithinMaxBreakpoint,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
|
@ -14,19 +16,40 @@ import {
|
|||
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
|
||||
} from '@elastic/eui';
|
||||
|
||||
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 { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useTimefilter } from '../../hooks/use_time_filter';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
import {
|
||||
AIOPS_FROZEN_TIER_PREFERENCE,
|
||||
type AiOpsKey,
|
||||
type AiOpsStorageMapped,
|
||||
} from '../../types/storage';
|
||||
|
||||
export const PageHeader: FC = () => {
|
||||
const { aiopsPageHeader, dataViewTitleHeader } = useCss();
|
||||
|
||||
const [, setGlobalState] = useUrlState('_g');
|
||||
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({
|
||||
timeRangeSelector: dataView.timeFieldName !== undefined,
|
||||
autoRefreshSelector: true,
|
||||
|
@ -39,44 +62,54 @@ export const PageHeader: FC = () => {
|
|||
[setGlobalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader css={aiopsPageHeader}>
|
||||
<EuiPageContentHeaderSection>
|
||||
<div css={dataViewTitleHeader}>
|
||||
<EuiTitle size="s">
|
||||
<h2>{dataView.getName()}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
const hasValidTimeField = useMemo(
|
||||
() => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '',
|
||||
[dataView.timeFieldName]
|
||||
);
|
||||
|
||||
<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}
|
||||
callback={updateTimeState}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
const isWithinLBreakpoint = useIsWithinMaxBreakpoint('l');
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader css={aiopsPageHeader}>
|
||||
<EuiPageContentHeaderSection>
|
||||
<div css={dataViewTitleHeader}>
|
||||
<EuiTitle size="s">
|
||||
<h2>{dataView.getName()}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
{isWithinLBreakpoint ? <EuiSpacer size="m" /> : null}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsTimeRangeSelectorSection"
|
||||
>
|
||||
{hasValidTimeField ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
<FullTimeRangeSelector
|
||||
frozenDataPreference={frozenDataPreference}
|
||||
setFrozenDataPreference={setFrozenDataPreference}
|
||||
dataView={dataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
callback={updateTimeState}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper
|
||||
isAutoRefreshOnly={!hasValidTimeField}
|
||||
showRefresh={!hasValidTimeField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,8 +14,13 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
|
|||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { CoreStart, CoreSetup, HttpStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import type { ThemeServiceStart } from '@kbn/core/public';
|
||||
import type {
|
||||
CoreStart,
|
||||
CoreSetup,
|
||||
HttpStart,
|
||||
IUiSettingsClient,
|
||||
ThemeServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
|
||||
export interface AiopsAppDependencies {
|
||||
|
|
|
@ -10,25 +10,23 @@ import { merge } from 'rxjs';
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { ChangePoint } from '@kbn/ml-agg-utils';
|
||||
|
||||
import type { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
|
||||
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 { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state';
|
||||
import {
|
||||
getEsQueryFromSavedSearch,
|
||||
SavedSearchSavedObject,
|
||||
} 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 { useTimeBuckets } from './use_time_buckets';
|
||||
import { useAiopsAppContext } from './use_aiops_app_context';
|
||||
|
||||
import { useDocumentCountStats } from './use_document_count_stats';
|
||||
|
||||
const DEFAULT_BAR_TARGET = 75;
|
||||
|
||||
export const useData = (
|
||||
|
@ -153,7 +151,7 @@ export const useData = (
|
|||
const timeUpdateSubscription = merge(
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
timefilter.getTimeUpdate$(),
|
||||
aiopsRefresh$
|
||||
mlTimefilterRefresh$
|
||||
).subscribe(() => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
|
|
|
@ -15,7 +15,7 @@ export const DataSourceContext = createContext<{
|
|||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
}>({
|
||||
get dataView(): never {
|
||||
throw new Error('Context is not implemented');
|
||||
throw new Error('DataSourceContext is not implemented');
|
||||
},
|
||||
savedSearch: null,
|
||||
});
|
||||
|
|
|
@ -5,16 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
|
||||
|
||||
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<{
|
||||
[AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
|
||||
}> | null;
|
||||
|
|
|
@ -44,6 +44,8 @@
|
|||
"@kbn/es-types",
|
||||
"@kbn/ml-url-state",
|
||||
"@kbn/ml-local-storage",
|
||||
"@kbn/ml-date-picker",
|
||||
"@kbn/ml-local-storage",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -10,14 +10,11 @@ import { Action } from '@elastic/eui/src/components/basic_table/action_types';
|
|||
import { MutableRefObject } from 'react';
|
||||
import { DataView } from '@kbn/data-views-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 { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
|
||||
import { FieldVisConfig } from '../../stats_table/types';
|
||||
import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context';
|
||||
import {
|
||||
dataVisualizerRefresh$,
|
||||
Refresh,
|
||||
} from '../../../../index_data_visualizer/services/timefilter_refresh_service';
|
||||
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
|
||||
import { APP_ID } from '../../../../../../common/constants';
|
||||
|
||||
|
@ -36,7 +33,7 @@ export function getActions(
|
|||
const refresh: Refresh = {
|
||||
lastRefresh: Date.now(),
|
||||
};
|
||||
dataVisualizerRefresh$.next(refresh);
|
||||
mlTimefilterRefresh$.next(refresh);
|
||||
};
|
||||
// Navigate to Lens with prefilled chart for data field
|
||||
if (services.application?.capabilities?.visualize?.show === true && lensPlugin !== undefined) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import React, { FC, useState, useEffect } from 'react';
|
|||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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 { useUrlState } from '@kbn/ml-url-state';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
|
@ -26,7 +26,6 @@ interface Props {
|
|||
searchString?: string | { [key: string]: any };
|
||||
searchQueryLanguage?: string;
|
||||
getAdditionalLinks?: GetAdditionalLinks;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const ACTIONS_PANEL_WIDTH = '240px';
|
||||
|
@ -36,7 +35,6 @@ export const ActionsPanel: FC<Props> = ({
|
|||
searchString,
|
||||
searchQueryLanguage,
|
||||
getAdditionalLinks,
|
||||
compact,
|
||||
}) => {
|
||||
const [globalState] = useUrlState('_g');
|
||||
|
||||
|
@ -121,19 +119,17 @@ export const ActionsPanel: FC<Props> = ({
|
|||
const showActionsPanel =
|
||||
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
|
||||
// passed the recognizerResults object, and then run the recognizer check which
|
||||
// controls whether the recognizer section is ultimately displayed.
|
||||
return showActionsPanel ? (
|
||||
<div
|
||||
data-test-subj="dataVisualizerActionsPanel"
|
||||
css={
|
||||
!compact &&
|
||||
css`
|
||||
width: ${ACTIONS_PANEL_WIDTH};
|
||||
`
|
||||
}
|
||||
>
|
||||
<div data-test-subj="dataVisualizerActionsPanel" css={dvActionsPanel}>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -9,8 +9,8 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { mlTimefilterRefresh$, Refresh } from '@kbn/ml-date-picker';
|
||||
import { useDataVisualizerKibana } from '../../../kibana_context';
|
||||
import { dataVisualizerRefresh$, Refresh } from '../../services/timefilter_refresh_service';
|
||||
|
||||
export interface DataVisualizerDataViewManagementProps {
|
||||
/**
|
||||
|
@ -56,7 +56,7 @@ export function DataVisualizerDataViewManagement(props: DataVisualizerDataViewMa
|
|||
const refresh: Refresh = {
|
||||
lastRefresh: Date.now(),
|
||||
};
|
||||
dataVisualizerRefresh$.next(refresh);
|
||||
mlTimefilterRefresh$.next(refresh);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -5,10 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import type { Required } from 'utility-types';
|
||||
|
||||
import {
|
||||
useEuiBreakpoint,
|
||||
useIsWithinMaxBreakpoint,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageBody,
|
||||
|
@ -26,15 +29,20 @@ import { Filter, FilterStateStore, Query } from '@kbn/es-query';
|
|||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
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 { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
|
||||
import {
|
||||
DV_FROZEN_TIER_PREFERENCE,
|
||||
DV_RANDOM_SAMPLER_PREFERENCE,
|
||||
type DVKey,
|
||||
type DVStorageMapped,
|
||||
} from '../../types/storage';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import {
|
||||
DataVisualizerTable,
|
||||
ItemIdToExpandedRowMap,
|
||||
|
@ -57,7 +65,6 @@ import { OMIT_FIELDS } from '../../../../../common/constants';
|
|||
import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
|
||||
import { SearchPanel } from '../search_panel';
|
||||
import { ActionsPanel } from '../actions_panel';
|
||||
import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper';
|
||||
import { createMergedEsQuery } from '../../utils/saved_search_utils';
|
||||
import { DataVisualizerDataViewManagement } from '../data_view_management';
|
||||
import { GetAdditionalLinks } from '../../../common/components/results_links';
|
||||
|
@ -125,7 +132,6 @@ export interface IndexDataVisualizerViewProps {
|
|||
currentSavedSearch: SavedSearchSavedObject | null;
|
||||
currentSessionId?: string;
|
||||
getAdditionalLinks?: GetAdditionalLinks;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => {
|
||||
|
@ -136,6 +142,15 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
DVStorageMapped<typeof DV_RANDOM_SAMPLER_PREFERENCE>
|
||||
>(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(
|
||||
() =>
|
||||
getDefaultDataVisualizerListState({
|
||||
|
@ -161,7 +176,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
dataVisualizerProps.currentSavedSearch
|
||||
);
|
||||
|
||||
const { currentDataView, currentSessionId, getAdditionalLinks, compact } = dataVisualizerProps;
|
||||
const { currentDataView, currentSessionId, getAdditionalLinks } = dataVisualizerProps;
|
||||
|
||||
useEffect(() => {
|
||||
if (dataVisualizerProps?.currentSavedSearch !== undefined) {
|
||||
|
@ -458,14 +473,21 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
() => currentDataView.timeFieldName !== undefined && currentDataView.timeFieldName !== '',
|
||||
[currentDataView.timeFieldName]
|
||||
);
|
||||
|
||||
const dvPageHeader = css({
|
||||
[useEuiBreakpoint(['xs', 's', 'm', 'l', 'xl'])]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
});
|
||||
|
||||
const isWithinXl = useIsWithinMaxBreakpoint('xl');
|
||||
|
||||
return (
|
||||
<EuiPageBody data-test-subj="dataVisualizerIndexPage" paddingSize="none" panelled={false}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader
|
||||
data-test-subj="dataVisualizerPageHeader"
|
||||
css={compact ? { flexDirection: 'column', alignItems: 'flex-start' } : null}
|
||||
>
|
||||
<EuiPageContentHeader data-test-subj="dataVisualizerPageHeader" css={dvPageHeader}>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiFlexGroup
|
||||
data-test-subj="dataViewTitleHeader"
|
||||
|
@ -483,7 +505,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
</EuiFlexGroup>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
{compact ? <EuiSpacer size="m" /> : null}
|
||||
{isWithinXl ? <EuiSpacer size="m" /> : null}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
|
@ -493,6 +515,8 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
{hasValidTimeField ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
frozenDataPreference={frozenDataPreference}
|
||||
setFrozenDataPreference={setFrozenDataPreference}
|
||||
dataView={currentDataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
|
@ -504,7 +528,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
<DatePickerWrapper
|
||||
isAutoRefreshOnly={!hasValidTimeField}
|
||||
showRefresh={!hasValidTimeField}
|
||||
compact={compact}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -513,7 +536,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageContentBody>
|
||||
<EuiFlexGroup gutterSize="m" direction={compact ? 'column' : 'row'}>
|
||||
<EuiFlexGroup gutterSize="m" direction={isWithinXl ? 'column' : 'row'}>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<SearchPanel
|
||||
|
@ -530,7 +553,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
setVisibleFieldNames={setVisibleFieldNames}
|
||||
showEmptyFields={showEmptyFields}
|
||||
onAddFilter={onAddFilter}
|
||||
compact={compact}
|
||||
/>
|
||||
|
||||
{overallStats?.totalCount !== undefined && (
|
||||
|
@ -576,14 +598,13 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{compact ? <EuiSpacer size="m" /> : null}
|
||||
{isWithinXl ? <EuiSpacer size="m" /> : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionsPanel
|
||||
dataView={currentDataView}
|
||||
searchQueryLanguage={searchQueryLanguage}
|
||||
searchString={searchString}
|
||||
getAdditionalLinks={getAdditionalLinks}
|
||||
compact={compact}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
|
||||
import React, { FC, useEffect, useState } from '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 { Query, Filter } from '@kbn/es-query';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
|
@ -45,7 +51,6 @@ interface Props {
|
|||
}): void;
|
||||
showEmptyFields: boolean;
|
||||
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const SearchPanel: FC<Props> = ({
|
||||
|
@ -60,7 +65,6 @@ export const SearchPanel: FC<Props> = ({
|
|||
visibleFieldNames,
|
||||
setSearchParams,
|
||||
showEmptyFields,
|
||||
compact,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
|
@ -139,6 +143,8 @@ export const SearchPanel: FC<Props> = ({
|
|||
},
|
||||
});
|
||||
|
||||
const isWithinXl = useIsWithinMaxBreakpoint('xl');
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
|
@ -168,7 +174,7 @@ export const SearchPanel: FC<Props> = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{compact ? <EuiSpacer size="s" /> : null}
|
||||
{isWithinXl ? <EuiSpacer size="s" /> : null}
|
||||
<EuiFlexItem grow={2} css={dvSearchPanelControls}>
|
||||
<DataVisualizerFieldNamesFilter
|
||||
overallStats={overallStats}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -20,9 +21,12 @@ import {
|
|||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} 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 type { Query } from '@kbn/es-query';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
|
||||
import { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
import { SamplingOption } from '../../../../../common/types/field_stats';
|
||||
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 services = { ...this.services[0], ...this.services[1] };
|
||||
const datePickerDeps = {
|
||||
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<KibanaThemeProvider theme$={this.services[0].theme.theme$}>
|
||||
<KibanaContextProvider services={{ ...this.services[0], ...this.services[1] }}>
|
||||
<Suspense fallback={<EmbeddableLoading />}>
|
||||
<IndexDataVisualizerViewWrapper
|
||||
id={this.input.id}
|
||||
embeddableContext={this}
|
||||
embeddableInput={this.getInput$()}
|
||||
onOutputChange={(output) => this.updateOutput(output)}
|
||||
/>
|
||||
</Suspense>
|
||||
<KibanaContextProvider services={services}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<Suspense fallback={<EmbeddableLoading />}>
|
||||
<IndexDataVisualizerViewWrapper
|
||||
id={this.input.id}
|
||||
embeddableContext={this}
|
||||
embeddableInput={this.getInput$()}
|
||||
onOutputChange={(output) => this.updateOutput(output)}
|
||||
/>
|
||||
</Suspense>
|
||||
</DatePickerContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
</I18nContext>,
|
||||
|
|
|
@ -15,13 +15,12 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
|
|||
import seedrandom from 'seedrandom';
|
||||
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 { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
|
||||
import type { RandomSamplerOption } from '../constants/random_sampler';
|
||||
import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state';
|
||||
import { useDataVisualizerKibana } from '../../kibana_context';
|
||||
import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils';
|
||||
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 type { FieldVisConfig } from '../../common/components/stats_table/types';
|
||||
import {
|
||||
|
@ -313,7 +312,7 @@ export const useDataVisualizerGridData = (
|
|||
const timeUpdateSubscription = merge(
|
||||
timefilter.getTimeUpdate$(),
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
dataVisualizerRefresh$
|
||||
mlTimefilterRefresh$
|
||||
).subscribe(() => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
|
|
|
@ -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());
|
||||
};
|
|
@ -5,19 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
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 { parse, stringify } from 'query-string';
|
||||
import { isEqual, throttle } from 'lodash';
|
||||
import { EuiResizeObserver } from '@elastic/eui';
|
||||
import { isEqual } from 'lodash';
|
||||
import { encode } from '@kbn/rison';
|
||||
import { SimpleSavedObject } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { getNestedProperty } from '@kbn/ml-nested-property';
|
||||
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
Provider as UrlStateContextProvider,
|
||||
parseUrlState,
|
||||
|
@ -38,6 +45,8 @@ import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_
|
|||
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
|
||||
import { DV_STORAGE_KEYS } from './types/storage';
|
||||
|
||||
const XXL_BREAKPOINT = 1400;
|
||||
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
||||
export interface DataVisualizerStateContextProviderProps {
|
||||
|
@ -247,36 +256,15 @@ export const DataVisualizerStateContextProvider: FC<DataVisualizerStateContextPr
|
|||
[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 (
|
||||
<UrlStateContextProvider value={{ searchString: urlSearchString, setUrlState }}>
|
||||
{currentDataView ? (
|
||||
// Needs ResizeObserver to measure window width - side bar navigation
|
||||
<EuiResizeObserver onResize={resizeHandler}>
|
||||
{(resizeRef) => (
|
||||
<div ref={resizeRef}>
|
||||
<IndexDataVisualizerComponent
|
||||
currentDataView={currentDataView}
|
||||
currentSavedSearch={currentSavedSearch}
|
||||
currentSessionId={currentSessionId}
|
||||
getAdditionalLinks={getAdditionalLinks}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiResizeObserver>
|
||||
<IndexDataVisualizerComponent
|
||||
currentDataView={currentDataView}
|
||||
currentSavedSearch={currentSavedSearch}
|
||||
currentSessionId={currentSessionId}
|
||||
getAdditionalLinks={getAdditionalLinks}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
@ -317,15 +305,30 @@ export const IndexDataVisualizer: FC<{
|
|||
unifiedSearch,
|
||||
...coreStart,
|
||||
};
|
||||
const datePickerDeps = {
|
||||
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
return (
|
||||
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
|
||||
<KibanaThemeProvider
|
||||
theme$={coreStart.theme.theme$}
|
||||
modify={{
|
||||
breakpoint: {
|
||||
xxl: XXL_BREAKPOINT,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<KibanaContextProvider services={{ ...services }}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={DV_STORAGE_KEYS}>
|
||||
<DataVisualizerStateContextProvider
|
||||
IndexDataVisualizerComponent={IndexDataVisualizerView}
|
||||
getAdditionalLinks={getAdditionalLinks}
|
||||
/>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<DataVisualizerStateContextProvider
|
||||
IndexDataVisualizerComponent={IndexDataVisualizerView}
|
||||
getAdditionalLinks={getAdditionalLinks}
|
||||
/>
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -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>();
|
|
@ -5,20 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
|
||||
|
||||
import { RandomSamplerOption } from '../constants/random_sampler';
|
||||
|
||||
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';
|
||||
export const DV_RANDOM_SAMPLER_PREFERENCE = 'dataVisualizer.randomSamplerPreference';
|
||||
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<{
|
||||
[DV_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
|
||||
[DV_RANDOM_SAMPLER_PREFERENCE]: RandomSamplerOption;
|
||||
|
|
|
@ -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' } } }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -53,6 +53,7 @@
|
|||
"@kbn/ml-nested-property",
|
||||
"@kbn/ml-url-state",
|
||||
"@kbn/ml-local-storage",
|
||||
"@kbn/ml-date-picker",
|
||||
"@kbn/ml-is-defined",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
|
||||
|
||||
import { EntityFieldType } from './anomalies';
|
||||
|
||||
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_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 =
|
||||
| {
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -8,24 +8,28 @@
|
|||
import React, { FC } from 'react';
|
||||
import './_index.scss';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { AppMountParameters, CoreStart, HttpStart } from '@kbn/core/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 { 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 { StorageContextProvider } from '@kbn/ml-local-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 { setLicenseCache } from './license';
|
||||
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
|
||||
import { mlUsageCollectionProvider } from './services/usage_collection';
|
||||
|
||||
import { MlRouter } from './routing';
|
||||
import { mlApiServicesProvider } from './services/ml_api_service';
|
||||
import { HttpService } from './services/http_service';
|
||||
import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator';
|
||||
|
||||
export type MlDependencies = Omit<
|
||||
MlSetupDependencies,
|
||||
|
@ -98,6 +102,13 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
...coreStart,
|
||||
};
|
||||
|
||||
const datePickerDeps = {
|
||||
...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
const I18nContext = coreStart.i18n.Context;
|
||||
const ApplicationUsageTrackingProvider =
|
||||
deps.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
|
||||
|
@ -113,7 +124,9 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
}}
|
||||
>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={ML_STORAGE_KEYS}>
|
||||
<MlRouter pageDeps={pageDeps} />
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<MlRouter pageDeps={pageDeps} />
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -5,20 +5,21 @@
|
|||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import type { AppMountParameters } from '@kbn/core/public';
|
||||
import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
|
||||
import { createHtmlPortalNode, HtmlPortalNode } from 'react-reverse-portal';
|
||||
import { DatePickerWrapper } from '@kbn/ml-date-picker';
|
||||
import { MlPageHeaderRenderer } from '../page_header/page_header';
|
||||
import { useSideNavItems } from './side_nav';
|
||||
import * as routes from '../../routing/routes';
|
||||
import { MlPageWrapper } from '../../routing/ml_page_wrapper';
|
||||
import { useMlKibana, useNavigateToPath } from '../../contexts/kibana';
|
||||
import { MlRoute, PageDependencies } from '../../routing/router';
|
||||
import { DatePickerWrapper } from '../navigation_menu/date_picker_wrapper';
|
||||
import { useActiveRoute } from '../../routing/use_active_route';
|
||||
import { useDocTitle } from '../../routing/use_doc_title';
|
||||
|
||||
|
@ -43,11 +44,28 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
|
|||
const {
|
||||
services: {
|
||||
http: { basePath },
|
||||
mlServices: { httpService },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const headerPortalNode = useMemo(() => createHtmlPortalNode(), []);
|
||||
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(
|
||||
() =>
|
||||
|
@ -61,8 +79,8 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
|
|||
const activeRoute = useActiveRoute(routeList);
|
||||
|
||||
const rightSideItems = useMemo(() => {
|
||||
return [...(activeRoute.enableDatePicker ? [<DatePickerWrapper />] : [])];
|
||||
}, [activeRoute.enableDatePicker]);
|
||||
return [...(activeRoute.enableDatePicker ? [<DatePickerWrapper isLoading={isLoading} />] : [])];
|
||||
}, [activeRoute.enableDatePicker, isLoading]);
|
||||
|
||||
useDocTitle(activeRoute);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ export { useMlKibana } from './kibana_context';
|
|||
export type { NavigateToPath } from './use_navigate_to_path';
|
||||
export { useNavigateToPath } from './use_navigate_to_path';
|
||||
export { useUiSettings } from './use_ui_settings_context';
|
||||
export { useTimefilter } from './use_timefilter';
|
||||
export { useNotifications } from './use_notifications_context';
|
||||
export { useMlLocator, useMlLink } from './use_create_url';
|
||||
export { useMlApiContext } from './use_ml_api_context';
|
||||
|
|
|
@ -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());
|
||||
};
|
|
@ -20,8 +20,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { isFullLicense } from '../license';
|
||||
import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana';
|
||||
import { useMlKibana, useNavigateToPath } from '../contexts/kibana';
|
||||
import { HelpMenu } from '../components/help_menu';
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
|||
GetAdditionalLinksParams,
|
||||
GetAdditionalLinks,
|
||||
} 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 { useMlKibana, useMlLocator } from '../../contexts/kibana';
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ import type {
|
|||
GetAdditionalLinks,
|
||||
GetAdditionalLinksParams,
|
||||
} 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 { ML_PAGES } from '../../../../common/constants/locator';
|
||||
import { isFullLicense } from '../../license';
|
||||
|
|
|
@ -14,6 +14,7 @@ import { switchMap, map } from 'rxjs/operators';
|
|||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import {
|
||||
getDateFormatTz,
|
||||
getSelectionInfluencers,
|
||||
|
@ -28,7 +29,7 @@ import {
|
|||
ExplorerJob,
|
||||
} from '../explorer_utils';
|
||||
import { ExplorerState } from '../reducers';
|
||||
import { useMlKibana, useTimefilter } from '../../contexts/kibana';
|
||||
import { useMlKibana } from '../../contexts/kibana';
|
||||
import { MlResultsService, mlResultsServiceProvider } from '../../services/results_service';
|
||||
import { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service';
|
||||
import type { InfluencersFilterQuery } from '../../../../common/types/es_client';
|
||||
|
|
|
@ -20,13 +20,13 @@ import { i18n } from '@kbn/i18n';
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import { useAnomalyExplorerContext } from './anomaly_explorer_context';
|
||||
import { escapeKueryForFieldValuePair } from '../util/string_utils';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
|
||||
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service';
|
||||
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import {
|
||||
AppStateSelectedCells,
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
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 { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state';
|
||||
import { useMlKibana, useTimefilter } from '../contexts/kibana';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { mlResultsServiceProvider } from '../services/results_service';
|
||||
import { AnomalyTimelineService } from '../services/anomaly_timeline_service';
|
||||
import { useExplorerUrlState } from './hooks/use_explorer_url_state';
|
||||
|
|
|
@ -28,9 +28,9 @@ import useDebounce from 'react-use/lib/useDebounce';
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
|
||||
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..';
|
||||
import {
|
||||
OVERALL_LABEL,
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { isEqual, sortBy, uniq } from 'lodash';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
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 type {
|
||||
AppStateSelectedCells,
|
||||
|
@ -38,8 +40,6 @@ import { mlJobService } from '../services/job_service';
|
|||
import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils';
|
||||
import type { TimeBucketsInterval } from '../util/time_buckets';
|
||||
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 { StateService } from '../services/state_service';
|
||||
import type { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state';
|
||||
|
|
|
@ -9,6 +9,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { addExcludeFrozenToQuery } from '@kbn/ml-query-utils';
|
||||
import { SavedSearchSavedObject } from '../../../../../../common/types/kibana';
|
||||
import { UrlConfig } from '../../../../../../common/types/custom_urls';
|
||||
import { IndexPatternTitle } from '../../../../../../common/types/kibana';
|
||||
|
@ -28,7 +29,6 @@ import {
|
|||
} from '../../../../../../common/types/anomaly_detection_jobs';
|
||||
import { Aggregation, Field, RuntimeMappings } from '../../../../../../common/types/fields';
|
||||
import { combineFieldsAndAggs } from '../../../../../../common/util/fields_utils';
|
||||
import { addExcludeFrozenToQuery } from '../../../../../../common/util/query_utils';
|
||||
import { createEmptyJob, createEmptyDatafeed } from './util/default_configs';
|
||||
import { mlJobService } from '../../../../services/job_service';
|
||||
import { JobRunner, ProgressSubscriber } from '../job_runner';
|
||||
|
|
|
@ -10,19 +10,26 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
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 { StepProps, WIZARD_STEPS } from '../step_types';
|
||||
import { JobCreatorContext } from '../job_creator_context';
|
||||
import { useMlContext } from '../../../../../contexts/ml';
|
||||
import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector';
|
||||
import { EventRateChart } from '../charts/event_rate_chart';
|
||||
import { LineChartPoint } from '../../../common/chart_loader';
|
||||
import { JOB_TYPE } from '../../../../../../../common/constants/new_job';
|
||||
import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service';
|
||||
import { TimeRangePicker, TimeRange } from '../../../common/components';
|
||||
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 }) => {
|
||||
const timefilter = useTimefilter();
|
||||
const { services } = useMlKibana();
|
||||
const mlContext = useMlContext();
|
||||
|
||||
|
@ -36,6 +43,15 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
|
|||
const [eventRateChartData, setEventRateChartData] = useState<LineChartPoint[]>([]);
|
||||
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() {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
|
@ -61,8 +77,8 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
|
|||
max: moment(end),
|
||||
});
|
||||
// update the timefilter, to keep the URL in sync
|
||||
const { timefilter } = services.data.query.timefilter;
|
||||
timefilter.setTime({
|
||||
const { timefilter: timefilterService } = services.data.query.timefilter;
|
||||
timefilterService.setTime({
|
||||
from: moment(start).toISOString(),
|
||||
to: moment(end).toISOString(),
|
||||
});
|
||||
|
@ -89,8 +105,8 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
|
|||
function fullTimeRangeCallback(range: GetTimeFieldRangeResponse) {
|
||||
if (range.start !== null && range.end !== null) {
|
||||
setTimeRange({
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
start: range.start.epoch,
|
||||
end: range.end.epoch,
|
||||
});
|
||||
} else {
|
||||
const { toasts } = services.notifications;
|
||||
|
@ -112,10 +128,13 @@ export const TimeRangeStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep })
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
frozenDataPreference={frozenDataPreference}
|
||||
setFrozenDataPreference={setFrozenDataPreference}
|
||||
dataView={mlContext.currentDataView}
|
||||
query={mlContext.combinedQuery}
|
||||
disabled={false}
|
||||
callback={fullTimeRangeCallback}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { useTimeBuckets } from '../../../../components/custom_hooks/use_time_buckets';
|
||||
import { Wizard } from './wizard';
|
||||
import { WIZARD_STEPS } from '../components/step_types';
|
||||
|
@ -34,7 +35,6 @@ import { ResultsLoader } from '../../common/results_loader';
|
|||
import { JobValidator } from '../../common/job_validator';
|
||||
import { useMlContext } from '../../../../contexts/ml';
|
||||
import { useMlKibana } from '../../../../contexts/kibana';
|
||||
import { getTimeFilterRange } from '../../../../components/full_time_range_selector';
|
||||
import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service';
|
||||
import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service';
|
||||
import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields';
|
||||
|
@ -52,6 +52,7 @@ export interface PageProps {
|
|||
}
|
||||
|
||||
export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
|
||||
const timefilter = useTimefilter();
|
||||
const mlContext = useMlContext();
|
||||
const {
|
||||
services: { maps: mapsPlugin },
|
||||
|
@ -74,7 +75,7 @@ export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
|
|||
|
||||
const { displayErrorToast } = useToastNotificationService();
|
||||
|
||||
const { from, to } = getTimeFilterRange();
|
||||
const { from, to } = getTimeFilterRange(timefilter);
|
||||
jobCreator.setTimeRange(from, to);
|
||||
|
||||
let firstWizardStep =
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
EuiSwitch,
|
||||
EuiTextAlign,
|
||||
} from '@elastic/eui';
|
||||
import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { ModuleJobUI, SAVE_STATE } from '../page';
|
||||
import { getTimeFilterRange } from '../../../../components/full_time_range_selector';
|
||||
import { useMlContext } from '../../../../contexts/ml';
|
||||
import {
|
||||
composeValidators,
|
||||
|
@ -51,7 +51,8 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
saveState,
|
||||
jobs,
|
||||
}) => {
|
||||
const { from, to } = getTimeFilterRange();
|
||||
const timefilter = useTimefilter();
|
||||
const { from, to } = getTimeFilterRange(timefilter);
|
||||
const { currentDataView: dataView } = useMlContext();
|
||||
|
||||
const jobPrefixValidator = useMemo(
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { isEqual, merge } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { addExcludeFrozenToQuery } from '@kbn/ml-query-utils';
|
||||
import { useMlKibana, useMlLocator } from '../../../contexts/kibana';
|
||||
import { useMlContext } from '../../../contexts/ml';
|
||||
import {
|
||||
|
@ -41,7 +42,6 @@ import { ML_PAGES } from '../../../../../common/constants/locator';
|
|||
import { TIME_FORMAT } from '../../../../../common/constants/time_format';
|
||||
import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning';
|
||||
import { RuntimeMappings } from '../../../../../common/types/fields';
|
||||
import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils';
|
||||
import { MlPageHeader } from '../../../components/page_header';
|
||||
|
||||
export interface ModuleJobUI extends ModuleJob {
|
||||
|
|
|
@ -5,19 +5,65 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { type FC } from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
|
||||
import { NotificationsList } from './notifications_list';
|
||||
import { useMlKibana } from '../../contexts/kibana';
|
||||
|
||||
jest.mock('../../contexts/kibana');
|
||||
jest.mock('../../services/toast_notification_service');
|
||||
jest.mock('../../contexts/ml/ml_notifications_context');
|
||||
jest.mock('../../contexts/kibana/use_timefilter');
|
||||
jest.mock('../../contexts/kibana/use_field_formatter');
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
|
@ -29,7 +75,7 @@ describe('NotificationsList', () => {
|
|||
});
|
||||
|
||||
test('starts fetching notification on mount with default params', async () => {
|
||||
const {} = render(<NotificationsList />, { wrapper: I18nProvider });
|
||||
const {} = render(<NotificationsList />, { wrapper: Wrapper });
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
|
|
|
@ -24,11 +24,11 @@ import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
|
|||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import { usePageUrlState } from '@kbn/ml-url-state';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import { EntityFilter } from './entity_filter';
|
||||
import { useMlNotifications } from '../../contexts/ml/ml_notifications_context';
|
||||
import { ML_NOTIFICATIONS_MESSAGE_LEVEL } from '../../../../common/constants/notifications';
|
||||
import { SavedObjectsWarning } from '../../components/saved_objects_warning';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '../../contexts/kibana/use_timefilter';
|
||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
||||
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
|
||||
import { useRefresh } from '../../routing/use_refresh';
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
import React, { FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { NotificationsList } from './components/notifications_list';
|
||||
import { useMlKibana, useTimefilter } from '../contexts/kibana';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
import { NodeAvailableWarning } from '../components/node_available_warning';
|
||||
import { UpgradeWarning } from '../components/upgrade';
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { Group } from './anomaly_detection_panel';
|
||||
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import React, { useEffect, useState, type FC } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { AnomalyDetectionPanel } from './anomaly_detection_panel';
|
||||
import { AnalyticsPanel } from './analytics_panel';
|
||||
import { AnomalyTimelineService } from '../../services/anomaly_timeline_service';
|
||||
import { mlResultsServiceProvider } from '../../services/results_service';
|
||||
import { useMlKibana, useTimefilter } from '../../contexts/kibana';
|
||||
import { useMlKibana } from '../../contexts/kibana';
|
||||
|
||||
interface Props {
|
||||
createAnomalyDetectionJobDisabled: boolean;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import { EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { checkPermission } from '../capabilities/check_capabilities';
|
||||
import { mlNodesAvailable } from '../ml_nodes_check';
|
||||
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 { UpgradeWarning } from '../components/upgrade';
|
||||
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 { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
|
||||
export const OverviewPage: FC = () => {
|
||||
|
|
|
@ -14,7 +14,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
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';
|
||||
|
||||
|
|
|
@ -8,18 +8,20 @@
|
|||
import React, { useEffect, FC, useMemo } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
mlTimefilterRefresh$,
|
||||
useRefreshIntervalUpdates,
|
||||
useTimefilter,
|
||||
} from '@kbn/ml-date-picker';
|
||||
import { NavigateToPath } from '../../contexts/kibana';
|
||||
import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list';
|
||||
import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service';
|
||||
import { MlRoute, PageLoader, PageProps } from '../router';
|
||||
import { useResolver } from '../use_resolver';
|
||||
import { basicResolvers } from '../resolvers';
|
||||
import { JobsPage } from '../../jobs/jobs_list';
|
||||
import { useTimefilter } from '../../contexts/kibana';
|
||||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||
import { AnnotationUpdatesService } from '../../services/annotations_service';
|
||||
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
|
||||
import { useRefreshIntervalUpdates } from '../../contexts/kibana/use_timefilter';
|
||||
|
||||
export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
|
||||
id: 'anomaly_detection',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { FC, Suspense } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { PageLoader, PageProps } from '../router';
|
||||
import { useResolver } from '../use_resolver';
|
||||
import { checkFullLicense } from '../../license';
|
||||
|
@ -15,7 +16,7 @@ import { getMlNodeCount } from '../../ml_nodes_check';
|
|||
import { loadMlServerInfo } from '../../services/ml_server_info';
|
||||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||
import type { MlRoute } from '..';
|
||||
import { NavigateToPath, useTimefilter } from '../../contexts/kibana';
|
||||
import { NavigateToPath } from '../../contexts/kibana';
|
||||
|
||||
const NotificationsPage = React.lazy(() => import('../../notifications/page'));
|
||||
|
||||
|
|
|
@ -10,16 +10,16 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
|
||||
import type { NavigateToPath } from '../../contexts/kibana';
|
||||
|
||||
import { MlRoute, PageLoader, PageProps } from '../router';
|
||||
import { useResolver } from '../use_resolver';
|
||||
|
||||
import { checkFullLicense } from '../../license';
|
||||
import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities';
|
||||
import { getMlNodeCount } from '../../ml_nodes_check';
|
||||
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';
|
||||
|
||||
const OverviewPage = React.lazy(() => import('../../overview/overview_page'));
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import React, { FC } from 'react';
|
||||
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 { useResolver } from '../../use_resolver';
|
||||
import { checkFullLicense } from '../../../license';
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import React, { FC } from 'react';
|
||||
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 { useResolver } from '../../use_resolver';
|
||||
import { checkFullLicense } from '../../../license';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue