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

Alert list frontend pagination #57142

Merged
merged 12 commits into from
Feb 18, 2020

Conversation

dplumlee
Copy link
Contributor

@dplumlee dplumlee commented Feb 7, 2020

Summary

Connects the alert list frontend to the backend api to pass alert data and pagination control. Updates the frontend routing to use and modify the url query for navigation and REST api control

alert_pagination_gif

Checklist

Delete any items that are not applicable to this PR.

For maintainers

@dplumlee dplumlee added WIP Work in progress v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v7.7.0 Team:Endpoint Response Endpoint Response Team Feature:Endpoint Elastic Endpoint feature labels Feb 7, 2020
@dplumlee dplumlee force-pushed the alert-list-frontend-pagination branch from 96b3d0c to 4cd62dc Compare February 7, 2020 19:01
@dplumlee dplumlee force-pushed the alert-list-frontend-pagination branch 3 times, most recently from 997c0c7 to f6457a0 Compare February 11, 2020 23:19
alerts: [
{
'@timestamp': new Date(0),
agent: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we want to have less silly data?

Copy link
Contributor

Choose a reason for hiding this comment

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

* you may not use this file except in compliance with the Elastic License.
*/

export function flattenedPromise<T>(): [Promise<T>, PromiseResolver<T>, PromiseRejector<T>] {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe a comment explaining what this is/why it's needed

export const routingMiddlewareFactory: MiddlewareFactory = (coreStart, history) => {
const [promise, resolve] = flattenedPromise();

return {
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 just to follow up with the comment @peluja1012 left on your pr, this is the new route action/middleware approach we're taking, would like to hear your thoughts

@@ -65,14 +67,14 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(({ basename, st
);
}}
/>
<Route path="/alerts" component={AlertIndex} />
<Route path="/alerts" render={() => <AlertIndex />} />
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 for consistency purposes and to be able to pass props down if/when needed

Copy link
Contributor

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.

@kqualters-elastic needs to pass down props and we can keep consistency with the rest of the app

});

it('should return alertListData', () => {
const actual = store.getState().alerts.length;
Copy link
Contributor

Choose a reason for hiding this comment

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

Good test, but the name actual seems a little ambiguous.

next(action);
const state = api.getState();
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) {
const response: AlertResultList = await coreStart.http.post(`/api/endpoint/alerts`, {
Copy link
Contributor

@madirey madirey Feb 11, 2020

Choose a reason for hiding this comment

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

You can always do this later, but we're going to be removing POST support for this API. If you want to try changing it now, it probably will just require changing this to .get and then changing the the line below to query: paginationDataFromUrl(state) as HttpFetchQuery), instead of body: JSON.stringify(paginationDataFromUrl(state)), ... I'm fine with waiting until the next PR though, whatever you feel like.

@@ -0,0 +1,12 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

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

We should move this file to the view/alerts folder, as hooks are associated with views. Also I think it might be better to rename the file to something like use_alerts_selector.ts because we will probably add more hooks in the future and I don't think we should keep them all in one file.


it("should recognize it's on the alert list page", () => {
const actual = isOnAlertPage(store.getState());
expect(actual).toBeTruthy();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think .toBe(true) applies better to this case because we expect isOnAlertPage to return either true or false.

import { AlertResultList } from '../../../../../common/types';
import { isOnAlertPage } from './selectors';

describe('alert list middleware and selectors', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should update this now that you split up the tests.

urlFromNewPageIndexParam,
} from './selectors';

describe('alert list middleware and selectors', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should update this now that you split up the tests.

it('loads the Alert List Page', async () => {
await testSubjects.existOrFail('alertListPage');
});
it('includes policy list table', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this description 😉

{ id: 'timestamp' },
{ id: 'archived' },
{ id: 'malware_score' },
{ id: 'Alert Type' },
Copy link
Contributor

@peluja1012 peluja1012 Feb 12, 2020

Choose a reason for hiding this comment

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

You can probably keep the id's the same as they were before. To provide friendly names to the column headers you can use the EuiDataGridColumn display property. So you could do something like:

{
    id: 'event_type',
    display: i18n.translate(
        'xpack.endpoint.application.endpoint.alerts.eventType',
        {
          defaultMessage: 'Event Type',
        }
}

const state = api.getState();
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) {
const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, {
query: paginationDataFromUrl(state) as HttpFetchQuery,
Copy link
Contributor

Choose a reason for hiding this comment

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

why as?

Copy link
Contributor

Choose a reason for hiding this comment

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

@oatkiller Cause I told him to 🙈

*/
export const paginationDataFromUrl = (state: AlertListState): qs.ParsedUrlQuery => {
// Removes the `?` from the beginning of query string if it exists
return state.location ? qs.parse(state.location.search.slice(1)) : {};
Copy link
Contributor

@oatkiller oatkiller Feb 13, 2020

Choose a reason for hiding this comment

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

i think you need to call out / validate each value here. otherwise a malicious actor can give an unwitting nerd a url like somekibana.net/endpoint?badStuff=encodedHacks and then the nerd clicks the url and a request goes to kibana w/ unwitting nerd's creds but with whatever params were provided by malicious actor @dplumlee

Copy link
Contributor

Choose a reason for hiding this comment

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

@oatkiller We are doing pretty comprehensive validation on the backend FYI. So maybe not necessary? Always helps to have an extra layer, I s'pose.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was also wondering this. I think once we start to grab data from the URL which is open to manipulation, we should do some sort of manipulation in order to avoid front-end code injections. I'm ok if we defer that to another Issue, but we should come up with a common approach.

const RouteCapture = memo(({ children }) => {
const location: EndpointAppLocation = useLocation();
const dispatch: (action: AppAction) => unknown = useDispatch();
dispatch({ type: 'userChangedUrl', payload: location });
Copy link
Contributor

Choose a reason for hiding this comment

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

I had mentioned this in my prior comment and just wanted to again make everyone aware - this will dispatch the action potentially BEFORE the component for the matched route is rendered. I think that is ok, but just wanted everyone to be aware of that behaviour.

applyMiddleware(alertMiddlewareFactory(coreStart), appSagaFactory(coreStart))
applyMiddleware(
substateMiddlewareFactory(s => s.alertList, alertMiddlewareFactory(coreStart).middleware),
appSagaFactory(coreStart)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just FYI. This is actually a bug @parkiino fixed at one point (should just be the sagaReduxMiddleware variable that was created on line 52). That being said, its also going away since Candace refactored it to use Middleware and remove Sagas


const json = useSelector(selectors.alertListData);
const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use of local state and not redux store?

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 dont know if we had a particular reason for not putting it in store beyond we didn't consider it part of the feature and it was just a side effect already built into the eui DataGrid component

return {
pageIndex,
pageSize,
pageSizeOptions: [10, 50, 100],
Copy link
Contributor

Choose a reason for hiding this comment

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

We should all sync up on the values to use for page size. @parkiino and I are using [10, 20, 50], which I think we discussed with Bonnie.

@@ -0,0 +1,22 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still used? should it be deleted?

@dplumlee dplumlee force-pushed the alert-list-frontend-pagination branch 2 times, most recently from 84c66af to d50c1c9 Compare February 14, 2020 19:55
@@ -27,6 +28,11 @@ export const alertListReducer: Reducer<AlertListState, AppAction> = (
...state,
...action.payload,
};
} else if (action.type === 'userChangedUrl') {
Copy link
Contributor

Choose a reason for hiding this comment

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

you are updating the Alerts store every time the URL changes - is that intentional? or did you forget to use isOnAlertsList()?

Also, when the user leaves the alert list page, the store for it should be reset.

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 suppose, only time the action is dispatched currently is if the app is already confirmed to be on the alerts page

Copy link
Contributor

Choose a reason for hiding this comment

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

The action is dispatched anytime the URL changes - so navigating to /management or /policy will trigger the action.

Copy link
Contributor

@peluja1012 peluja1012 Feb 18, 2020

Choose a reason for hiding this comment

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

@paul-tavares We didn't want to force other teams to use this approach so we are storing the location in the alerts piece of the store. If all of us want to use this, we can store the location at the top level of the store. The idea is to update location every time the url changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I created an issue for clearing the state when navigating away https://github.com/elastic/endpoint-app-team/issues/183

Copy link
Contributor

Choose a reason for hiding this comment

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

@peluja1012 I actually thought we had agreed to use this new approach instead of usePageId() when we talked through the revised implementation around using location provided by react-router. At least I was planning on refactoring the Policy/Management pages to use userChangedUrl instead once this merged in.

Not sure if storing it globally would facilitate things though - since currently we're building our reducers/middleware to NOT be aware of global state. Accessing the location information would be an issue thus why each concern needs to store it, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

@paul-tavares Oh that's nice. I didn't realize we were all moving forward with this approach. It would certainly be easier the way the code is laid out now to have each concern store the location. I don't know if I like the fact that we'll need a copied and pasted reducer clause to update the location in each concern though. I would rather have location be global. We could refactor the code to allow for a global location piece of state or create a reducer helper to handle updating the location in each concern. Either way, I'd like to do this work in a separate PR. This PR is currently blocking work on Alert Details and Alerts Filtering, so I would like to get it merged as soon as we can.

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed 👍

* Returns a boolean based on whether or not the user is on the alerts page
*/
export const isOnAlertPage = (state: AlertListState): boolean => {
return state.location ? state.location.pathname.endsWith('/alerts') : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

(optional I would suggest you do a strict === as in state.location.pathname === '/alerts'

@dplumlee dplumlee force-pushed the alert-list-frontend-pagination branch from 08600f4 to 376da43 Compare February 18, 2020 18:23
@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

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

@dplumlee dplumlee merged commit f6dc674 into elastic:master Feb 18, 2020
dplumlee added a commit to dplumlee/kibana that referenced this pull request Feb 18, 2020
@dplumlee dplumlee deleted the alert-list-frontend-pagination branch February 18, 2020 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Endpoint Elastic Endpoint feature release_note:skip Skip the PR/issue when compiling release notes Team:Endpoint Response Endpoint Response Team v7.7.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants