Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a flyout to alert list. #57926

Merged
merged 23 commits into from
Feb 21, 2020
Merged

Conversation

oatkiller
Copy link
Contributor

@oatkiller oatkiller commented Feb 18, 2020

Summary

Add a button to alert list that adds selected_alert query string to url. if qs is present, flyout shows up. No data loading logic added yet.

image

Related to https://github.com/elastic/endpoint-app-team/issues/92

Issues

The tests for this PR emit warnings. These don't effect the test. I believe they are being caused by useEffect calls in EuiDataGrid that set state right after the first render, causing a subsequent render in many cases, but that's just a guess. I . haven't been able to work out a solution with #eui yet.
https://github.com/elastic/endpoint-app-team/issues/205

This uses our custom type for createStructuredSelector, but the type isn't completely compatible w/ the implementation:
https://github.com/elastic/endpoint-app-team/issues/204

And here are a bunch of other issues created during the development of this PR:

Checklist

Delete any items that are not applicable to this PR.

For maintainers

@oatkiller oatkiller added Feature:Resolver Security Solution Resolver feature Team:Endpoint Data Visibility Team managing the endpoint resolver release_note:skip Skip the PR/issue when compiling release notes v7.7.0 labels Feb 19, 2020
@oatkiller oatkiller changed the title WIP: add a flyout to alert list. Add a flyout to alert list. Feb 19, 2020
state: AlertListState
) => (alertID: string) => string = state => {
return (alertID: string) => {
const urlPaginationData = { ...queryParams(state) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the the variable name urlPaginationData?

@@ -72,8 +94,38 @@ export const urlFromNewPageIndexParam: (
state: AlertListState
) => (newPageIndex: number) => string = state => {
return newPageIndex => {
const urlPaginationData = paginationDataFromUrl(state);
const urlPaginationData: AlertIndexQueryParams = { ...queryParams(state) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to spread ...queryParams(state)?

@@ -7,9 +7,9 @@ import { KibanaRequest } from 'kibana/server';
import { EndpointAppConstants } from '../../../common/types';
import { EndpointAppContext, AlertRequestParams, JSONish } from '../../types';

export const buildAlertListESQuery = async (
export const buildAlertListESQuery: (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fn used no await so I removed async. Might look into adding lint to enforce this.

must: [
{
match: {
'event.kind': 'alert',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter by alerts

{
'@timestamp': {
order: 'desc',
expect(query).toMatchInlineSnapshot(`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update the test to accommodate for the new filter

{
'@timestamp': {
order: 'desc',
expect(query).toMatchInlineSnapshot(`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update the test to accommodate for the new filter

</EuiPageBody>
</EuiPage>
<>
{hasSelectedAlert && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the url has a selected alert in the query string, show this.

@@ -71,7 +71,7 @@ export interface EndpointResultList {
}

export interface AlertData {
'@timestamp': Date;
'@timestamp': string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
export const urlWithSelectedAlert: (
state: AlertListState
) => (alertID: string) => string = state => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to nest these functions? Why not just have one function

Copy link

@alexk307 alexk307 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just some questions


const handleAlertClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
if (event.target instanceof HTMLElement) {
Copy link
Contributor

@parkiino parkiino Feb 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bc i'm still a noob at types): do we have to do this because of typescript?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this actually isn't necessary. The event.target could be any DOM node that is a descendant of event.currentTarget aka the thing that the event listener is on. Any DOM node isn't necessarily an HTMLElement. DOM nodes don't all support alertId.

In this case, I could just refer to event.currentTarget. The (synthetic) event handler is registered to an html element (the one w/ the data-* attribute) and so referring to event.currentTarget will work w/o the guard.

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okie good to know

};
} else {
return {};
export const uiQueryParams: (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do something similar for Policy list PR that I have pending to add pagination to the URL.
In my PR I was trying to NOT hold on to the URL information passed through from React-Router, but rather (in the reducer) do the needed processing on it that Policy needs and save the the output to the store. Is that OK? or should I follow the same approach as here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand you here, but let me know if I'm off base.

When modeling redux state, I try to record a normalized (like database normalization: https://en.wikipedia.org/wiki/Database_normalization) representation of the relevant side effects. If some calculation is done, I do it in a selector. The selector forms the read-only model for the state. Any value that can be calculated as a result of the reduced-side effects (the state) is provided by the selector. The selector (via reselect) also handles caching / invalidating the calculated views of state.

More thoughts

Redux as a pattern separates the writing/mutation/updating parts of a model from the viewing/calculating/presenting parts of the same model.

Reducer/state

  • Keeps a normalized record of side effects
  • Keeps data that is required by selector, or data that is required in order to update state in the future (e.g. a counter)
  • Keeps data in a format that is easily updated when new side effects are introduced
  • Doesn't calculate things from state. A reducer could be run 10x before the view runs a selector
  • Fields of state are analogous to private instance members in a class

Selector

  • Like a read-only model for the state
  • Calculates 'views' on the state. Think about the 'presenter' model.
  • Selectors are functions that are called at the time they are needed, do some calculation (maybe) and then memoize it (usually.)
  • Selectors are like 'getters' in a class

All that having been said, I don't think the approach we have in this PR is the final goal for URL handling. I think its too prone to copy-paste errors and there is too much boilerplate. We should hit the drawing board.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paul-tavares i've changed my query string stuff a lot. I think it's closer to what you have now. We should be able to come up w/ a common implementation in a PR or so I think. Let me know what you think of this one.

const value = query[key];
if (typeof value === 'string') {
data[key] = value;
} else if (Array.isArray(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱 forgot about this. I will add awareness of it to my PR

@@ -84,6 +99,30 @@ export const AlertIndex = memo(() => {
);

const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id));
const formatter = new Intl.DateTimeFormat(i18n.getLocale(), {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use <FormattedDate> and/or <FormattedTime> from @kbn/i18n/react instead of a local formatter?

Also - Just FYI: in Policy list we discussed displaying date/time in relative format if within the last 1h and formatted otherwise. I have that in policy_list now, but if common requirement across all of our pages, then we should move/adjust/improve it 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to <FormattedDate />, its equivalent

if (event.target instanceof HTMLElement) {
const alertId: string | undefined = event.target.dataset.alertId;
if (alertId !== undefined) {
history.push(urlWithSelectedAlert(alertId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not realize that history.push handled URL that started with ?. I was using useLocation() to grab the current pathname in mine. I will refactor to remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

history will interpret urls as relative to the current one.

// ran this on this page via console
history.pushState(null, '', '?cool=true')
// url is now: 'https://github.com/elastic/kibana/pull/57926/files?cool=true'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paul-tavares i switched my implementation to use LocationDescriptionObject because its easier

defaultMessage: 'Malicious File',
}
return (
<EuiLink data-alert-id={'TODO'} onClick={handleAlertClick}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the URL be set on this item? (allowing user's to do control click to new window or copy URL to clipboard)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip. After realizing that EuiLink returns a button, I didn't even think about trying to use href'. If href works, that'll simplify things a lot.

/**
* Returns a url like the current one, but with a new alert id.
*/
export const urlWithSelectedAlert: (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be named something like urlSearchParams* to be more reflective of what it is returning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I view this as returning a relative URL. It is a string, beginning with ? and so it fits the technical def: https://tools.ietf.org/html/rfc1808#section-5

The intent is that these will be used with history.pushState or the href attribute of an a tag. These accept relative URLs and treat them as absolute after combining them with the base URL of the page.

Maybe I could explain the above in a comment. With all that being said, do you still think I should go for a diff name?


/**
* Returns a function that takes in a new page index and returns a new query param string
*/
export const urlFromNewPageIndexParam: (
state: AlertListState
) => (newPageIndex: number) => string = state => {
) => (newPageIndex: number) => string = createSelector(uiQueryParams, paramData => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexk307 I missed your earlier comment, so replying here.
Selectors have a common interface, they take the state of the app (what is returned by the top level reducer) and return whatever they want. The return value should be a primitive, or treated as immutable as its used to invalidate views and things.

These selectors want to take a second param. In this case, the page number for the link they want. But since selectors always take exactly one argument, we use a thunk instead.

Helpers that take a selector, e.g. useSelector, createStructuredSelector, createSelector, etc, all still work fine, since we are still a standard (odd) selector. Its not a pattern i like to use too often, but we've got a whole bunch here (and in Resolver.)

@@ -63,6 +64,10 @@ export interface GlobalState {
readonly policyList: PolicyListState;
}

/**
* A better type for createStructuredSelector. This doesn't support the options object.
* TODO
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

substateMiddlewareFactory(
globalState => globalState.alertList,
alertMiddlewareFactory(coreStart)
disableMiddleware
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe put a comment in here saying why you need the ability to disable the middleware?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

durp

@peluja1012 peluja1012 added the Team:Endpoint Response Endpoint Response Team label Feb 20, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/endpoint-response (Team:Endpoint Response)

request_page_index: 0,
result_from_index: 0,
};
const response: AlertResultList = mockAlertResultList();
return response;
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace

disable test i broke. lets rethink this
[history, urlFromNewPageIndexParam]
newPageIndex => {
return history.push(
urlFromQueryParams({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paul-tavares doing something closer to what you have.
@dplumlee @peluja1012 thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works for me. maybe we should also use the same querystring package that @paul-tavares is using (query-string) just to keep it consistent. I think it's in kibana already and it's a bit better than the one we're using

Copy link
Contributor

@peluja1012 peluja1012 Feb 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oatkiller looks good but closing the flyout doesn't seem to be working when only selected_alert is present. The user gets redirected to /app/endpoint/:
close_flyout_bug mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I goofed this up. I'm working on another slight improvement, but I feel like I might just be getting in the way of stuff @paul-tavares is doing. Maybe worth syncing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

oatkiller added 2 commits February 21, 2020 11:30
@oatkiller
Copy link
Contributor Author

@paul-tavares @peluja1012 @kqualters-elastic @dplumlee @parkiino Changed the way query strings are done, fixed tests and stuff. Comments and things might be sloppy, @dplumlee and @parkiino will fix that :)

);

const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id));

const handleFlyoutClose = useCallback(() => {
const { selected_alert, ...paramsWithoutSelectedAlert } = queryParams;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we doing url params in snake case? is that the convention? i forget...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oatkiller oatkiller merged commit 02efb01 into elastic:master Feb 21, 2020
@oatkiller oatkiller deleted the alert-list-flyout branch February 21, 2020 19:44
mbondyra added a commit to mbondyra/kibana that referenced this pull request Feb 21, 2020
…_improve-advanced-settings-save

* commit '02efb01c481f9f24d8d707f06dfc68b2fb805001': (43 commits)
  [Endpoint] Add a flyout to alert list. (elastic#57926)
  Make sure index pattern has fields before parsing (elastic#58242)
  Sanitize workpad before sending to API (elastic#57704)
  [ML] Transform: Support multi-line JSON notation in advanced editor (elastic#58015)
  [Endpoint] Refactor Management List Tests (elastic#58148)
  [kbn/optimizer] include bootstrap cache key in optimizer cache key (elastic#58176)
  Do not refresh color scale on each lookup (elastic#57792)
  Updating to @elastic/[email protected] (elastic#54662)
  Trigger context (elastic#57870)
  [ML] Transforms: Adds clone feature to transforms list. (elastic#57837)
  [ML] New Platform server shim: update fields service routes (elastic#58060)
  [Endpoint] EMT-184: change endpoints to metadata up and down the code base. (elastic#58038)
  document difference between log record formats (elastic#57798)
  Expose elasticsearch config schema (elastic#57655)
  [ui/agg_response/tabify] update types for search/expressions/build_tabular_inspector_data.ts (elastic#58130)
  [SIEM] Cleans Cypress tests code (elastic#58134)
  fix: 🐛 make dev server Storybook builds work again (elastic#58188)
  Prevent core savedObjects plugin from being overridden (elastic#58193)
  Expose serverBasePath on client-side (elastic#58070)
  Fix legend sizing on area charts (elastic#58083)
  ...
oatkiller pushed a commit that referenced this pull request Feb 22, 2020
* Filter alert API so it shows only Alerts instead of all documents in the index
* Clicking an item in the alert list will open a flyout
jloleysens added a commit to jloleysens/kibana that referenced this pull request Feb 24, 2020
…-out-of-legacy

* 'master' of github.com:elastic/kibana:
  [SIEM] [Case] Enable case by default. Snake to camel on UI (elastic#57936)
  [File upload] Update remaining File Upload dependencies for NP migration (elastic#58128)
  Use EuiTokens for ES field types (elastic#57911)
  Added UI support for the default action group for Alert Type Model (elastic#57603)
  force savedObject API consumers to define SO type explicitly (elastic#58022)
  Update dependency @elastic/charts to ^17.1.1 (elastic#57634)
  [Endpoint] Add a flyout to alert list. (elastic#57926)
  Make sure index pattern has fields before parsing (elastic#58242)
  Sanitize workpad before sending to API (elastic#57704)
  [ML] Transform: Support multi-line JSON notation in advanced editor (elastic#58015)
  [Endpoint] Refactor Management List Tests (elastic#58148)
  [kbn/optimizer] include bootstrap cache key in optimizer cache key (elastic#58176)
  Do not refresh color scale on each lookup (elastic#57792)
  Updating to @elastic/[email protected] (elastic#54662)
  Trigger context (elastic#57870)
  [ML] Transforms: Adds clone feature to transforms list. (elastic#57837)
  [ML] New Platform server shim: update fields service routes (elastic#58060)
  [Endpoint] EMT-184: change endpoints to metadata up and down the code base. (elastic#58038)
@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Resolver Security Solution Resolver feature release_note:skip Skip the PR/issue when compiling release notes Team:Endpoint Data Visibility Team managing the endpoint resolver Team:Endpoint Response Endpoint Response Team v7.7.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants