Add date range picker above kql (#27281)
* Create Date Range picker component * add full functionality for date range picker * clean up * add generic type * remove unused export * review * fix more review * fix type issue * Fix typo * fix refresh on loading more table * Fix loading in load more table * Fix test by adding new state * Move switch theme * Fix rebase issue with date logic
This commit is contained in:
parent
6521214503
commit
bc47a8c828
1
x-pack/plugins/infra/types/eui.d.ts
vendored
1
x-pack/plugins/infra/types/eui.d.ts
vendored
|
@ -164,6 +164,7 @@ declare module '@elastic/eui' {
|
|||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
dateFormat?: string;
|
||||
isCustom?: boolean;
|
||||
};
|
||||
|
||||
export const EuiDatePickerRange: React.SFC<EuiDatePickerRangeProps>;
|
||||
|
|
|
@ -143,7 +143,7 @@ export interface HostsData {
|
|||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
export interface HostsEdges extends Record<string, {}> {
|
||||
export interface HostsEdges {
|
||||
host: HostItem;
|
||||
cursor: CursorType;
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ export interface UncommonProcessesData {
|
|||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
export interface UncommonProcessesEdges extends Record<string, {}> {
|
||||
export interface UncommonProcessesEdges {
|
||||
uncommonProcess: UncommonProcessItem;
|
||||
cursor: CursorType;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,20 @@ describe('Flyout', () => {
|
|||
dragAndDrop: {
|
||||
dataProviders: {},
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: 0,
|
||||
to: 1,
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
timeline: {
|
||||
timelineById: {
|
||||
test: {
|
||||
|
|
|
@ -99,7 +99,7 @@ describe('Load More Table Component', () => {
|
|||
title={<h3>Hosts</h3>}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.setState({ paginationLoading: true });
|
||||
expect(wrapper.find('[data-test-subj="LoadingPanelLoadMoreTable"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper
|
||||
|
|
|
@ -47,6 +47,7 @@ interface BasicTableProps<T> {
|
|||
|
||||
interface BasicTableState {
|
||||
isPopoverOpen: boolean;
|
||||
paginationLoading: boolean;
|
||||
}
|
||||
|
||||
export interface Columns<T> {
|
||||
|
@ -62,8 +63,20 @@ export interface Columns<T> {
|
|||
export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, BasicTableState> {
|
||||
public readonly state = {
|
||||
isPopoverOpen: false,
|
||||
paginationLoading: false,
|
||||
};
|
||||
|
||||
public componentDidUpdate(prevProps: BasicTableProps<T>) {
|
||||
const { paginationLoading } = this.state;
|
||||
const { loading } = this.props;
|
||||
if (paginationLoading && prevProps.loading && !loading) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
paginationLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
columns,
|
||||
|
@ -73,12 +86,12 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
|
|||
loadingTitle,
|
||||
pageOfItems,
|
||||
title,
|
||||
loadMore,
|
||||
limit,
|
||||
updateLimitPagination,
|
||||
} = this.props;
|
||||
const { paginationLoading } = this.state;
|
||||
|
||||
if (loading && isEmpty(pageOfItems)) {
|
||||
if (loading && !paginationLoading) {
|
||||
return (
|
||||
<LoadingPanel
|
||||
height="auto"
|
||||
|
@ -153,7 +166,7 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
|
|||
<EuiButton
|
||||
data-test-subj="loadingMoreButton"
|
||||
isLoading={loading}
|
||||
onClick={loadMore}
|
||||
onClick={this.loadMore}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load More'}
|
||||
</EuiButton>
|
||||
|
@ -167,14 +180,24 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
|
|||
);
|
||||
}
|
||||
|
||||
private loadMore = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
paginationLoading: true,
|
||||
});
|
||||
this.props.loadMore();
|
||||
};
|
||||
|
||||
private onButtonClick = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private closePopover = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,58 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
|
||||
import { first, last, noop } from 'lodash/fp';
|
||||
import moment, { Moment } from 'moment';
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { getDateRange } from '../timeline/body/mini_map/date_ranges';
|
||||
|
||||
// TODO: replace this stub
|
||||
const getDefaultStartDate = () => {
|
||||
const dates: Date[] = getDateRange('day');
|
||||
return moment(first(dates));
|
||||
};
|
||||
|
||||
// TODO: replace this stub
|
||||
const getDefaultEndDate = () => {
|
||||
const dates: Date[] = getDateRange('day');
|
||||
return moment(last(dates));
|
||||
};
|
||||
|
||||
interface DatePickerProps {
|
||||
startDate?: Moment;
|
||||
endDate?: Moment;
|
||||
}
|
||||
|
||||
export const DatePicker = pure<DatePickerProps>(
|
||||
({ startDate = getDefaultStartDate(), endDate = getDefaultEndDate() }) => (
|
||||
<>
|
||||
<EuiDatePickerRange
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
selected={startDate}
|
||||
onChange={noop}
|
||||
isInvalid={false}
|
||||
aria-label="Start date"
|
||||
showTimeSelect
|
||||
popperPlacement="top-end"
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
selected={endDate}
|
||||
onChange={noop}
|
||||
isInvalid={false}
|
||||
aria-label="End date"
|
||||
showTimeSelect
|
||||
popperPlacement="top-end"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
|
@ -5,6 +5,9 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
// @ts-ignore
|
||||
EuiHeaderLogo,
|
||||
EuiHealth,
|
||||
|
@ -14,9 +17,28 @@ import { pure } from 'recompose';
|
|||
|
||||
import { FooterContainer } from '.';
|
||||
import { WhoAmI } from '../../containers/who_am_i';
|
||||
import { ThemeSwitcher } from '../theme_switcher';
|
||||
|
||||
export const Footer = pure(() => (
|
||||
<FooterContainer data-test-subj="footer">
|
||||
<WhoAmI sourceId="default">{() => <EuiHealth color="success">Live data</EuiHealth>}</WhoAmI>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<WhoAmI sourceId="default">
|
||||
{({ appName }) => <EuiHealth color="success">Live {appName} data</EuiHealth>}
|
||||
</WhoAmI>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<ThemeSwitcher />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton href="kibana#home/tutorial_directory/security" target="_blank">
|
||||
Add data
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FooterContainer>
|
||||
));
|
||||
|
|
|
@ -28,6 +28,20 @@ describe('Load More Table Component', () => {
|
|||
hosts: {
|
||||
limit: 2,
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: 0,
|
||||
to: 1,
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
dragAndDrop: {
|
||||
dataProviders: {},
|
||||
},
|
||||
|
|
|
@ -27,6 +27,20 @@ describe('UncommonProcess Table Component', () => {
|
|||
hosts: {
|
||||
limit: 2,
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: 0,
|
||||
to: 1,
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
dragAndDrop: {
|
||||
dataProviders: {},
|
||||
},
|
||||
|
|
|
@ -47,19 +47,6 @@ export const FooterContainer = styled.div`
|
|||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SubHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SubHeaderDatePicker = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 5px 0 5px 0;
|
||||
`;
|
||||
|
||||
export const PaneScrollContainer = styled.div`
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { inputsModel } from '../../store';
|
||||
|
||||
interface OwnProps {
|
||||
id: string;
|
||||
loading: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
setQuery: (params: { id: string; loading: boolean; refetch: inputsModel.Refetch }) => void;
|
||||
}
|
||||
|
||||
export function manageQuery<T>(WrappedComponent: React.ComponentClass<T> | React.ComponentType<T>) {
|
||||
class ManageQuery extends React.PureComponent<OwnProps & T> {
|
||||
public componentDidUpdate(prevProps: OwnProps) {
|
||||
const { loading, id, refetch, setQuery } = this.props;
|
||||
if (prevProps.loading !== loading) {
|
||||
setQuery({ id, loading, refetch });
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const otherProps = omit(['id', 'refetch', 'setQuery'], this.props);
|
||||
return <WrappedComponent {...otherProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
return ManageQuery;
|
||||
}
|
|
@ -4,18 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
EuiButton,
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore
|
||||
EuiTabs,
|
||||
} from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getHostsUrl, getNetworkUrl, getOverviewUrl } from '../../link_to';
|
||||
import { ThemeSwitcher } from '../../theme_switcher';
|
||||
|
||||
interface NavTab {
|
||||
id: string;
|
||||
|
@ -49,30 +46,6 @@ interface NavigationState {
|
|||
selectedTabId: string;
|
||||
}
|
||||
|
||||
const AddSources = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
top: -48px;
|
||||
`;
|
||||
|
||||
const AddData = pure(() => (
|
||||
<AddSources data-test-subj="add-sources">
|
||||
<EuiButton href="kibana#home/tutorial_directory/security" target="_blank">
|
||||
Add data
|
||||
</EuiButton>
|
||||
<ThemeSwitcher />
|
||||
</AddSources>
|
||||
));
|
||||
|
||||
const AddDataContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 0px;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const NavigationContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -93,9 +66,6 @@ export class Navigation extends React.PureComponent<{}, NavigationState> {
|
|||
return (
|
||||
<NavigationContainer>
|
||||
<EuiTabs>{this.renderTabs()}</EuiTabs>
|
||||
<AddDataContainer>
|
||||
<AddData />
|
||||
</AddDataContainer>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDatePicker, EuiFieldText, EuiFormRow, EuiPopover } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
type Position = 'start' | 'end';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
position: Position;
|
||||
isInvalid: boolean;
|
||||
needsUpdating?: boolean;
|
||||
date: Moment;
|
||||
buttonProps?: string;
|
||||
buttonOnly?: boolean;
|
||||
onChange: (date: moment.Moment | null) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
export class GlobalDateButton extends React.PureComponent<Props, State> {
|
||||
public readonly state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
id,
|
||||
position,
|
||||
isInvalid,
|
||||
needsUpdating = false,
|
||||
date,
|
||||
buttonProps,
|
||||
buttonOnly,
|
||||
onChange,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const { isPopoverOpen } = this.state;
|
||||
|
||||
const classes = classNames([
|
||||
'euiGlobalDatePicker__dateButton',
|
||||
`euiGlobalDatePicker__dateButton--${position}`,
|
||||
{
|
||||
'euiGlobalDatePicker__dateButton-isSelected': isPopoverOpen,
|
||||
'euiGlobalDatePicker__dateButton-isInvalid': isInvalid,
|
||||
'euiGlobalDatePicker__dateButton-needsUpdating': needsUpdating,
|
||||
},
|
||||
]);
|
||||
|
||||
let title = date.format('L LTS');
|
||||
if (isInvalid) {
|
||||
title = `Invalid date: ${title}`;
|
||||
} else if (needsUpdating) {
|
||||
title = `Update needed: ${title}`;
|
||||
}
|
||||
|
||||
const button = (
|
||||
<button
|
||||
onClick={buttonOnly ? undefined : this.togglePopover}
|
||||
className={classes}
|
||||
title={title}
|
||||
{...buttonProps}
|
||||
>
|
||||
{date.format('L LTS')}
|
||||
</button>
|
||||
);
|
||||
|
||||
return buttonOnly ? (
|
||||
button
|
||||
) : (
|
||||
<EuiPopover
|
||||
id={`${id}-popover`}
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
ownFocus
|
||||
{...rest}
|
||||
>
|
||||
<div style={{ width: 390, padding: 0 }}>
|
||||
<EuiDatePicker
|
||||
selected={date}
|
||||
dateFormat="L LTS"
|
||||
inline
|
||||
fullWidth
|
||||
showTimeSelect
|
||||
shadow={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<EuiFormRow style={{ padding: '0 8px 8px' }}>
|
||||
<EuiFieldText />
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
private togglePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDatePickerRange, EuiFlexGroup, EuiFlexItem, EuiFormControlLayout } from '@elastic/eui';
|
||||
import { get, getOr, has, isEqual } from 'lodash/fp';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { inputsActions, inputsModel, State } from '../../store';
|
||||
import { GlobalDateButton } from './global_date_button';
|
||||
import { QuickSelectPopover } from './quick_select_popover';
|
||||
import { UpdateButton } from './update_button';
|
||||
|
||||
export type DateType = 'relative' | 'absolute';
|
||||
|
||||
interface RangeDatePickerStateRedux {
|
||||
from: number;
|
||||
to: number;
|
||||
isTimerOn: boolean;
|
||||
duration: number;
|
||||
loading: boolean;
|
||||
refetch: inputsModel.Refetch[];
|
||||
}
|
||||
|
||||
interface RangeDatePickerDispatchProps {
|
||||
setAbsoluteRangeDatePicker: (params: { id: string; from: number; to: number }) => void;
|
||||
setRelativeRangeDatePicker: (
|
||||
params: { id: string; option: string; from: number; to: number }
|
||||
) => void;
|
||||
startAutoReload: (params: { id: string }) => void;
|
||||
stopAutoReload: (params: { id: string }) => void;
|
||||
setDuration: (params: { id: string; duration: number }) => void;
|
||||
}
|
||||
interface OwnProps {
|
||||
id: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type RangeDatePickerProps = OwnProps & RangeDatePickerDispatchProps & RangeDatePickerStateRedux;
|
||||
|
||||
interface RecentylUsedBasic {
|
||||
kind: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface RecentylUsedDateRange {
|
||||
kind: string;
|
||||
timerange: number[];
|
||||
}
|
||||
|
||||
export type RecentlyUsedI = RecentylUsedBasic | RecentylUsedDateRange;
|
||||
|
||||
interface RangeDatePickerState {
|
||||
recentlyUsed: RecentlyUsedI[];
|
||||
}
|
||||
|
||||
class RangeDatePickerComponents extends React.PureComponent<
|
||||
RangeDatePickerProps,
|
||||
RangeDatePickerState
|
||||
> {
|
||||
public readonly state = {
|
||||
recentlyUsed: [] as RecentlyUsedI[],
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { recentlyUsed } = this.state;
|
||||
const { id, loading, disabled = false, from, to, isTimerOn, refetch } = this.props;
|
||||
|
||||
const quickSelectPopover = (
|
||||
<QuickSelectPopover
|
||||
disabled={disabled}
|
||||
recentlyUsed={recentlyUsed}
|
||||
isTimerOn={isTimerOn}
|
||||
onChange={this.onChange}
|
||||
updateAutoReload={this.updateTimer}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem style={{ maxWidth: 480 }}>
|
||||
<EuiFormControlLayout className="euiGlobalDatePicker" prepend={quickSelectPopover}>
|
||||
<EuiDatePickerRange
|
||||
className="euiDatePickerRange--inGroup"
|
||||
iconType={false}
|
||||
isCustom
|
||||
startDateControl={
|
||||
<GlobalDateButton
|
||||
id={`${id}-from`}
|
||||
date={moment(from)}
|
||||
position="start"
|
||||
onChange={this.handleChangeFrom}
|
||||
isInvalid={from && to ? from > to : false}
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<GlobalDateButton
|
||||
id={`${id}-to`}
|
||||
date={moment(to)}
|
||||
position="end"
|
||||
onChange={this.handleChangeTo}
|
||||
isInvalid={from && to ? from > to : false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UpdateButton loading={loading} refetch={refetch} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChangeFrom = (date: moment.Moment | null) => {
|
||||
const { id, to } = this.props;
|
||||
if (date && moment(this.props.from) !== date) {
|
||||
this.props.setAbsoluteRangeDatePicker({ id, from: date.valueOf(), to });
|
||||
this.updateRecentlyUsed({
|
||||
kind: 'date-range',
|
||||
timerange: [date.valueOf(), to],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleChangeTo = (date: moment.Moment | null) => {
|
||||
const { id, from } = this.props;
|
||||
if (date && moment(this.props.to) !== date) {
|
||||
this.props.setAbsoluteRangeDatePicker({ id, from, to: date.valueOf() });
|
||||
this.updateRecentlyUsed({
|
||||
kind: 'date-range',
|
||||
timerange: [from, date.valueOf()],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private updateRecentlyUsed = (msg?: RecentlyUsedI) => {
|
||||
const { recentlyUsed } = this.state;
|
||||
if (
|
||||
msg &&
|
||||
recentlyUsed.filter((i: RecentlyUsedI) => {
|
||||
const timerange = getOr(false, 'timerange', msg);
|
||||
const text = getOr(false, 'text', msg);
|
||||
if (timerange && has('timerange', i)) {
|
||||
return isEqual(timerange, get('timerange', i));
|
||||
} else if (text && has('text', i)) {
|
||||
return text === get('text', i);
|
||||
}
|
||||
return false;
|
||||
}).length === 0
|
||||
) {
|
||||
recentlyUsed.unshift(msg);
|
||||
this.setState({
|
||||
...this.state,
|
||||
recentlyUsed: recentlyUsed.slice(0, 5),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onChange = (from: Moment, to: Moment, type: DateType, msg?: RecentlyUsedI) => {
|
||||
const { id } = this.props;
|
||||
if (type === 'absolute') {
|
||||
this.props.setAbsoluteRangeDatePicker({
|
||||
id,
|
||||
from: from.valueOf(),
|
||||
to: to.valueOf(),
|
||||
});
|
||||
} else if (type === 'relative') {
|
||||
this.props.setRelativeRangeDatePicker({
|
||||
id,
|
||||
option: msg ? msg.kind : '',
|
||||
from: from.valueOf(),
|
||||
to: to.valueOf(),
|
||||
});
|
||||
}
|
||||
this.updateRecentlyUsed(msg);
|
||||
};
|
||||
|
||||
private updateTimer = (isTimerOn: boolean, duration: number, durationKind: string) => {
|
||||
const { id } = this.props;
|
||||
this.props.setDuration({
|
||||
id,
|
||||
duration: moment
|
||||
.duration(duration, durationKind as moment.unitOfTime.DurationConstructor)
|
||||
.asMilliseconds(),
|
||||
});
|
||||
if (isTimerOn) {
|
||||
this.props.startAutoReload({ id });
|
||||
} else {
|
||||
this.props.stopAutoReload({ id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, { id }: OwnProps) => {
|
||||
const myState = getOr({}, `local.inputs.${id}`, state);
|
||||
return {
|
||||
from: get('timerange.from', myState),
|
||||
to: get('timerange.to', myState),
|
||||
isTimerOn: get('policy.kind', myState) === 'interval',
|
||||
duration: get('policy.duration', myState),
|
||||
loading: myState.query.filter((i: inputsModel.GlobalQuery) => i.loading === true).length > 0,
|
||||
refetch: myState.query.map((i: inputsModel.GlobalQuery) => i.refetch),
|
||||
};
|
||||
};
|
||||
|
||||
export const RangeDatePicker = connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
setAbsoluteRangeDatePicker: inputsActions.setAbsoluteRangeDatePicker,
|
||||
setRelativeRangeDatePicker: inputsActions.setRelativeRangeDatePicker,
|
||||
startAutoReload: inputsActions.startAutoReload,
|
||||
stopAutoReload: inputsActions.stopAutoReload,
|
||||
setDuration: inputsActions.setDuration,
|
||||
}
|
||||
)(RangeDatePickerComponents);
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { DateType, RecentlyUsedI } from '..';
|
||||
|
||||
export enum DatePickerOptions {
|
||||
today = 'today',
|
||||
yesterday = 'yesterday',
|
||||
thisWeek = 'this-week',
|
||||
weekToDate = 'week-to-date',
|
||||
thisMonth = 'this-month',
|
||||
monthToDate = 'month-to-date',
|
||||
thisYear = 'this-year',
|
||||
yearToDate = 'year-to-date',
|
||||
}
|
||||
|
||||
const commonDates: Array<{ id: DatePickerOptions; label: string }> = [
|
||||
{
|
||||
id: DatePickerOptions.today,
|
||||
label: 'Today',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.yesterday,
|
||||
label: 'Yesterday',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.thisWeek,
|
||||
label: 'This week',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.weekToDate,
|
||||
label: 'Week to date',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.thisMonth,
|
||||
label: 'This month',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.monthToDate,
|
||||
label: 'Month to date',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.thisYear,
|
||||
label: 'This year',
|
||||
},
|
||||
{
|
||||
id: DatePickerOptions.yearToDate,
|
||||
label: 'Year to date',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
setRangeDatePicker: (from: Moment, to: Moment, type: DateType, msg: RecentlyUsedI) => void;
|
||||
}
|
||||
|
||||
export const CommonlyUsed = pure<Props>(({ setRangeDatePicker }) => {
|
||||
const links = commonDates.map(date => {
|
||||
return (
|
||||
<EuiFlexItem key={date.id}>
|
||||
<EuiLink onClick={() => updateRangeDatePickerByCommonUsed(date.id, setRangeDatePicker)}>
|
||||
{date.label}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>Commonly used</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" className="euiGlobalDatePicker__popoverSection">
|
||||
<EuiFlexGrid gutterSize="s" columns={2}>
|
||||
{links}
|
||||
</EuiFlexGrid>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const updateRangeDatePickerByCommonUsed = (
|
||||
option: DatePickerOptions,
|
||||
setRangeDatePicker: (from: Moment, to: Moment, kind: DateType, msg: RecentlyUsedI) => void
|
||||
) => {
|
||||
let from = null;
|
||||
let to = null;
|
||||
let kind: DateType = 'absolute';
|
||||
if (option === DatePickerOptions.today) {
|
||||
from = moment().startOf('day');
|
||||
to = moment()
|
||||
.startOf('day')
|
||||
.add(24, 'hour');
|
||||
kind = 'absolute';
|
||||
} else if (option === DatePickerOptions.yesterday) {
|
||||
from = moment()
|
||||
.subtract(1, 'day')
|
||||
.startOf('day');
|
||||
to = moment()
|
||||
.subtract(1, 'day')
|
||||
.startOf('day')
|
||||
.add(24, 'hour');
|
||||
kind = 'absolute';
|
||||
} else if (option === DatePickerOptions.thisWeek) {
|
||||
from = moment().startOf('week');
|
||||
to = moment()
|
||||
.startOf('week')
|
||||
.add(1, 'week');
|
||||
kind = 'absolute';
|
||||
} else if (option === DatePickerOptions.weekToDate) {
|
||||
from = moment().startOf('week');
|
||||
to = moment();
|
||||
kind = 'relative';
|
||||
} else if (option === DatePickerOptions.thisMonth) {
|
||||
from = moment().startOf('month');
|
||||
to = moment()
|
||||
.startOf('month')
|
||||
.add(1, 'month');
|
||||
kind = 'absolute';
|
||||
} else if (option === DatePickerOptions.monthToDate) {
|
||||
from = moment().startOf('month');
|
||||
to = moment();
|
||||
kind = 'relative';
|
||||
} else if (option === DatePickerOptions.thisYear) {
|
||||
from = moment().startOf('year');
|
||||
to = moment()
|
||||
.startOf('year')
|
||||
.add(1, 'year');
|
||||
kind = 'absolute';
|
||||
} else if (option === DatePickerOptions.yearToDate) {
|
||||
from = moment().startOf('year');
|
||||
to = moment();
|
||||
kind = 'relative';
|
||||
}
|
||||
if (from && to) {
|
||||
const text = getOr('', 'label', commonDates.filter(i => i.id === option)[0]);
|
||||
setRangeDatePicker(from, to, kind, {
|
||||
kind: option,
|
||||
text,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiPopover,
|
||||
EuiPopoverProps,
|
||||
} from '@elastic/eui';
|
||||
import { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { DateType, RecentlyUsedI } from '../index';
|
||||
import { CommonlyUsed } from './commonly_used';
|
||||
import { QuickSelect } from './quick_select';
|
||||
import { MyRecentlyUsed } from './recently_used';
|
||||
import { Timer } from './timer';
|
||||
|
||||
type MyEuiPopoverProps = Pick<
|
||||
EuiPopoverProps,
|
||||
'id' | 'closePopover' | 'button' | 'isOpen' | 'anchorPosition'
|
||||
> & {
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
const MyEuiPopover: React.SFC<MyEuiPopoverProps> = EuiPopover;
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
recentlyUsed: RecentlyUsedI[];
|
||||
isTimerOn: boolean;
|
||||
onChange: (from: Moment, to: Moment, type: DateType, msg?: RecentlyUsedI) => void;
|
||||
updateAutoReload: (isTimerOn: boolean, interval: number, intervalType: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
quickSelectTime: number;
|
||||
quickSelectUnit: string;
|
||||
duration: number;
|
||||
durationKind: string;
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
export class QuickSelectPopover extends React.PureComponent<Props, State> {
|
||||
public readonly state = {
|
||||
isPopoverOpen: false,
|
||||
quickSelectTime: 1,
|
||||
quickSelectUnit: 'hours',
|
||||
duration: 5,
|
||||
durationKind: 'minutes',
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { quickSelectTime, quickSelectUnit, duration, durationKind } = this.state;
|
||||
const { disabled, isTimerOn, recentlyUsed, updateAutoReload } = this.props;
|
||||
const quickSelectButton = (
|
||||
<EuiButtonEmpty
|
||||
className="euiFormControlLayout__prepend euiGlobalDatePicker__quickSelectButton"
|
||||
textProps={{ className: 'euiGlobalDatePicker__quickSelectButtonText' }}
|
||||
onClick={this.togglePopover}
|
||||
disabled={disabled}
|
||||
aria-label="Date quick select"
|
||||
size="xs"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
>
|
||||
<EuiIcon type="calendar" />
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<MyEuiPopover
|
||||
id="QuickSelectPopover"
|
||||
button={quickSelectButton}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover.bind(this)}
|
||||
anchorPosition="downLeft"
|
||||
zIndex={1001}
|
||||
>
|
||||
<div style={{ width: '400px' }}>
|
||||
<QuickSelect
|
||||
quickSelectTime={quickSelectTime}
|
||||
quickSelectUnit={quickSelectUnit}
|
||||
onChange={this.updateState}
|
||||
setRangeDatePicker={this.onChange}
|
||||
/>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<CommonlyUsed setRangeDatePicker={this.onChange} />
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<MyRecentlyUsed recentlyUsed={recentlyUsed} setRangeDatePicker={this.onChange} />
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<Timer
|
||||
duration={duration}
|
||||
durationKind={durationKind}
|
||||
timerIsOn={isTimerOn}
|
||||
onChange={this.updateState}
|
||||
toggleTimer={isOn => updateAutoReload(isOn, duration, durationKind)}
|
||||
/>
|
||||
</div>
|
||||
</MyEuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
private updateState = (
|
||||
stateType: string,
|
||||
args: React.FormEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
let value: string | number = args!.currentTarget.value;
|
||||
|
||||
if ((stateType === 'quickSelectTime' || stateType === 'duration') && value !== '') {
|
||||
value = parseInt(args!.currentTarget.value, 10);
|
||||
}
|
||||
this.setState({
|
||||
...this.state,
|
||||
[stateType]: value,
|
||||
});
|
||||
};
|
||||
|
||||
private onChange = (from: Moment, to: Moment, type: DateType, msg?: RecentlyUsedI) => {
|
||||
this.setState(
|
||||
{
|
||||
...this.state,
|
||||
isPopoverOpen: false,
|
||||
},
|
||||
() => {
|
||||
this.props.onChange(from, to, type, msg);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private closePopover = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
private togglePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { DateType, RecentlyUsedI } from '..';
|
||||
|
||||
interface Options {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const singleLastOptions: Options[] = [
|
||||
{
|
||||
value: 'seconds',
|
||||
text: 'second',
|
||||
},
|
||||
{
|
||||
value: 'minutes',
|
||||
text: 'minute',
|
||||
},
|
||||
{
|
||||
value: 'hours',
|
||||
text: 'hour',
|
||||
},
|
||||
{
|
||||
value: 'days',
|
||||
text: 'day',
|
||||
},
|
||||
{
|
||||
value: 'weeks',
|
||||
text: 'week',
|
||||
},
|
||||
{
|
||||
value: 'months',
|
||||
text: 'month',
|
||||
},
|
||||
{
|
||||
value: 'years',
|
||||
text: 'year',
|
||||
},
|
||||
];
|
||||
|
||||
export const pluralLastOptions: Options[] = [
|
||||
{
|
||||
value: 'seconds',
|
||||
text: 'seconds',
|
||||
},
|
||||
{
|
||||
value: 'minutes',
|
||||
text: 'minutes',
|
||||
},
|
||||
{
|
||||
value: 'hours',
|
||||
text: 'hours',
|
||||
},
|
||||
{
|
||||
value: 'days',
|
||||
text: 'days',
|
||||
},
|
||||
{
|
||||
value: 'weeks',
|
||||
text: 'weeks',
|
||||
},
|
||||
{
|
||||
value: 'months',
|
||||
text: 'months',
|
||||
},
|
||||
{
|
||||
value: 'years',
|
||||
text: 'years',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
quickSelectTime: number;
|
||||
quickSelectUnit: string;
|
||||
onChange: (
|
||||
stateType: string,
|
||||
args: React.FormEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>
|
||||
) => void;
|
||||
setRangeDatePicker: (from: Moment, to: Moment, type: DateType, msg?: RecentlyUsedI) => void;
|
||||
}
|
||||
|
||||
export const QuickSelect = pure<Props>(
|
||||
({ setRangeDatePicker, quickSelectTime, quickSelectUnit, onChange }) => (
|
||||
<>
|
||||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>Quick select</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<span>Last</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiFieldNumber
|
||||
aria-label="Count of"
|
||||
value={quickSelectTime}
|
||||
step={0}
|
||||
onChange={arg => {
|
||||
onChange('quickSelectTime', arg);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
value={quickSelectUnit}
|
||||
options={quickSelectTime === 1 ? singleLastOptions : pluralLastOptions}
|
||||
onChange={arg => {
|
||||
onChange('quickSelectUnit', arg);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow>
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
updateRangeDatePickerByQuickSelect(
|
||||
quickSelectTime,
|
||||
quickSelectUnit,
|
||||
setRangeDatePicker
|
||||
)
|
||||
}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
Apply
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
export const updateRangeDatePickerByQuickSelect = (
|
||||
quickSelectTime: number,
|
||||
quickSelectUnit: string,
|
||||
setRangeDatePicker: (from: Moment, to: Moment, type: DateType, msg?: RecentlyUsedI) => void
|
||||
) => {
|
||||
const quickSelectUnitStr =
|
||||
quickSelectTime === 1
|
||||
? getOr('', 'text', singleLastOptions.filter(i => i.value === quickSelectUnit)[0])
|
||||
: getOr('', 'text', pluralLastOptions.filter(i => i.value === quickSelectUnit)[0]);
|
||||
const from = moment().subtract(
|
||||
quickSelectTime,
|
||||
quickSelectUnit as moment.unitOfTime.DurationConstructor
|
||||
);
|
||||
const to = moment();
|
||||
setRangeDatePicker(from, to, 'relative', {
|
||||
kind: 'quick-select',
|
||||
text: `Last ${quickSelectTime} ${quickSelectUnitStr}`,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { DateType, RecentlyUsedI } from '../index';
|
||||
import { DatePickerOptions, updateRangeDatePickerByCommonUsed } from './commonly_used';
|
||||
import { updateRangeDatePickerByQuickSelect } from './quick_select';
|
||||
|
||||
interface Props {
|
||||
recentlyUsed: RecentlyUsedI[];
|
||||
setRangeDatePicker: (from: Moment, to: Moment, kind: DateType) => void;
|
||||
}
|
||||
|
||||
export const MyRecentlyUsed = pure<Props>(({ setRangeDatePicker, recentlyUsed }) => {
|
||||
const links = recentlyUsed.map((date: RecentlyUsedI) => {
|
||||
let dateRange;
|
||||
let dateLink = null;
|
||||
const text = getOr(false, 'text', date);
|
||||
const timerange = getOr(false, 'timerange', date);
|
||||
if (text) {
|
||||
dateLink = (
|
||||
<EuiLink onClick={() => updateRangeDatePicker(date.kind, setRangeDatePicker, text)}>
|
||||
{text}
|
||||
</EuiLink>
|
||||
);
|
||||
} else if (timerange) {
|
||||
dateRange = `${moment(timerange[0]).format('L LTS')} – ${moment(timerange[1]).format(
|
||||
'L LTS'
|
||||
)}`;
|
||||
dateLink = (
|
||||
<EuiLink onClick={() => setRangeDatePicker(timerange[0], timerange[1], 'absolute')}>
|
||||
{dateRange}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`${dateRange || date.kind}`}>
|
||||
{dateLink}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>Recently used date ranges</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" className="euiGlobalDatePicker__popoverSection">
|
||||
<EuiFlexGroup gutterSize="s" style={{ flexDirection: 'column' }}>
|
||||
{links}
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const updateRangeDatePicker = (
|
||||
option: string,
|
||||
setRangeDatePicker: (from: Moment, to: Moment, kind: DateType, msg?: RecentlyUsedI) => void,
|
||||
text?: string
|
||||
) => {
|
||||
if (option === 'quick-select') {
|
||||
const options = text!.split(' ');
|
||||
updateRangeDatePickerByQuickSelect(parseInt(options[1], 10), options[2], setRangeDatePicker);
|
||||
} else {
|
||||
updateRangeDatePickerByCommonUsed(option as DatePickerOptions, setRangeDatePicker);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
interface Options {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const singleLastOptions: Options[] = [
|
||||
{
|
||||
value: 'minutes',
|
||||
text: 'minute',
|
||||
},
|
||||
{
|
||||
value: 'hours',
|
||||
text: 'hour',
|
||||
},
|
||||
];
|
||||
|
||||
export const pluralLastOptions: Options[] = [
|
||||
{
|
||||
value: 'minutes',
|
||||
text: 'minutes',
|
||||
},
|
||||
{
|
||||
value: 'hours',
|
||||
text: 'hours',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
timerIsOn: boolean;
|
||||
duration: number;
|
||||
durationKind: string;
|
||||
onChange: (
|
||||
stateType: string,
|
||||
args: React.FormEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>
|
||||
) => void;
|
||||
toggleTimer: (timerIsOn: boolean) => void;
|
||||
}
|
||||
|
||||
export const Timer = pure<Props>(({ onChange, timerIsOn, duration, durationKind, toggleTimer }) => (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>Refresh every</span>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiFieldNumber
|
||||
aria-label="Count of"
|
||||
value={duration}
|
||||
step={0}
|
||||
onChange={arg => {
|
||||
onChange('duration', arg);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
aria-label="Count Type"
|
||||
value={durationKind}
|
||||
options={duration === 1 ? singleLastOptions : pluralLastOptions}
|
||||
onChange={arg => {
|
||||
onChange('durationKind', arg);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow>
|
||||
<EuiButton
|
||||
iconType={timerIsOn ? 'stop' : 'play'}
|
||||
onClick={() => toggleTimer(!timerIsOn)}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
{timerIsOn ? 'Stop' : 'Start'}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
));
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { inputsModel } from '../../store';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
refetch: inputsModel.Refetch[];
|
||||
}
|
||||
|
||||
export const UpdateButton = pure<Props>(({ loading, refetch }) => {
|
||||
const color = loading ? 'secondary' : 'primary';
|
||||
const icon = 'refresh';
|
||||
let text = 'Refresh';
|
||||
|
||||
if (loading) {
|
||||
text = 'Updating';
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
isLoading={loading}
|
||||
className="euiGlobalDatePicker__updateButton"
|
||||
color={color}
|
||||
fill
|
||||
iconType={icon}
|
||||
onClick={() => refetchQuery(refetch)}
|
||||
textProps={{ className: 'euiGlobalDatePicker__updateButtonText' }}
|
||||
>
|
||||
{text}
|
||||
</EuiButton>
|
||||
);
|
||||
});
|
||||
|
||||
const refetchQuery = (query: inputsModel.Refetch[]) => {
|
||||
query.forEach((refetch: inputsModel.Refetch) => refetch());
|
||||
};
|
|
@ -23,6 +23,20 @@ describe('Header', () => {
|
|||
notesById: {},
|
||||
theme: 'dark',
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: 0,
|
||||
to: 1,
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
limit: 2,
|
||||
},
|
||||
|
|
|
@ -52,6 +52,20 @@ describe('Timeline', () => {
|
|||
hosts: {
|
||||
limit: 2,
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: 0,
|
||||
to: 1,
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
dragAndDrop: {
|
||||
dataProviders: {},
|
||||
},
|
||||
|
|
|
@ -10,16 +10,19 @@ import { Query } from 'react-apollo';
|
|||
import { pure } from 'recompose';
|
||||
|
||||
import { EventItem, GetEventsQuery, KpiItem } from '../../../common/graphql/types';
|
||||
|
||||
import { inputsModel } from '../../store';
|
||||
import { eventsQuery } from './index.gql_query';
|
||||
|
||||
export interface EventsArgs {
|
||||
id: string;
|
||||
events?: EventItem[];
|
||||
kpiEventType?: KpiItem[];
|
||||
loading: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export interface EventsProps {
|
||||
id?: string;
|
||||
children?: (args: EventsArgs) => React.ReactNode;
|
||||
sourceId: string;
|
||||
startDate: number;
|
||||
|
@ -28,7 +31,7 @@ export interface EventsProps {
|
|||
}
|
||||
|
||||
export const EventsQuery = pure<EventsProps>(
|
||||
({ children, filterQuery, sourceId, startDate, endDate }) => (
|
||||
({ id = 'eventsQuery', children, filterQuery, sourceId, startDate, endDate }) => (
|
||||
<Query<GetEventsQuery.Query, GetEventsQuery.Variables>
|
||||
query={eventsQuery}
|
||||
fetchPolicy="cache-and-network"
|
||||
|
@ -38,18 +41,22 @@ export const EventsQuery = pure<EventsProps>(
|
|||
sourceId,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: endDate,
|
||||
to: startDate,
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({ data, loading }) =>
|
||||
children!({
|
||||
{({ data, loading, refetch }) => {
|
||||
const events = getOr([], 'source.getEvents.events', data);
|
||||
const kpiEventType = getOr([], 'source.getEvents.kpiEventType', data);
|
||||
return children!({
|
||||
id,
|
||||
refetch,
|
||||
loading,
|
||||
events: getOr([], 'source.getEvents.events', data),
|
||||
kpiEventType: getOr([], 'source.getEvents.kpiEventType', data),
|
||||
})
|
||||
}
|
||||
events,
|
||||
kpiEventType,
|
||||
});
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { pure } from 'recompose';
|
||||
import {
|
||||
globalPolicySelector,
|
||||
globalTimeRangeSelector,
|
||||
inputsActions,
|
||||
inputsModel,
|
||||
State,
|
||||
} from '../../store';
|
||||
|
||||
interface GlobalTimeArgs {
|
||||
poll: number;
|
||||
from: number;
|
||||
to: number;
|
||||
setQuery: (params: { id: string; loading: boolean; refetch: inputsModel.Refetch }) => void;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
children: (args: GlobalTimeArgs) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface GlobalTimeDispatch {
|
||||
setQuery: (params: { id: string; loading: boolean; refetch: inputsModel.Refetch }) => void;
|
||||
}
|
||||
|
||||
interface GlobalTimeReduxState {
|
||||
from: number;
|
||||
to: number;
|
||||
poll: number;
|
||||
}
|
||||
|
||||
type GlobalTimeProps = OwnProps & GlobalTimeReduxState & GlobalTimeDispatch;
|
||||
|
||||
const GlobalTimeComponent = pure<GlobalTimeProps>(({ children, poll, from, to, setQuery }) => (
|
||||
<>
|
||||
{children({
|
||||
poll,
|
||||
from,
|
||||
to,
|
||||
setQuery,
|
||||
})}
|
||||
</>
|
||||
));
|
||||
|
||||
const mapStateToProps = (state: State) => {
|
||||
const timerange: inputsModel.TimeRange = globalTimeRangeSelector(state);
|
||||
const policy: inputsModel.Policy = globalPolicySelector(state);
|
||||
return {
|
||||
poll: policy.kind === 'interval' && timerange.kind === 'absolute' ? policy.duration : 0,
|
||||
from: timerange.from,
|
||||
to: timerange.to,
|
||||
};
|
||||
};
|
||||
|
||||
export const GlobalTime = connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
setQuery: inputsActions.setQuery,
|
||||
}
|
||||
)(GlobalTimeComponent);
|
|
@ -12,24 +12,27 @@ import { pure } from 'recompose';
|
|||
import { GetHostsQuery, HostsEdges, PageInfo } from '../../../common/graphql/types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { hostsSelector, State } from '../../store';
|
||||
import { hostsSelector, inputsModel, State } from '../../store';
|
||||
import { hostsQuery } from './index.gql_query';
|
||||
|
||||
export interface HostsArgs {
|
||||
id: string;
|
||||
hosts: HostsEdges[];
|
||||
totalCount: number;
|
||||
pageInfo: PageInfo;
|
||||
loading: boolean;
|
||||
loadMore: (cursor: string) => void;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export interface OwnProps {
|
||||
id?: string;
|
||||
children: (args: HostsArgs) => React.ReactNode;
|
||||
sourceId: string;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
filterQuery?: string;
|
||||
cursor: string | null;
|
||||
poll: number;
|
||||
}
|
||||
|
||||
export interface HostsComponentReduxProps {
|
||||
|
@ -39,31 +42,35 @@ export interface HostsComponentReduxProps {
|
|||
type HostsProps = OwnProps & HostsComponentReduxProps;
|
||||
|
||||
const HostsComponentQuery = pure<HostsProps>(
|
||||
({ children, filterQuery, sourceId, startDate, endDate, limit = 2, cursor }) => (
|
||||
({ id = 'hostsQuery', children, filterQuery, sourceId, startDate, endDate, limit = 2, poll }) => (
|
||||
<Query<GetHostsQuery.Query, GetHostsQuery.Variables>
|
||||
query={hostsQuery}
|
||||
fetchPolicy="cache-and-network"
|
||||
pollInterval={poll}
|
||||
notifyOnNetworkStatusChange
|
||||
variables={{
|
||||
sourceId,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: endDate,
|
||||
to: startDate,
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
pagination: {
|
||||
limit,
|
||||
cursor,
|
||||
cursor: null,
|
||||
tiebreaker: null,
|
||||
},
|
||||
filterQuery,
|
||||
}}
|
||||
>
|
||||
{({ data, loading, fetchMore }) =>
|
||||
children({
|
||||
{({ data, loading, fetchMore, refetch }) => {
|
||||
const hosts = getOr([], 'source.Hosts.edges', data);
|
||||
return children({
|
||||
id,
|
||||
refetch,
|
||||
loading,
|
||||
totalCount: getOr(0, 'source.Hosts.totalCount', data),
|
||||
hosts: getOr([], 'source.Hosts.edges', data),
|
||||
hosts,
|
||||
pageInfo: getOr({}, 'source.Hosts.pageInfo', data),
|
||||
loadMore: (newCursor: string) =>
|
||||
fetchMore({
|
||||
|
@ -89,8 +96,8 @@ const HostsComponentQuery = pure<HostsProps>(
|
|||
};
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -16,24 +16,28 @@ import {
|
|||
} from '../../../common/graphql/types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { State } from '../../store';
|
||||
import { inputsModel, State } from '../../store';
|
||||
import { uncommonProcessesQuery } from './index.gql_query';
|
||||
|
||||
export interface UncommonProcessesArgs {
|
||||
id: string;
|
||||
uncommonProcesses: UncommonProcessesEdges[];
|
||||
totalCount: number;
|
||||
pageInfo: PageInfo;
|
||||
loading: boolean;
|
||||
loadMore: (cursor: string) => void;
|
||||
refetch: inputsModel.Refetch;
|
||||
}
|
||||
|
||||
export interface OwnProps {
|
||||
id?: string;
|
||||
children: (args: UncommonProcessesArgs) => React.ReactNode;
|
||||
sourceId: string;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
filterQuery?: string;
|
||||
cursor: string | null;
|
||||
poll: number;
|
||||
}
|
||||
|
||||
export interface UncommonProcessesComponentReduxProps {
|
||||
|
@ -43,17 +47,28 @@ export interface UncommonProcessesComponentReduxProps {
|
|||
type UncommonProcessesProps = OwnProps & UncommonProcessesComponentReduxProps;
|
||||
|
||||
const UncommonProcessesComponentQuery = pure<UncommonProcessesProps>(
|
||||
({ children, filterQuery, sourceId, startDate, endDate, limit, cursor }) => (
|
||||
({
|
||||
id = 'uncommonProcessesQuery',
|
||||
children,
|
||||
filterQuery,
|
||||
sourceId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
cursor,
|
||||
poll,
|
||||
}) => (
|
||||
<Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables>
|
||||
query={uncommonProcessesQuery}
|
||||
fetchPolicy="cache-and-network"
|
||||
pollInterval={poll}
|
||||
notifyOnNetworkStatusChange
|
||||
variables={{
|
||||
sourceId,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: endDate,
|
||||
to: startDate,
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
},
|
||||
pagination: {
|
||||
limit,
|
||||
|
@ -63,9 +78,11 @@ const UncommonProcessesComponentQuery = pure<UncommonProcessesProps>(
|
|||
filterQuery,
|
||||
}}
|
||||
>
|
||||
{({ data, loading, fetchMore }) =>
|
||||
{({ data, loading, fetchMore, refetch }) =>
|
||||
children({
|
||||
id,
|
||||
loading,
|
||||
refetch,
|
||||
totalCount: getOr(0, 'source.UncommonProcesses.totalCount', data),
|
||||
uncommonProcesses: getOr([], 'source.UncommonProcesses.edges', data),
|
||||
pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data),
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
// @ts-ignore
|
||||
EuiSearchBar,
|
||||
} from '@elastic/eui';
|
||||
|
@ -16,12 +17,11 @@ import { defaultTo, noop } from 'lodash/fp';
|
|||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { pure } from 'recompose';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
import { AutoSizer } from '../../components/auto_sizer';
|
||||
import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper';
|
||||
import { Flyout, flyoutHeaderHeight } from '../../components/flyout';
|
||||
|
@ -33,12 +33,10 @@ import {
|
|||
Pane,
|
||||
PaneHeader,
|
||||
PaneScrollContainer,
|
||||
SubHeader,
|
||||
SubHeaderDatePicker,
|
||||
} from '../../components/page';
|
||||
import { DatePicker } from '../../components/page/date_picker';
|
||||
import { Footer } from '../../components/page/footer';
|
||||
import { Navigation } from '../../components/page/navigation';
|
||||
import { RangeDatePicker } from '../../components/range_date_picker';
|
||||
import { StatefulTimeline } from '../../components/timeline';
|
||||
import { headers } from '../../components/timeline/body/column_headers/headers';
|
||||
import { themeSelector } from '../../store/local/app';
|
||||
|
@ -107,17 +105,15 @@ const HomePageComponent = pure<Props>(({ theme }) => (
|
|||
headers={headers}
|
||||
/>
|
||||
</Flyout>
|
||||
<MyEuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem grow={false} data-test-subj="datePickerContainer">
|
||||
<RangeDatePicker id="global" />
|
||||
</EuiFlexItem>
|
||||
</MyEuiFlexGroup>
|
||||
<PageHeader data-test-subj="pageHeader">
|
||||
<Navigation data-test-subj="navigation" />
|
||||
</PageHeader>
|
||||
<PageContent data-test-subj="pageContent">
|
||||
<SubHeader data-test-subj="subHeader">
|
||||
<SubHeaderDatePicker data-test-subj="datePickerContainer">
|
||||
<DatePicker />
|
||||
</SubHeaderDatePicker>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</SubHeader>
|
||||
|
||||
<Pane data-test-subj="pane">
|
||||
<PaneHeader data-test-subj="paneHeader">
|
||||
<EuiSearchBar onChange={noop} />
|
||||
|
@ -148,3 +144,7 @@ const mapStateToProps = (state: State) => ({
|
|||
});
|
||||
|
||||
export const HomePage = connect(mapStateToProps)(HomePageComponent);
|
||||
|
||||
const MyEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin: 2px 0px;
|
||||
`;
|
||||
|
|
|
@ -18,73 +18,89 @@ import {
|
|||
UncommonProcessTable,
|
||||
} from '../../components/page/hosts';
|
||||
|
||||
import { manageQuery } from '../../components/page/manage_query';
|
||||
import { EventsQuery } from '../../containers/events';
|
||||
import { GlobalTime } from '../../containers/global_time';
|
||||
import { HostsQuery } from '../../containers/hosts';
|
||||
import { WithSource } from '../../containers/source';
|
||||
import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
|
||||
|
||||
const basePath = chrome.getBasePath();
|
||||
|
||||
// TODO: wire up the date picker to remove the hard-coded start/end dates, which show good data for the KPI event type
|
||||
const startDate = 1514782800000;
|
||||
const endDate = 1546318799999;
|
||||
const HostsTableManage = manageQuery(HostsTable);
|
||||
const EventsTableManage = manageQuery(EventsTable);
|
||||
const UncommonProcessTableManage = manageQuery(UncommonProcessTable);
|
||||
|
||||
export const Hosts = pure(() => (
|
||||
<WithSource sourceId="default">
|
||||
{({ auditbeatIndicesExist }) =>
|
||||
auditbeatIndicesExist || isUndefined(auditbeatIndicesExist) ? (
|
||||
<>
|
||||
<EventsQuery sourceId="default" startDate={startDate} endDate={endDate}>
|
||||
{({ kpiEventType, loading }) => (
|
||||
<TypesBar
|
||||
loading={loading}
|
||||
data={kpiEventType!.map((i: KpiItem) => ({
|
||||
x: i.count,
|
||||
y: i.value,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</EventsQuery>
|
||||
<HostsQuery sourceId="default" startDate={startDate} endDate={endDate} cursor={null}>
|
||||
{({ hosts, totalCount, loading, pageInfo, loadMore }) => (
|
||||
<HostsTable
|
||||
loading={loading}
|
||||
data={hosts}
|
||||
totalCount={totalCount}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
)}
|
||||
</HostsQuery>
|
||||
<UncommonProcessesQuery
|
||||
sourceId="default"
|
||||
startDate={0} // TODO: Wire this up to the date-time picker
|
||||
endDate={1544817214088} // TODO: Wire this up to the date-time picker
|
||||
cursor={null}
|
||||
>
|
||||
{({ uncommonProcesses, totalCount, loading, pageInfo, loadMore }) => (
|
||||
<UncommonProcessTable
|
||||
loading={loading}
|
||||
data={uncommonProcesses}
|
||||
totalCount={totalCount}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
)}
|
||||
</UncommonProcessesQuery>
|
||||
<EventsQuery sourceId="default" startDate={startDate} endDate={endDate}>
|
||||
{({ events, loading }) => (
|
||||
<EventsTable
|
||||
data={events!}
|
||||
loading={loading}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
)}
|
||||
</EventsQuery>
|
||||
</>
|
||||
<GlobalTime>
|
||||
{({ poll, to, from, setQuery }) => (
|
||||
<>
|
||||
<EventsQuery sourceId="default" startDate={from} endDate={to}>
|
||||
{({ kpiEventType, loading }) => (
|
||||
<TypesBar
|
||||
loading={loading}
|
||||
data={kpiEventType!.map((i: KpiItem) => ({
|
||||
x: i.count,
|
||||
y: i.value,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</EventsQuery>
|
||||
<HostsQuery sourceId="default" startDate={from} endDate={to} poll={poll}>
|
||||
{({ hosts, totalCount, loading, pageInfo, loadMore, id, refetch }) => (
|
||||
<HostsTableManage
|
||||
id={id}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
loading={loading}
|
||||
data={hosts}
|
||||
totalCount={totalCount}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
)}
|
||||
</HostsQuery>
|
||||
<UncommonProcessesQuery
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
poll={poll}
|
||||
cursor={null}
|
||||
>
|
||||
{({ uncommonProcesses, totalCount, loading, pageInfo, loadMore, id, refetch }) => (
|
||||
<UncommonProcessTableManage
|
||||
id={id}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
loading={loading}
|
||||
data={uncommonProcesses}
|
||||
totalCount={totalCount}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
)}
|
||||
</UncommonProcessesQuery>
|
||||
<EventsQuery sourceId="default" startDate={from} endDate={to}>
|
||||
{({ events, loading, id, refetch }) => (
|
||||
<EventsTableManage
|
||||
id={id}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
data={events!}
|
||||
loading={loading}
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
/>
|
||||
)}
|
||||
</EventsQuery>
|
||||
</>
|
||||
)}
|
||||
</GlobalTime>
|
||||
) : (
|
||||
<EmptyPage
|
||||
title="Looks like you don't have any auditbeat indices."
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { dragAndDropActions, hostsActions, timelineActions } from './local';
|
||||
export { dragAndDropActions, hostsActions, timelineActions, inputsActions } from './local';
|
||||
|
|
11
x-pack/plugins/secops/public/store/epic.ts
Normal file
11
x-pack/plugins/secops/public/store/epic.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { combineEpics } from 'redux-observable';
|
||||
|
||||
import { createLocalEpic } from './local';
|
||||
|
||||
export const createRootEpic = <State>() => combineEpics(createLocalEpic<State>());
|
|
@ -7,5 +7,7 @@
|
|||
export * from './actions';
|
||||
export * from './reducer';
|
||||
export * from './selectors';
|
||||
export * from './epic';
|
||||
export * from './model';
|
||||
|
||||
export { createStore } from './store';
|
||||
|
|
|
@ -8,3 +8,4 @@ export { dragAndDropActions } from './drag_and_drop';
|
|||
export { hostsActions } from './hosts';
|
||||
export { timelineActions } from './timeline';
|
||||
export { appActions } from './app';
|
||||
export { inputsActions } from './inputs';
|
||||
|
|
11
x-pack/plugins/secops/public/store/local/epic.ts
Normal file
11
x-pack/plugins/secops/public/store/local/epic.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { combineEpics } from 'redux-observable';
|
||||
|
||||
import { createGlobalTimeEpic } from './inputs';
|
||||
|
||||
export const createLocalEpic = <State>() => combineEpics(createGlobalTimeEpic<State>());
|
|
@ -7,3 +7,5 @@
|
|||
export * from './actions';
|
||||
export * from './reducer';
|
||||
export * from './selectors';
|
||||
export * from './epic';
|
||||
export * from './model';
|
||||
|
|
33
x-pack/plugins/secops/public/store/local/inputs/actions.ts
Normal file
33
x-pack/plugins/secops/public/store/local/inputs/actions.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { Refetch } from './model';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/secops/local/inputs');
|
||||
|
||||
export const setAbsoluteRangeDatePicker = actionCreator<{
|
||||
id: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}>('SET_ABSOLUTE_RANGE_DATE_PICKER');
|
||||
|
||||
export const setRelativeRangeDatePicker = actionCreator<{
|
||||
id: string;
|
||||
option: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}>('SET_RELATIVE_RANGE_DATE_PICKER');
|
||||
|
||||
export const setDuration = actionCreator<{ id: string; duration: number }>('SET_DURATION');
|
||||
|
||||
export const startAutoReload = actionCreator<{ id: string }>('START_KQL_AUTO_RELOAD');
|
||||
|
||||
export const stopAutoReload = actionCreator<{ id: string }>('STOP_KQL_AUTO_RELOAD');
|
||||
|
||||
export const setQuery = actionCreator<{ id: string; loading: boolean; refetch: Refetch }>(
|
||||
'SET_QUERY'
|
||||
);
|
70
x-pack/plugins/secops/public/store/local/inputs/epic.ts
Normal file
70
x-pack/plugins/secops/public/store/local/inputs/epic.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import { Action } from 'redux';
|
||||
import { Epic } from 'redux-observable';
|
||||
import { timer } from 'rxjs';
|
||||
import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import { setRelativeRangeDatePicker, startAutoReload, stopAutoReload } from './actions';
|
||||
import { Policy, TimeRange } from './model';
|
||||
|
||||
interface GlobalTimeEpicDependencies<State> {
|
||||
selectGlobalPolicy: (state: State) => Policy;
|
||||
selectGlobalTimeRange: (state: State) => TimeRange;
|
||||
}
|
||||
|
||||
export const createGlobalTimeEpic = <State>(): Epic<
|
||||
Action,
|
||||
Action,
|
||||
State,
|
||||
GlobalTimeEpicDependencies<State>
|
||||
> => (action$, state$, { selectGlobalPolicy, selectGlobalTimeRange }) => {
|
||||
const policy$ = state$.pipe(
|
||||
map(selectGlobalPolicy),
|
||||
filter(isNotNull)
|
||||
);
|
||||
|
||||
const timerange$ = state$.pipe(
|
||||
map(selectGlobalTimeRange),
|
||||
filter(isNotNull)
|
||||
);
|
||||
|
||||
return action$.pipe(
|
||||
filter(startAutoReload.match),
|
||||
withLatestFrom(policy$, timerange$),
|
||||
filter(([action, policy, timerange]) => timerange.kind === 'relative'),
|
||||
exhaustMap(([action, policy, timerange]) =>
|
||||
timer(0, policy.duration).pipe(
|
||||
map(() => {
|
||||
const option = get('option', timerange);
|
||||
if (option === 'quick-select') {
|
||||
const diff = timerange.to - timerange.from;
|
||||
return setRelativeRangeDatePicker({
|
||||
id: 'global',
|
||||
option,
|
||||
to: moment().valueOf(),
|
||||
from: moment()
|
||||
.subtract(diff, 'ms')
|
||||
.valueOf(),
|
||||
});
|
||||
}
|
||||
return setRelativeRangeDatePicker({
|
||||
id: 'global',
|
||||
option,
|
||||
to: moment().valueOf(),
|
||||
from: timerange.from,
|
||||
});
|
||||
}),
|
||||
takeUntil(action$.pipe(filter(stopAutoReload.match)))
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const isNotNull = <T>(value: T | null): value is T => value !== null;
|
14
x-pack/plugins/secops/public/store/local/inputs/index.ts
Normal file
14
x-pack/plugins/secops/public/store/local/inputs/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as inputsActions from './actions';
|
||||
import * as inputsModel from './model';
|
||||
|
||||
export { inputsActions };
|
||||
export * from './reducer';
|
||||
export * from './selectors';
|
||||
export * from './epic';
|
||||
export { inputsModel };
|
42
x-pack/plugins/secops/public/store/local/inputs/model.ts
Normal file
42
x-pack/plugins/secops/public/store/local/inputs/model.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
interface AbsoluteTimeRange {
|
||||
kind: 'absolute';
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
interface RelativeTimeRange {
|
||||
kind: 'relative';
|
||||
option: 'week-to-date' | 'month-to-date' | 'year-to-date' | 'quick-select';
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
export type TimeRange = AbsoluteTimeRange | RelativeTimeRange;
|
||||
|
||||
export interface Policy {
|
||||
kind: 'manual' | 'interval';
|
||||
duration: number; // in ms
|
||||
}
|
||||
|
||||
export type Refetch = () => void;
|
||||
export interface GlobalQuery {
|
||||
id: string;
|
||||
loading: boolean;
|
||||
refetch: null | Refetch;
|
||||
}
|
||||
|
||||
export interface InputsRange {
|
||||
timerange: TimeRange;
|
||||
policy: Policy;
|
||||
query: GlobalQuery[];
|
||||
}
|
||||
|
||||
export interface InputsModel {
|
||||
global: InputsRange;
|
||||
}
|
101
x-pack/plugins/secops/public/store/local/inputs/reducer.ts
Normal file
101
x-pack/plugins/secops/public/store/local/inputs/reducer.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get, unionBy } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
|
||||
import {
|
||||
setAbsoluteRangeDatePicker,
|
||||
setDuration,
|
||||
setQuery,
|
||||
setRelativeRangeDatePicker,
|
||||
startAutoReload,
|
||||
stopAutoReload,
|
||||
} from './actions';
|
||||
import { InputsModel } from './model';
|
||||
|
||||
export type InputsState = InputsModel;
|
||||
|
||||
export const initialInputsState: InputsState = {
|
||||
global: {
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from: moment()
|
||||
.subtract(1, 'hour')
|
||||
.valueOf(),
|
||||
to: moment().valueOf(),
|
||||
},
|
||||
query: [],
|
||||
policy: {
|
||||
kind: 'manual',
|
||||
duration: 5000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const inputsReducer = reducerWithInitialState(initialInputsState)
|
||||
.case(setAbsoluteRangeDatePicker, (state, { id, from, to }) => ({
|
||||
...state,
|
||||
[id]: {
|
||||
...get(id, state),
|
||||
timerange: {
|
||||
kind: 'absolute',
|
||||
from,
|
||||
to,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setRelativeRangeDatePicker, (state, { id, option, from, to }) => ({
|
||||
...state,
|
||||
[id]: {
|
||||
...get(id, state),
|
||||
timerange: {
|
||||
kind: 'relative',
|
||||
option,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setQuery, (state, { id, loading, refetch }) => ({
|
||||
...state,
|
||||
global: {
|
||||
...state.global,
|
||||
query: unionBy('id', [{ id, loading, refetch }], state.global.query),
|
||||
},
|
||||
}))
|
||||
.case(setDuration, (state, { id, duration }) => ({
|
||||
...state,
|
||||
[id]: {
|
||||
...get(id, state),
|
||||
policy: {
|
||||
...get(`${id}.policy`, state),
|
||||
duration,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(startAutoReload, (state, { id }) => ({
|
||||
...state,
|
||||
[id]: {
|
||||
...get(id, state),
|
||||
policy: {
|
||||
...get(`${id}.policy`, state),
|
||||
kind: 'interval',
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(stopAutoReload, (state, { id }) => ({
|
||||
...state,
|
||||
[id]: {
|
||||
...get(id, state),
|
||||
policy: {
|
||||
...get(`${id}.policy`, state),
|
||||
kind: 'manual',
|
||||
},
|
||||
},
|
||||
}))
|
||||
.build();
|
23
x-pack/plugins/secops/public/store/local/inputs/selectors.ts
Normal file
23
x-pack/plugins/secops/public/store/local/inputs/selectors.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { State } from '../../reducer';
|
||||
import { GlobalQuery, Policy, TimeRange } from './model';
|
||||
|
||||
const selectGlobalTimeRange = (state: State): TimeRange => state.local.inputs.global.timerange;
|
||||
const selectGlobalPolicy = (state: State): Policy => state.local.inputs.global.policy;
|
||||
const selectGlobalQuery = (state: State): GlobalQuery[] => state.local.inputs.global.query;
|
||||
|
||||
export const globalTimeRangeSelector = createSelector(
|
||||
selectGlobalTimeRange,
|
||||
timerange => timerange
|
||||
);
|
||||
|
||||
export const globalPolicySelector = createSelector(selectGlobalPolicy, policy => policy);
|
||||
|
||||
export const globalQuery = createSelector(selectGlobalQuery, query => query);
|
7
x-pack/plugins/secops/public/store/local/model.ts
Normal file
7
x-pack/plugins/secops/public/store/local/model.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { inputsModel } from './inputs';
|
|
@ -9,6 +9,7 @@ import { combineReducers } from 'redux';
|
|||
import { appReducer, AppState, initialAppState } from './app';
|
||||
import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from './drag_and_drop';
|
||||
import { hostsReducer, HostsState, initialHostsState } from './hosts';
|
||||
import { initialInputsState, inputsReducer, InputsState } from './inputs';
|
||||
import { initialTimelineState, timelineReducer, TimelineState } from './timeline';
|
||||
|
||||
export interface LocalState {
|
||||
|
@ -16,6 +17,7 @@ export interface LocalState {
|
|||
dragAndDrop: DragAndDropState;
|
||||
timeline: TimelineState;
|
||||
hosts: HostsState;
|
||||
inputs: InputsState;
|
||||
}
|
||||
|
||||
export const initialLocalState: LocalState = {
|
||||
|
@ -23,6 +25,7 @@ export const initialLocalState: LocalState = {
|
|||
dragAndDrop: initialDragAndDropState,
|
||||
timeline: initialTimelineState,
|
||||
hosts: initialHostsState,
|
||||
inputs: initialInputsState,
|
||||
};
|
||||
|
||||
export const localReducer = combineReducers<LocalState>({
|
||||
|
@ -30,4 +33,5 @@ export const localReducer = combineReducers<LocalState>({
|
|||
dragAndDrop: dragAndDropReducer,
|
||||
timeline: timelineReducer,
|
||||
hosts: hostsReducer,
|
||||
inputs: inputsReducer,
|
||||
});
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
export * from './drag_and_drop';
|
||||
export * from './hosts';
|
||||
export * from './timeline';
|
||||
export * from './inputs';
|
||||
|
|
7
x-pack/plugins/secops/public/store/model.ts
Normal file
7
x-pack/plugins/secops/public/store/model.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { inputsModel } from './local';
|
|
@ -4,4 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { dataProvidersSelector, hostsSelector, timelineByIdSelector } from './local';
|
||||
export {
|
||||
dataProvidersSelector,
|
||||
hostsSelector,
|
||||
timelineByIdSelector,
|
||||
globalTimeRangeSelector,
|
||||
globalPolicySelector,
|
||||
} from './local';
|
||||
|
|
|
@ -4,10 +4,24 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnyAction, applyMiddleware, compose, createStore as createReduxStore, Store } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
Action,
|
||||
AnyAction,
|
||||
applyMiddleware,
|
||||
compose,
|
||||
createStore as createReduxStore,
|
||||
Store,
|
||||
} from 'redux';
|
||||
import { createEpicMiddleware } from 'redux-observable';
|
||||
|
||||
import { initialState, reducer, State } from '.';
|
||||
import {
|
||||
createRootEpic,
|
||||
globalPolicySelector,
|
||||
globalTimeRangeSelector,
|
||||
initialState,
|
||||
reducer,
|
||||
State,
|
||||
} from '.';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -18,5 +32,20 @@ declare global {
|
|||
export const createStore = (state = initialState): Store<State, AnyAction> => {
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
return createReduxStore(reducer, state, composeEnhancers(applyMiddleware(thunk)));
|
||||
const middlewareDependencies = {
|
||||
selectGlobalPolicy: globalPolicySelector,
|
||||
selectGlobalTimeRange: globalTimeRangeSelector,
|
||||
};
|
||||
|
||||
const epicMiddleware = createEpicMiddleware<Action, Action, State, typeof middlewareDependencies>(
|
||||
{
|
||||
dependencies: middlewareDependencies,
|
||||
}
|
||||
);
|
||||
|
||||
const store = createReduxStore(reducer, state, composeEnhancers(applyMiddleware(epicMiddleware)));
|
||||
|
||||
epicMiddleware.run(createRootEpic<State>());
|
||||
|
||||
return store;
|
||||
};
|
||||
|
|
|
@ -39,8 +39,8 @@ export const buildQuery = (options: EventsRequestOptions) => {
|
|||
{
|
||||
range: {
|
||||
[options.sourceConfiguration.fields.timestamp]: {
|
||||
gte: to,
|
||||
lte: from,
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,8 +30,8 @@ export const buildQuery = (options: HostsRequestOptions) => {
|
|||
{
|
||||
range: {
|
||||
[options.sourceConfiguration.fields.timestamp]: {
|
||||
gte: to,
|
||||
lte: from,
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -31,8 +31,8 @@ export const buildQuery = (options: UncommonProcessesRequestOptions) => {
|
|||
{
|
||||
range: {
|
||||
[options.sourceConfiguration.fields.timestamp]: {
|
||||
gte: to,
|
||||
lte: from,
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
import { get, has, merge } from 'lodash/fp';
|
||||
|
||||
export const mergeFieldsWithHit = (
|
||||
export const mergeFieldsWithHit = <T>(
|
||||
fieldName: string,
|
||||
propertyName: string,
|
||||
flattenedFields: Record<string, {}>,
|
||||
flattenedFields: T,
|
||||
fieldMap: Readonly<Record<string, string>>,
|
||||
hit: { _source: {} }
|
||||
) => {
|
||||
|
@ -18,7 +18,7 @@ export const mergeFieldsWithHit = (
|
|||
if (has(esField, hit._source)) {
|
||||
const objectWithProperty = {
|
||||
[propertyName]: {
|
||||
...flattenedFields[propertyName],
|
||||
...get(propertyName, flattenedFields),
|
||||
...fieldName
|
||||
.split('.')
|
||||
.reduceRight((obj, next) => ({ [next]: obj }), get(esField, hit._source)),
|
||||
|
|
Loading…
Reference in a new issue