Add widgets on Hosts Page (#25404)

* get date picker popover in the right place

* add two widget for hosts pages

* fix spelling mistake

* fix type

* add pr review
This commit is contained in:
Xavier Mouligneau 2018-11-08 15:51:40 -05:00 committed by Andrew Goldstein
parent 34558a1e01
commit b671b9df3f
No known key found for this signature in database
GPG key ID: 42995DC9117D52CE
13 changed files with 554 additions and 38 deletions

View file

@ -7,7 +7,11 @@
declare module '@elastic/eui/lib/experimental' {
import { CommonProps } from '@elastic/eui/src/components/common';
export type EuiSeriesChartProps = CommonProps & {
width?: number | string;
height?: number | string;
orientation?: string;
xType?: string;
yType?: string;
stackBy?: string;
statusText?: string;
yDomain?: number[];
@ -21,7 +25,7 @@ declare module '@elastic/eui/lib/experimental' {
export const EuiSeriesChart: React.SFC<EuiSeriesChartProps>;
type EuiSeriesProps = CommonProps & {
data: Array<{ x: number; y: number; y0?: number }>;
data: Array<{ x: number; y: number | string; y0?: number }>;
lineSize?: number;
name: string;
color?: string;

View file

@ -524,6 +524,98 @@ export namespace SayMyNameResolvers {
>;
}
export namespace GetEventsQuery {
export type Variables = {
sourceId: string;
timerange: TimerangeInput;
filterQuery?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'Source';
getEvents?: GetEvents | null;
};
export type GetEvents = {
__typename?: 'EventsData';
events: Events[];
kpiEventType: KpiEventType[];
};
export type Events = {
__typename?: 'EventItem';
timestamp?: string | null;
event?: Event | null;
host?: Host | null;
source?: _Source | null;
destination?: Destination | null;
geo?: Geo | null;
suricata?: Suricata | null;
};
export type Event = {
__typename?: 'EventEcsFields';
type?: string | null;
severity?: number | null;
module?: string | null;
category?: string | null;
id?: number | null;
};
export type Host = {
__typename?: 'HostEcsFields';
hostname?: string | null;
ip?: string | null;
};
export type _Source = {
__typename?: 'SourceEcsFields';
ip?: string | null;
port?: number | null;
};
export type Destination = {
__typename?: 'DestinationEcsFields';
ip?: string | null;
port?: number | null;
};
export type Geo = {
__typename?: 'GeoEcsFields';
region_name?: string | null;
country_iso_code?: string | null;
};
export type Suricata = {
__typename?: 'SuricataEcsFields';
eve?: Eve | null;
};
export type Eve = {
__typename?: 'SuricataEveData';
proto?: string | null;
flow_id?: number | null;
alert?: Alert | null;
};
export type Alert = {
__typename?: 'SuricataAlertData';
signature?: string | null;
signature_id?: number | null;
};
export type KpiEventType = {
__typename?: 'KpiItem';
value: string;
count: number;
};
}
export namespace WhoAmIQuery {
export type Variables = {
sourceId: string;

View file

@ -0,0 +1,131 @@
/*
* 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 { EuiBasicTable, EuiTitle } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { EventItem } from '../../../common/graphql/types';
import { LoadingPanel } from '../loading';
export interface HoryzontalBarChartData {
x: number;
y: string;
}
interface BasicTableProps {
sortField: string;
// tslint:disable-next-line:no-any
pageOfItems: any[];
columns: Columns[];
title: string;
loading: boolean;
}
interface BasicTableState {
pageIndex: number;
pageSize: number;
sortField: string;
sortDirection: string;
}
export interface Columns {
field?: string;
name: string;
isMobileHeader?: boolean;
sortable?: boolean;
truncateText?: boolean;
hideForMobile?: boolean;
render?: (item: EventItem) => void;
}
interface Page {
index: number;
size: number;
}
interface Sort {
field: string;
direction: string;
}
interface TableChange {
page: Page;
sort: Sort;
}
export class BasicTable extends React.PureComponent<BasicTableProps, BasicTableState> {
public readonly state = {
pageIndex: 0,
pageSize: 3,
sortField: this.props.sortField,
sortDirection: 'asc',
};
public render() {
const { pageIndex, pageSize, sortField, sortDirection } = this.state;
const { columns, title, loading } = this.props;
if (loading) {
return <LoadingPanel height="100%" width="100%" text={`Loading ${title}`} />;
}
const pagination = {
pageIndex,
pageSize,
totalItemCount: this.props.pageOfItems.length,
pageSizeOptions: [3, 5, 8],
hidePerPageOptions: false,
};
const sorting = {
sort: {
field: sortField,
direction: sortDirection,
},
};
return (
<BasicTableContainer>
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
<EuiBasicTable
items={this.getCurrentItems()}
columns={columns}
pagination={pagination}
sorting={sorting}
onChange={this.onTableChange}
/>
</BasicTableContainer>
);
}
private getCurrentItems = () => {
const { pageOfItems } = this.props;
const { pageIndex, pageSize } = this.state;
return pageOfItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
};
private onTableChange = ({ page, sort }: TableChange) => {
const { index: pageIndex, size: pageSize } = page;
const { field: sortField, direction: sortDirection } = sort;
this.setState({
pageIndex,
pageSize,
sortField,
sortDirection,
});
};
}
export const BasicTableContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
`;

View file

@ -0,0 +1,48 @@
/*
* 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 { pure } from 'recompose';
import { EuiTitle } from '@elastic/eui';
import {
// @ts-ignore
EuiBarSeries,
// @ts-ignore
EuiSeriesChart,
} from '@elastic/eui/lib/experimental';
import { LoadingPanel } from '../loading';
export interface HorizontalBarChartData {
x: number;
y: string;
}
interface HorizontalBarChartProps {
barChartdata: HorizontalBarChartData[];
width?: number;
height?: number;
title: string;
loading: boolean;
}
export const HorizontalBarChart = pure<HorizontalBarChartProps>(
({ barChartdata, width, height, title, loading }) => {
return loading ? (
<LoadingPanel height="100%" width="100%" text="Loading data" />
) : (
<React.Fragment>
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
<EuiSeriesChart width={width} height={height} yType="ordinal" orientation="horizontal">
<EuiBarSeries name="Tag counts" data={barChartdata} />
</EuiSeriesChart>
</React.Fragment>
);
}
);

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 { EuiLoadingChart, EuiPanel, EuiText } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
interface LoadingProps {
text: string;
height: number | string;
width: number | string;
}
export const LoadingPanel = pure<LoadingProps>(({ height, text, width }) => (
<InfraLoadingStaticPanel style={{ height, width }}>
<InfraLoadingStaticContentPanel>
<EuiPanel>
<EuiLoadingChart size="m" />
<EuiText>
<p>{text}</p>
</EuiText>
</EuiPanel>
</InfraLoadingStaticContentPanel>
</InfraLoadingStaticPanel>
));
export const InfraLoadingStaticPanel = styled.div`
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
`;
export const InfraLoadingStaticContentPanel = styled.div`
flex: 0 0 auto;
align-self: center;
text-align: center;
`;

View file

@ -39,6 +39,7 @@ export const DatePicker = pure<DatePickerProps>(
isInvalid={false}
aria-label="Start date"
showTimeSelect
popperPlacement="top-end"
/>
}
endDateControl={
@ -48,6 +49,7 @@ export const DatePicker = pure<DatePickerProps>(
isInvalid={false}
aria-label="End date"
showTimeSelect
popperPlacement="top-end"
/>
}
/>

View file

@ -28,48 +28,48 @@ export interface OwnProps {
width: number;
}
interface StateProps {
dataProviders: DataProvider[];
data: ECS[];
range: Range;
sort: Sort;
interface StateReduxProps {
dataProviders?: DataProvider[];
data?: ECS[];
range?: Range;
sort?: Sort;
}
interface DispatchProps {
createTimeline: ActionCreator<{ id: string }>;
addProvider: ActionCreator<{
createTimeline?: ActionCreator<{ id: string }>;
addProvider?: ActionCreator<{
id: string;
provider: DataProvider;
}>;
updateData: ActionCreator<{
updateData?: ActionCreator<{
id: string;
data: ECS[];
}>;
updateProviders: ActionCreator<{
updateProviders?: ActionCreator<{
id: string;
providers: DataProvider[];
}>;
updateRange: ActionCreator<{
updateRange?: ActionCreator<{
id: string;
range: Range;
}>;
updateSort: ActionCreator<{
updateSort?: ActionCreator<{
id: string;
sort: Sort;
}>;
removeProvider: ActionCreator<{
removeProvider?: ActionCreator<{
id: string;
providerId: string;
}>;
}
type Props = OwnProps & StateProps & DispatchProps;
type Props = OwnProps & StateReduxProps & DispatchProps;
class StatefulTimelineComponent extends React.PureComponent<Props> {
public componentDidMount() {
const { createTimeline, id } = this.props;
createTimeline({ id });
createTimeline!({ id });
}
public render() {
@ -87,30 +87,30 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
} = this.props;
const onColumnSorted: OnColumnSorted = sorted => {
updateSort({ id, sort: sorted });
updateSort!({ id, sort: sorted });
};
const onDataProviderRemoved: OnDataProviderRemoved = dataProvider => {
removeProvider({ id, providerId: dataProvider.id });
removeProvider!({ id, providerId: dataProvider.id });
};
const onRangeSelected: OnRangeSelected = selectedRange => {
updateRange({ id, range: selectedRange });
updateRange!({ id, range: selectedRange });
};
return (
<Timeline
columnHeaders={headers}
columnRenderers={columnRenderers}
dataProviders={dataProviders}
data={data}
dataProviders={dataProviders!}
data={data!}
onColumnSorted={onColumnSorted}
onDataProviderRemoved={onDataProviderRemoved}
onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery
onRangeSelected={onRangeSelected}
range={range}
range={range!}
rowRenderers={rowRenderers}
sort={sort}
sort={sort!}
width={width}
/>
);

View file

@ -0,0 +1,56 @@
/*
* 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 gql from 'graphql-tag';
export const eventsQuery = gql`
query GetEventsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {
source(id: $sourceId) {
getEvents(timerange: $timerange, filterQuery: $filterQuery) {
events {
timestamp
event {
type
severity
module
category
id
}
host {
hostname
ip
}
source {
ip
port
}
destination {
ip
port
}
geo {
region_name
country_iso_code
}
suricata {
eve {
proto
flow_id
alert {
signature
signature_id
}
}
}
}
kpiEventType {
value
count
}
}
}
}
`;

View file

@ -0,0 +1,55 @@
/*
* 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 { getOr } from 'lodash/fp';
import React from 'react';
import { Query } from 'react-apollo';
import { pure } from 'recompose';
import { EventItem, GetEventsQuery, KpiItem } from '../../../common/graphql/types';
import { eventsQuery } from './events.gql_query';
interface EventsArgs {
events: EventItem[];
kpiEventType: KpiItem[];
loading: boolean;
}
interface EventsProps {
children: (args: EventsArgs) => React.ReactNode;
sourceId: string;
startDate: number;
endDate: number;
filterQuery?: string;
}
export const EventsQuery = pure<EventsProps>(
({ children, filterQuery, sourceId, startDate, endDate }) => (
<Query<GetEventsQuery.Query, GetEventsQuery.Variables>
query={eventsQuery}
fetchPolicy="no-cache"
notifyOnNetworkStatusChange
variables={{
filterQuery,
sourceId,
timerange: {
interval: '12h',
from: endDate,
to: startDate,
},
}}
>
{({ data, loading }) =>
children({
loading,
events: getOr([], 'source.getEvents.events', data),
kpiEventType: getOr([], 'source.getEvents.kpiEventType', data),
})
}
</Query>
)
);

View file

@ -21,7 +21,6 @@ import {
PageContent,
PageHeader,
Pane1,
Pane1FlexContent,
Pane1Header,
Pane1Style,
Pane2,
@ -74,16 +73,14 @@ export const HomePage = pure(() => (
<EuiSearchBar onChange={noop} />
</Pane1Header>
<PaneScrollContainer data-test-subj="pane1ScrollContainer">
<Pane1FlexContent data-test-subj="pane1FlexContent">
<Switch>
<Redirect from="/" exact={true} to="/overview" />
<Route path="/overview" component={Overview} />
<Route path="/hosts" component={Hosts} />
<Route path="/network" component={Network} />
<Route path="/link-to" component={LinkToPage} />
<Route component={NotFoundPage} />
</Switch>
</Pane1FlexContent>
<Switch>
<Redirect from="/" exact={true} to="/overview" />
<Route path="/overview" component={Overview} />
<Route path="/hosts" component={Hosts} />
<Route path="/network" component={Network} />
<Route path="/link-to" component={LinkToPage} />
<Route component={NotFoundPage} />
</Switch>
</PaneScrollContainer>
</Pane1>

View file

@ -3,12 +3,94 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import { Placeholders } from '../../components/visualization_placeholder';
import { EventItem, KpiItem } from '../../../common/graphql/types';
import { BasicTable, Columns } from '../../components/basic_table';
import { HorizontalBarChart, HorizontalBarChartData } from '../../components/horizontal_bar_chart';
import { Pane1FlexContent } from '../../components/page';
import { Placeholders, VisualizationPlaceholder } from '../../components/visualization_placeholder';
import { EventsQuery } from '../../containers/events';
export const Hosts = pure(() => (
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Hosts" />
<EventsQuery sourceId="default" startDate={1541044800000} endDate={1543640399999}>
{({ events, kpiEventType, loading }) => (
<Pane1FlexContent data-test-subj="pane1FlexContent">
<VisualizationPlaceholder>
<HorizontalBarChart
loading={loading}
title="KPI event types"
width={490}
height={279}
barChartdata={
kpiEventType.map((i: KpiItem) => ({
x: i.count,
y: i.value,
})) as HorizontalBarChartData[]
}
/>
</VisualizationPlaceholder>
<VisualizationPlaceholder>
<BasicTable
columns={eventsColumns}
loading={loading}
pageOfItems={events}
sortField="host.hostname"
title="Events"
/>
</VisualizationPlaceholder>
<Placeholders timelineId="pane2-timeline" count={8} myRoute="Hosts" />
</Pane1FlexContent>
)}
</EventsQuery>
));
const eventsColumns: Columns[] = [
{
name: 'Host name',
sortable: true,
truncateText: false,
hideForMobile: false,
render: (item: EventItem) => (
<React.Fragment>{getOr('--', 'host.hostname', item)}</React.Fragment>
),
},
{
name: 'Event type',
sortable: true,
truncateText: true,
hideForMobile: true,
render: (item: EventItem) => <React.Fragment>{getOr('--', 'event.type', item)}</React.Fragment>,
},
{
name: 'Source',
truncateText: true,
render: (item: EventItem) => (
<React.Fragment>
{getOr('--', 'source.ip', item).slice(0, 12)} : {getOr('--', 'source.port', item)}
</React.Fragment>
),
},
{
name: 'Destination',
sortable: true,
truncateText: true,
render: (item: EventItem) => (
<React.Fragment>
{getOr('--', 'destination.ip', item).slice(0, 12)} : {getOr('--', 'destination.port', item)}
</React.Fragment>
),
},
{
name: 'Location',
sortable: true,
truncateText: true,
render: (item: EventItem) => (
<React.Fragment>
{getOr('--', 'geo.region_name', item)} - {getOr('--', 'geo.country_iso_code', item)}
</React.Fragment>
),
},
];

View file

@ -7,8 +7,11 @@
import React from 'react';
import { pure } from 'recompose';
import { Pane1FlexContent } from '../../components/page';
import { Placeholders } from '../../components/visualization_placeholder';
export const Network = pure(() => (
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Network" />
<Pane1FlexContent>
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Network" />
</Pane1FlexContent>
));

View file

@ -7,8 +7,11 @@
import React from 'react';
import { pure } from 'recompose';
import { Pane1FlexContent } from '../../components/page';
import { Placeholders } from '../../components/visualization_placeholder';
export const Overview = pure(() => (
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Overview" />
<Pane1FlexContent>
<Placeholders timelineId="pane2-timeline" count={10} myRoute="Overview" />
</Pane1FlexContent>
));