Skip to content

Commit

Permalink
[Resolver] model location.search in redux (#76140)
Browse files Browse the repository at this point in the history
Read location.search from the redux store instead of a hook so that the entire view has a single (synchronized) source of truth.

Also, no longer pass `pushToQueryParams` function to various components.
  • Loading branch information
Robert Austin authored Aug 28, 2020
1 parent 712c4bd commit 3fc3f58
Show file tree
Hide file tree
Showing 18 changed files with 271 additions and 179 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.
*/

/**
* The legacy `crumbEvent` and `crumbId` parameters.
* @deprecated
*/
export function breadcrumbParameters(
locationSearch: string,
resolverComponentInstanceID: string
): { crumbEvent: string; crumbId: string } {
const urlSearchParams = new URLSearchParams(locationSearch);
const { eventKey, idKey } = parameterNames(resolverComponentInstanceID);
return {
// Use `''` for backwards compatibility with deprecated code.
crumbEvent: urlSearchParams.get(eventKey) ?? '',
crumbId: urlSearchParams.get(idKey) ?? '',
};
}

/**
* Parameter names based on the `resolverComponentInstanceID`.
*/
function parameterNames(
resolverComponentInstanceID: string
): {
idKey: string;
eventKey: string;
} {
const idKey: string = `resolver-${resolverComponentInstanceID}-id`;
const eventKey: string = `resolver-${resolverComponentInstanceID}-event`;
return {
idKey,
eventKey,
};
}
28 changes: 28 additions & 0 deletions x-pack/plugins/security_solution/public/resolver/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,37 @@ interface UserSelectedRelatedEventCategory {
};
}

/**
* Used by `useStateSyncingActions` hook.
* This is dispatched when external sources provide new parameters for Resolver.
* When the component receives a new 'databaseDocumentID' prop, this is fired.
*/
interface AppReceivedNewExternalProperties {
type: 'appReceivedNewExternalProperties';
/**
* Defines the externally provided properties that Resolver acknowledges.
*/
payload: {
/**
* the `_id` of an ES document. This defines the origin of the Resolver graph.
*/
databaseDocumentID?: string;
/**
* An ID that uniquely identifies this Resolver instance from other concurrent Resolvers.
*/
resolverComponentInstanceID: string;

/**
* The `search` part of the URL of this page.
*/
locationSearch: string;
};
}

export type ResolverAction =
| CameraAction
| DataAction
| AppReceivedNewExternalProperties
| UserBroughtProcessIntoView
| UserFocusedOnResolverNode
| UserSelectedResolverNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,10 @@ interface ServerReturnedRelatedEventData {
readonly payload: ResolverRelatedEvents;
}

/**
* Used by `useStateSyncingActions` hook.
* This is dispatched when external sources provide new parameters for Resolver.
* When the component receives a new 'databaseDocumentID' prop, this is fired.
*/
interface AppReceivedNewExternalProperties {
type: 'appReceivedNewExternalProperties';
/**
* Defines the externally provided properties that Resolver acknowledges.
*/
payload: {
/**
* the `_id` of an ES document. This defines the origin of the Resolver graph.
*/
databaseDocumentID?: string;
resolverComponentInstanceID: string;
};
}

