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:
Xavier Mouligneau 2018-12-18 12:06:04 -05:00 committed by GitHub
parent 6521214503
commit bc47a8c828
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1780 additions and 218 deletions

View file

@ -164,6 +164,7 @@ declare module '@elastic/eui' {
disabled?: boolean;
isLoading?: boolean;
dateFormat?: string;
isCustom?: boolean;
};
export const EuiDatePickerRange: React.SFC<EuiDatePickerRangeProps>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {},
},

View file

@ -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: {},
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {},
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -7,5 +7,7 @@
export * from './actions';
export * from './reducer';
export * from './selectors';
export * from './epic';
export * from './model';
export { createStore } from './store';

View file

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

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

View file

@ -7,3 +7,5 @@
export * from './actions';
export * from './reducer';
export * from './selectors';
export * from './epic';
export * from './model';

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

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

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

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

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

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

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

View file

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

View file

@ -7,3 +7,4 @@
export * from './drag_and_drop';
export * from './hosts';
export * from './timeline';
export * from './inputs';

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

View file

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

View file

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

View file

@ -39,8 +39,8 @@ export const buildQuery = (options: EventsRequestOptions) => {
{
range: {
[options.sourceConfiguration.fields.timestamp]: {
gte: to,
lte: from,
gte: from,
lte: to,
},
},
},

View file

@ -30,8 +30,8 @@ export const buildQuery = (options: HostsRequestOptions) => {
{
range: {
[options.sourceConfiguration.fields.timestamp]: {
gte: to,
lte: from,
gte: from,
lte: to,
},
},
},

View file

@ -31,8 +31,8 @@ export const buildQuery = (options: UncommonProcessesRequestOptions) => {
{
range: {
[options.sourceConfiguration.fields.timestamp]: {
gte: to,
lte: from,
gte: from,
lte: to,
},
},
},

View file

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