Skip to content

Commit

Permalink
add a flyout
Browse files Browse the repository at this point in the history
  • Loading branch information
oatkiller committed Feb 18, 2020
1 parent f6dc674 commit 0a15730
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 43 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/endpoint/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface EndpointResultList {
}

export interface AlertData {
'@timestamp': Date;
'@timestamp': string;
agent: {
id: string;
version: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,42 @@ export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreSta
next(action);
const state = api.getState();
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) {
const response: AlertResultList = {
total: 1,
request_page_size: 1,
request_page_index: 0,
result_from_index: 0,
alerts: [
{
'@timestamp': new Date().toString(),
agent: { id: '', version: '' },
event: {
action: '',
},
file_classification: {
malware_classification: {
score: 1,
},
},
host: {
hostname: '',
ip: '',
os: {
name: '',
},
},
thread: {},
},
],
};
api.dispatch({ type: 'serverReturnedAlertsData', payload: response });
/*
* TODO dont commit this file
const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, {
query: paginationDataFromUrl(state) as HttpFetchQuery,
});
api.dispatch({ type: 'serverReturnedAlertsData', payload: response });
*/
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/

import qs from 'querystring';
import { AlertListState } from '../../types';
import { createSelector } from 'reselect';
import { Immutable } from '../../../../../common/types';
import { AlertListState, AlertIndexQueryParams } from '../../types';

/**
* Returns the Alert Data array from state
Expand All @@ -32,20 +34,41 @@ export const isOnAlertPage = (state: AlertListState): boolean => {
};

/**
* Returns the query object received from parsing the URL query params
* Returns the query object received from parsing the URL query params.
* The query value passed when requesting alert list data from the server.
* Also used to get new client side URLs.
*/
export const paginationDataFromUrl = (state: AlertListState): qs.ParsedUrlQuery => {
if (state.location) {
// Removes the `?` from the beginning of query string if it exists
const query = qs.parse(state.location.search.slice(1));
return {
...(query.page_size ? { page_size: query.page_size } : {}),
...(query.page_index ? { page_index: query.page_index } : {}),
};
} else {
return {};
// TODO rename to alertIndexQueryParams
export const paginationDataFromUrl: (
state: AlertListState
) => Immutable<AlertIndexQueryParams> = createSelector(
(state: AlertListState) => state.location,
(location: AlertListState['location']) => {
const data: AlertIndexQueryParams = {};
if (location) {
// Removes the `?` from the beginning of query string if it exists
const query = qs.parse(location.search.slice(1));
if (typeof query.page_size === 'string') {
data.page_size = query.page_size;
} else if (Array.isArray(query.page_size)) {
data.page_size = query.page_size[query.page_size.length - 1];
}

if (typeof query.page_index === 'string') {
data.page_index = query.page_index;
} else if (Array.isArray(query.page_index)) {
data.page_index = query.page_index[query.page_index.length - 1];
}

if (typeof query.selected_alert === 'string') {
data.selected_alert = query.selected_alert;
} else if (Array.isArray(query.selected_alert)) {
data.selected_alert = query.selected_alert[query.selected_alert.length - 1];
}
}
return data;
}
};
);

/**
* Returns a function that takes in a new page size and returns a new query param string
Expand All @@ -54,7 +77,7 @@ export const urlFromNewPageSizeParam: (
state: AlertListState
) => (newPageSize: number) => string = state => {
return newPageSize => {
const urlPaginationData = paginationDataFromUrl(state);
const urlPaginationData: AlertIndexQueryParams = { ...paginationDataFromUrl(state) };
urlPaginationData.page_size = newPageSize.toString();

// Only set the url back to page zero if the user has changed the page index already
Expand All @@ -72,8 +95,39 @@ export const urlFromNewPageIndexParam: (
state: AlertListState
) => (newPageIndex: number) => string = state => {
return newPageIndex => {
const urlPaginationData = paginationDataFromUrl(state);
const urlPaginationData: AlertIndexQueryParams = { ...paginationDataFromUrl(state) };
urlPaginationData.page_index = newPageIndex.toString();
return '?' + qs.stringify(urlPaginationData);
};
};

/**
* Returns a url like the current one, but with a new alert id.
*/
export const urlWithSelectedAlert: (
state: AlertListState
) => (alertID: string) => string = state => {
return (alertID: string) => {
const urlPaginationData = { ...paginationDataFromUrl(state) };
urlPaginationData.selected_alert = alertID;
return '?' + qs.stringify(urlPaginationData);
};
};

/**
* Returns a url like the current one, but with no alert id
*/
export const urlWithoutSelectedAlert: (state: AlertListState) => string = createSelector(
paginationDataFromUrl,
urlPaginationData => {
// TODO, different pattern for calculating URL w/ and w/o qs values
const newUrlPaginationData = { ...urlPaginationData };
delete newUrlPaginationData.selected_alert;
return '?' + qs.stringify(newUrlPaginationData);
}
);

export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector(
paginationDataFromUrl,
({ selected_alert: selectedAlert }) => selectedAlert !== undefined
);
7 changes: 7 additions & 0 deletions x-pack/plugins/endpoint/public/applications/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,10 @@ export type AlertListData = AlertResultList;
export type AlertListState = Immutable<AlertResultList> & {
readonly location?: Immutable<EndpointAppLocation>;
};

export interface AlertIndexQueryParams {
page_size?: string;
page_index?: string;
// TODO, reference alert event id type directly
selected_alert?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@

import { memo, useState, useMemo, useCallback } from 'react';
import React from 'react';
import { EuiDataGrid, EuiDataGridColumn, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
import {
EuiLink,
EuiDataGrid,
EuiDataGridColumn,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
EuiBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHistory } from 'react-router-dom';
import * as selectors from '../../store/alerts/selectors';
Expand All @@ -15,7 +27,7 @@ import { useAlertListSelector } from './hooks/use_alerts_selector';
export const AlertIndex = memo(() => {
const history = useHistory();

const columns: EuiDataGridColumn[] = useMemo(() => {
const columns = useMemo((): EuiDataGridColumn[] => {
return [
{
id: 'alert_type',
Expand Down Expand Up @@ -68,10 +80,14 @@ export const AlertIndex = memo(() => {
];
}, []);

// TODO consider structuredSelector
const { pageIndex, pageSize, total } = useAlertListSelector(selectors.alertListPagination);
const urlFromNewPageSizeParam = useAlertListSelector(selectors.urlFromNewPageSizeParam);
const urlWithSelectedAlert = useAlertListSelector(selectors.urlWithSelectedAlert);
const urlWithoutSelectedAlert = useAlertListSelector(selectors.urlWithoutSelectedAlert);
const urlFromNewPageIndexParam = useAlertListSelector(selectors.urlFromNewPageIndexParam);
const alertListData = useAlertListSelector(selectors.alertListData);
const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert);

const onChangeItemsPerPage = useCallback(
newPageSize => history.push(urlFromNewPageSizeParam(newPageSize)),
Expand All @@ -84,6 +100,30 @@ export const AlertIndex = memo(() => {
);

const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id));
const formatter = new Intl.DateTimeFormat(i18n.getLocale(), {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});

const handleAlertClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
if (event.target instanceof HTMLElement) {
const alertId: string | undefined = event.target.dataset.alertId;
if (alertId !== undefined) {
history.push(urlWithSelectedAlert(alertId));
}
}
},
[history, urlWithSelectedAlert]
);

const handleFlyoutClose = useCallback(() => {
history.push(urlWithoutSelectedAlert);
}, [history, urlWithoutSelectedAlert]);

const renderCellValue = useMemo(() => {
return ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => {
Expand All @@ -94,11 +134,16 @@ export const AlertIndex = memo(() => {
const row = alertListData[rowIndex % pageSize];

if (columnId === 'alert_type') {
return i18n.translate(
'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription',
{
defaultMessage: 'Malicious File',
}
return (
<EuiLink data-alert-id={'TODO'} onClick={handleAlertClick}>
{/* TODO populate data-alert-id with something real */}
{i18n.translate(
'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription',
{
defaultMessage: 'Malicious File',
}
)}
</EuiLink>
);
} else if (columnId === 'event_type') {
return row.event.action;
Expand All @@ -109,15 +154,29 @@ export const AlertIndex = memo(() => {
} else if (columnId === 'host_name') {
return row.host.hostname;
} else if (columnId === 'timestamp') {
return row['@timestamp'];
const date = new Date(row['@timestamp']);
if (isFinite(date.getTime())) {
return formatter.format(date);
} else {
return (
<EuiBadge color="warning">
{i18n.translate(
'xpack.endpoint.application.endpoint.alerts.alertDate.timestampInvalidLabel',
{
defaultMessage: 'invalid',
}
)}
</EuiBadge>
);
}
} else if (columnId === 'archived') {
return null;
} else if (columnId === 'malware_score') {
return row.file_classification.malware_classification.score;
}
return null;
};
}, [alertListData, pageSize, total]);
}, [alertListData, formatter, handleAlertClick, pageSize, total]);

const pagination = useMemo(() => {
return {
Expand All @@ -130,23 +189,40 @@ export const AlertIndex = memo(() => {
}, [onChangeItemsPerPage, onChangePage, pageIndex, pageSize]);

return (
<EuiPage data-test-subj="alertListPage">
<EuiPageBody>
<EuiPageContent>
<EuiDataGrid
aria-label="Alert List"
rowCount={total}
columns={columns}
columnVisibility={{
visibleColumns,
setVisibleColumns,
}}
renderCellValue={renderCellValue}
pagination={pagination}
data-test-subj="alertListGrid"
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
<>
{/*
TODO, rethink this. we may already have this in state. we still need `hasSelectedAlert`, to know to show this flyout. we should also have `selectedAlert`, which will eventually be loaded from server. */}
{hasSelectedAlert && (
<EuiFlyout size="l" onClose={handleFlyoutClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
alert detailz
{/* TODO, make an issue to add logic to get selected alert. it might already be in state! */}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>hey!</EuiFlyoutBody>
</EuiFlyout>
)}
<EuiPage data-test-subj="alertListPage">
<EuiPageBody>
<EuiPageContent>
<EuiDataGrid
aria-label="Alert List"
rowCount={total}
columns={columns}
columnVisibility={{
visibleColumns,
setVisibleColumns,
}}
renderCellValue={renderCellValue}
pagination={pagination}
data-test-subj="alertListGrid"
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</>
);
});

0 comments on commit 0a15730

Please sign in to comment.