export type DataAction =
| ServerReturnedResolverData
| ServerFailedToReturnResolverData
| ServerFailedToReturnRelatedEventData
| ServerReturnedRelatedEventData
| AppReceivedNewExternalProperties
| AppRequestedResolverData
| AppAbortedResolverDataRequest;
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import * as selectors from './selectors';
import { DataState } from '../../types';
import { ResolverAction } from '../actions';
import { dataReducer } from './reducer';
import { DataAction } from './action';
import { createStore } from 'redux';
import {
mockTreeWithNoAncestorsAnd2Children,
Expand All @@ -20,7 +20,7 @@ import { uniquePidForProcess } from '../../models/process_event';
import { EndpointEvent } from '../../../../common/endpoint/types';

describe('data state', () => {
let actions: DataAction[] = [];
let actions: ResolverAction[] = [];

/**
* Get state, given an ordered collection of actions.
Expand Down Expand Up @@ -68,7 +68,13 @@ describe('data state', () => {
actions = [
{
type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID },
payload: {
databaseDocumentID,
resolverComponentInstanceID,

// `locationSearch` doesn't matter for this test
locationSearch: '',
},
},
];
});
Expand Down Expand Up @@ -120,7 +126,13 @@ describe('data state', () => {
actions = [
{
type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID },
payload: {
databaseDocumentID,
resolverComponentInstanceID,

// `locationSearch` doesn't matter for this test
locationSearch: '',
},
},
{
type: 'appRequestedResolverData',
Expand Down Expand Up @@ -182,6 +194,8 @@ describe('data state', () => {
payload: {
databaseDocumentID: firstDatabaseDocumentID,
resolverComponentInstanceID: resolverComponentInstanceID1,
// `locationSearch` doesn't matter for this test
locationSearch: '',
},
},
// this happens when the middleware starts the request
Expand All @@ -195,6 +209,8 @@ describe('data state', () => {
payload: {
databaseDocumentID: secondDatabaseDocumentID,
resolverComponentInstanceID: resolverComponentInstanceID2,
// `locationSearch` doesn't matter for this test
locationSearch: '',
},
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
selectedNode: nodeID,
};
return next;
} else if (action.type === 'appReceivedNewExternalProperties') {
const next: ResolverUIState = {
...state,
locationSearch: action.payload.locationSearch,
resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
};
return next;
} else {
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ export const ariaFlowtoNodeID: (
}
);

/**
* The legacy `crumbEvent` and `crumbId` parameters.
* @deprecated
*/
export const breadcrumbParameters = composeSelectors(
uiStateSelector,
uiSelectors.breadcrumbParameters
);

/**
* Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a
* concern-specific selector. `selector` should return the concern-specific state.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
/*
* 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 { ResolverUIState } from '../../types';

/**
* id of the "current" tree node (fake-focused)
*/
export const ariaActiveDescendant = createSelector(
(uiState: ResolverUIState) => uiState,
/* eslint-disable no-shadow */
({ ariaActiveDescendant }) => {
return ariaActiveDescendant;
}
);

/**
* id of the currently "selected" tree node
*/
export const selectedNode = createSelector(
(uiState: ResolverUIState) => uiState,
/* eslint-disable no-shadow */
({ selectedNode }: ResolverUIState) => {
return selectedNode;
}
);
/*
* 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 { ResolverUIState } from '../../types';
import * as locationSearchModel from '../../models/location_search';

/**
* id of the "current" tree node (fake-focused)
*/
export const ariaActiveDescendant = createSelector(
(uiState: ResolverUIState) => uiState,
/* eslint-disable no-shadow */
({ ariaActiveDescendant }) => {
return ariaActiveDescendant;
}
);

/**
* id of the currently "selected" tree node
*/
export const selectedNode = createSelector(
(uiState: ResolverUIState) => uiState,
/* eslint-disable no-shadow */
({ selectedNode }: ResolverUIState) => {
return selectedNode;
}
);

/**
* The legacy `crumbEvent` and `crumbId` parameters.
* @deprecated
*/
export const breadcrumbParameters = createSelector(
(state: ResolverUIState) => state.locationSearch,
(state: ResolverUIState) => state.resolverComponentInstanceID,
(locationSearch, resolverComponentInstanceID) => {
if (locationSearch === undefined || resolverComponentInstanceID === undefined) {
// Equivalent to `null`
return {
crumbId: '',
crumbEvent: '',
};
}
return locationSearchModel.breadcrumbParameters(locationSearch, resolverComponentInstanceID);
}
);
20 changes: 18 additions & 2 deletions x-pack/plugins/security_solution/public/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ export interface ResolverUIState {
* `nodeID` of the selected node
*/
readonly selectedNode: string | null;

/**
* The `search` part of the URL.
*/
readonly locationSearch?: string;

/**
* An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
*/
readonly resolverComponentInstanceID?: string;
}

/**
Expand Down Expand Up @@ -198,7 +208,12 @@ export interface DataState {
* The id used for the pending request, if there is one.
*/
readonly pendingRequestDatabaseDocumentID?: string;
readonly resolverComponentInstanceID: string | undefined;

/**
* An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
* Used to prevent collisions in things like query parameters.
*/
readonly resolverComponentInstanceID?: string;

/**
* The parameters and response from the last successful request.
Expand Down Expand Up @@ -510,8 +525,9 @@ export interface ResolverProps {
* Used as the origin of the Resolver graph.
*/
databaseDocumentID?: string;

/**
* A string literal describing where in the application resolver is located.
* An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
* Used to prevent collisions in things like query parameters.
*/
resolverComponentInstanceID: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { StyledBreadcrumbs } from './panel_content_utilities';

import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
import { CrumbInfo } from '../../types';
import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';

/**
* This view gives counts for all the related events of a process grouped by related event type.
Expand All @@ -27,11 +27,9 @@ import { CrumbInfo } from '../../types';
*/
export const EventCountsForProcess = memo(function EventCountsForProcess({
processEvent,
pushToQueryParams,
relatedStats,
}: {
processEvent: ResolverEvent;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
relatedStats: ResolverNodeStats;
}) {
interface EventCountsTableView {
Expand Down Expand Up @@ -62,6 +60,7 @@ export const EventCountsForProcess = memo(function EventCountsForProcess({
defaultMessage: 'Events',
}
);
const pushToQueryParams = useReplaceBreadcrumbParameters();
const crumbs = useMemo(() => {
return [
{
Expand Down
Loading

0 comments on commit 3fc3f58

Please sign in to comment.