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

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

2
.github/CODEOWNERS vendored
View file

@ -1078,9 +1078,11 @@ src/plugins/chart_expressions/common @elastic/kibana-visualizations
x-pack/packages/ml/agg_utils @elastic/ml-ui
x-pack/packages/ml/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

View file

@ -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",

View file

@ -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"],

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,20 +5,24 @@
* 2.0.
*/
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 />);

View file

@ -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;
};

View file

@ -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();
});
});

View file

@ -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',
}

View file

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

View file

@ -6,31 +6,35 @@
*/
import { renderHook } from '@testing-library/react-hooks';
import { 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 })
);

View file

@ -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(() => {

View file

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

View file

@ -5,38 +5,50 @@
* 2.0.
*/
// 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),
});
}

View file

@ -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>();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'],
};

View file

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

View file

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

View file

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

View file

@ -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];
}

View file

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

View file

@ -8,13 +8,9 @@
// TODO Consolidate with duplicate query utils in
// `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;
};

View file

@ -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';

View file

@ -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>

View file

@ -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 {

View file

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

View file

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

View file

@ -6,6 +6,7 @@
*/
import React, { FC } from 'react';
import { 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>
);

View file

@ -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">

View file

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

View file

@ -5,12 +5,18 @@
* 2.0.
*/
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>
);

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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({

View file

@ -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,
});

View file

@ -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;

View file

@ -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/**/*",

View file

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

View file

@ -10,14 +10,11 @@ import { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { MutableRefObject } from 'react';
import { 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) {

View file

@ -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

View file

@ -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);
},
});
};

View file

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

View file

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

View file

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

View file

@ -5,10 +5,13 @@
* 2.0.
*/
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>

View file

@ -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}

View file

@ -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>,

View file

@ -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({

View file

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

View file

@ -5,19 +5,26 @@
* 2.0.
*/
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>

View file

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

View file

@ -5,20 +5,14 @@
* 2.0.
*/
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;

View file

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

View file

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

View file

@ -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 =
| {
/**

View file

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

View file

@ -8,24 +8,28 @@
import React, { FC } from 'react';
import './_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>

View file

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

View file

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

View file

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

View file

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

View file

@ -5,20 +5,21 @@
* 2.0.
*/
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);

View file

@ -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';

View file

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

View file

@ -20,8 +20,9 @@ import {
} from '@elastic/eui';
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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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';

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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 />

View file

@ -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 =

View file

@ -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(

View file

@ -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 {

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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 = () => {

View file

@ -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';

View file

@ -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',

View file

@ -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'));

View file

@ -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'));

View file

@ -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';

View file

@ -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