[Resolver] model location.search in redux (#76140)

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.
This commit is contained in:
Robert Austin 2020-08-28 08:53:04 -04:00 committed by GitHub
parent 712c4bdde7
commit 3fc3f58c62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 271 additions and 179 deletions

View file

@ -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,
};
}

View file

@ -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 = export type ResolverAction =
| CameraAction | CameraAction
| DataAction | DataAction
| AppReceivedNewExternalProperties
| UserBroughtProcessIntoView | UserBroughtProcessIntoView
| UserFocusedOnResolverNode | UserFocusedOnResolverNode
| UserSelectedResolverNode | UserSelectedResolverNode

View file

@ -60,30 +60,10 @@ interface ServerReturnedRelatedEventData {
readonly payload: ResolverRelatedEvents; 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 = export type DataAction =
| ServerReturnedResolverData | ServerReturnedResolverData
| ServerFailedToReturnResolverData | ServerFailedToReturnResolverData
| ServerFailedToReturnRelatedEventData | ServerFailedToReturnRelatedEventData
| ServerReturnedRelatedEventData | ServerReturnedRelatedEventData
| AppReceivedNewExternalProperties
| AppRequestedResolverData | AppRequestedResolverData
| AppAbortedResolverDataRequest; | AppAbortedResolverDataRequest;

View file

@ -6,8 +6,8 @@
import * as selectors from './selectors'; import * as selectors from './selectors';
import { DataState } from '../../types'; import { DataState } from '../../types';
import { ResolverAction } from '../actions';
import { dataReducer } from './reducer'; import { dataReducer } from './reducer';
import { DataAction } from './action';
import { createStore } from 'redux'; import { createStore } from 'redux';
import { import {
mockTreeWithNoAncestorsAnd2Children, mockTreeWithNoAncestorsAnd2Children,
@ -20,7 +20,7 @@ import { uniquePidForProcess } from '../../models/process_event';
import { EndpointEvent } from '../../../../common/endpoint/types'; import { EndpointEvent } from '../../../../common/endpoint/types';
describe('data state', () => { describe('data state', () => {
let actions: DataAction[] = []; let actions: ResolverAction[] = [];
/** /**
* Get state, given an ordered collection of actions. * Get state, given an ordered collection of actions.
@ -68,7 +68,13 @@ describe('data state', () => {
actions = [ actions = [
{ {
type: 'appReceivedNewExternalProperties', type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID }, payload: {
databaseDocumentID,
resolverComponentInstanceID,
// `locationSearch` doesn't matter for this test
locationSearch: '',
},
}, },
]; ];
}); });
@ -120,7 +126,13 @@ describe('data state', () => {
actions = [ actions = [
{ {
type: 'appReceivedNewExternalProperties', type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID }, payload: {
databaseDocumentID,
resolverComponentInstanceID,
// `locationSearch` doesn't matter for this test
locationSearch: '',
},
}, },
{ {
type: 'appRequestedResolverData', type: 'appRequestedResolverData',
@ -182,6 +194,8 @@ describe('data state', () => {
payload: { payload: {
databaseDocumentID: firstDatabaseDocumentID, databaseDocumentID: firstDatabaseDocumentID,
resolverComponentInstanceID: resolverComponentInstanceID1, resolverComponentInstanceID: resolverComponentInstanceID1,
// `locationSearch` doesn't matter for this test
locationSearch: '',
}, },
}, },
// this happens when the middleware starts the request // this happens when the middleware starts the request
@ -195,6 +209,8 @@ describe('data state', () => {
payload: { payload: {
databaseDocumentID: secondDatabaseDocumentID, databaseDocumentID: secondDatabaseDocumentID,
resolverComponentInstanceID: resolverComponentInstanceID2, resolverComponentInstanceID: resolverComponentInstanceID2,
// `locationSearch` doesn't matter for this test
locationSearch: '',
}, },
}, },
]; ];

View file

@ -48,6 +48,13 @@ const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
selectedNode: nodeID, selectedNode: nodeID,
}; };
return next; return next;
} else if (action.type === 'appReceivedNewExternalProperties') {
const next: ResolverUIState = {
...state,
locationSearch: action.payload.locationSearch,
resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
};
return next;
} else { } else {
return state; return state;
} }

View file

@ -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 * 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. * concern-specific selector. `selector` should return the concern-specific state.

View file

@ -1,30 +1,50 @@
/* /*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ResolverUIState } from '../../types'; import { ResolverUIState } from '../../types';
import * as locationSearchModel from '../../models/location_search';
/**
* id of the "current" tree node (fake-focused) /**
*/ * id of the "current" tree node (fake-focused)
export const ariaActiveDescendant = createSelector( */
(uiState: ResolverUIState) => uiState, export const ariaActiveDescendant = createSelector(
/* eslint-disable no-shadow */ (uiState: ResolverUIState) => uiState,
({ ariaActiveDescendant }) => { /* eslint-disable no-shadow */
return ariaActiveDescendant; ({ ariaActiveDescendant }) => {
} return ariaActiveDescendant;
); }
);
/**
* id of the currently "selected" tree node /**
*/ * id of the currently "selected" tree node
export const selectedNode = createSelector( */
(uiState: ResolverUIState) => uiState, export const selectedNode = createSelector(
/* eslint-disable no-shadow */ (uiState: ResolverUIState) => uiState,
({ selectedNode }: ResolverUIState) => { /* eslint-disable no-shadow */
return selectedNode; ({ 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);
}
);

View file

@ -50,6 +50,16 @@ export interface ResolverUIState {
* `nodeID` of the selected node * `nodeID` of the selected node
*/ */
readonly selectedNode: string | null; 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;
} }
/** /**
@ -198,7 +208,12 @@ export interface DataState {
* The id used for the pending request, if there is one. * The id used for the pending request, if there is one.
*/ */
readonly pendingRequestDatabaseDocumentID?: string; 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. * The parameters and response from the last successful request.
@ -510,8 +525,9 @@ export interface ResolverProps {
* Used as the origin of the Resolver graph. * Used as the origin of the Resolver graph.
*/ */
databaseDocumentID?: string; 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. * Used to prevent collisions in things like query parameters.
*/ */
resolverComponentInstanceID: string; resolverComponentInstanceID: string;

View file

@ -12,7 +12,7 @@ import { StyledBreadcrumbs } from './panel_content_utilities';
import * as event from '../../../../common/endpoint/models/event'; import * as event from '../../../../common/endpoint/models/event';
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; 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. * This view gives counts for all the related events of a process grouped by related event type.
@ -27,11 +27,9 @@ import { CrumbInfo } from '../../types';
*/ */
export const EventCountsForProcess = memo(function EventCountsForProcess({ export const EventCountsForProcess = memo(function EventCountsForProcess({
processEvent, processEvent,
pushToQueryParams,
relatedStats, relatedStats,
}: { }: {
processEvent: ResolverEvent; processEvent: ResolverEvent;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
relatedStats: ResolverNodeStats; relatedStats: ResolverNodeStats;
}) { }) {
interface EventCountsTableView { interface EventCountsTableView {
@ -62,6 +60,7 @@ export const EventCountsForProcess = memo(function EventCountsForProcess({
defaultMessage: 'Events', defaultMessage: 'Events',
} }
); );
const pushToQueryParams = useReplaceBreadcrumbParameters();
const crumbs = useMemo(() => { const crumbs = useMemo(() => {
return [ return [
{ {

View file

@ -17,7 +17,6 @@ import { EventCountsForProcess } from './event_counts_for_process';
import { ProcessDetails } from './process_details'; import { ProcessDetails } from './process_details';
import { ProcessListWithCounts } from './process_list_with_counts'; import { ProcessListWithCounts } from './process_list_with_counts';
import { RelatedEventDetail } from './related_event_detail'; import { RelatedEventDetail } from './related_event_detail';
import { useResolverQueryParams } from '../use_resolver_query_params';
/** /**
* The team decided to use this table to determine which breadcrumbs/view to display: * The team decided to use this table to determine which breadcrumbs/view to display:
@ -39,7 +38,7 @@ const PanelContent = memo(function PanelContent() {
const { timestamp } = useContext(SideEffectContext); const { timestamp } = useContext(SideEffectContext);
const { pushToQueryParams, queryParams } = useResolverQueryParams(); const queryParams = useSelector(selectors.breadcrumbParameters);
const graphableProcesses = useSelector(selectors.graphableProcesses); const graphableProcesses = useSelector(selectors.graphableProcesses);
const graphableProcessEntityIds = useMemo(() => { const graphableProcessEntityIds = useMemo(() => {
@ -164,16 +163,13 @@ const PanelContent = memo(function PanelContent() {
const panelInstance = useMemo(() => { const panelInstance = useMemo(() => {
if (panelToShow === 'processDetails') { if (panelToShow === 'processDetails') {
return ( return <ProcessDetails processEvent={uiSelectedEvent!} />;
<ProcessDetails processEvent={uiSelectedEvent!} pushToQueryParams={pushToQueryParams} />
);
} }
if (panelToShow === 'eventCountsForProcess') { if (panelToShow === 'eventCountsForProcess') {
return ( return (
<EventCountsForProcess <EventCountsForProcess
processEvent={uiSelectedEvent!} processEvent={uiSelectedEvent!}
pushToQueryParams={pushToQueryParams}
relatedStats={relatedStatsForIdFromParams!} relatedStats={relatedStatsForIdFromParams!}
/> />
); );
@ -183,7 +179,6 @@ const PanelContent = memo(function PanelContent() {
return ( return (
<ProcessEventList <ProcessEventList
processEvent={uiSelectedEvent!} processEvent={uiSelectedEvent!}
pushToQueryParams={pushToQueryParams}
relatedStats={relatedStatsForIdFromParams!} relatedStats={relatedStatsForIdFromParams!}
eventType={crumbEvent} eventType={crumbEvent}
/> />
@ -198,21 +193,13 @@ const PanelContent = memo(function PanelContent() {
<RelatedEventDetail <RelatedEventDetail
relatedEventId={crumbId} relatedEventId={crumbId}
parentEvent={uiSelectedEvent!} parentEvent={uiSelectedEvent!}
pushToQueryParams={pushToQueryParams}
countForParent={parentCount} countForParent={parentCount}
/> />
); );
} }
// The default 'Event List' / 'List of all processes' view // The default 'Event List' / 'List of all processes' view
return <ProcessListWithCounts pushToQueryParams={pushToQueryParams} />; return <ProcessListWithCounts />;
}, [ }, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow]);
uiSelectedEvent,
crumbEvent,
crumbId,
pushToQueryParams,
relatedStatsForIdFromParams,
panelToShow,
]);
return <>{panelInstance}</>; return <>{panelInstance}</>;
}); });

View file

@ -1,62 +1,61 @@
/* /*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui';
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { StyledBreadcrumbs } from './panel_content_utilities'; import { StyledBreadcrumbs } from './panel_content_utilities';
import { CrumbInfo } from '../../types'; import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';
/** /**
* Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state. * Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state.
* *
* @param {function} pushToQueryparams A function to update the hash value in the URL to control panel state * @param {function} pushToQueryparams A function to update the hash value in the URL to control panel state
* @param {string} translatedErrorMessage The message to display in the panel when something goes wrong * @param {string} translatedErrorMessage The message to display in the panel when something goes wrong
*/ */
export const PanelContentError = memo(function ({ export const PanelContentError = memo(function ({
translatedErrorMessage, translatedErrorMessage,
pushToQueryParams, }: {
}: { translatedErrorMessage: string;
translatedErrorMessage: string; }) {
pushToQueryParams: (arg0: CrumbInfo) => unknown; const pushToQueryParams = useReplaceBreadcrumbParameters();
}) { const crumbs = useMemo(() => {
const crumbs = useMemo(() => { return [
return [ {
{ text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.events', {
text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.events', { defaultMessage: 'Events',
defaultMessage: 'Events', }),
}), onClick: () => {
onClick: () => { pushToQueryParams({ crumbId: '', crumbEvent: '' });
pushToQueryParams({ crumbId: '', crumbEvent: '' }); },
}, },
}, {
{ text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.error', {
text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.error', { defaultMessage: 'Error',
defaultMessage: 'Error', }),
}), onClick: () => {},
onClick: () => {}, },
}, ];
]; }, [pushToQueryParams]);
}, [pushToQueryParams]); return (
return ( <>
<> <StyledBreadcrumbs breadcrumbs={crumbs} />
<StyledBreadcrumbs breadcrumbs={crumbs} /> <EuiSpacer size="l" />
<EuiSpacer size="l" /> <EuiText textAlign="center">{translatedErrorMessage}</EuiText>
<EuiText textAlign="center">{translatedErrorMessage}</EuiText> <EuiSpacer size="l" />
<EuiSpacer size="l" /> <EuiButtonEmpty
<EuiButtonEmpty onClick={() => {
onClick={() => { pushToQueryParams({ crumbId: '', crumbEvent: '' });
pushToQueryParams({ crumbId: '', crumbEvent: '' }); }}
}} >
> {i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.goBack', {
{i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.goBack', { defaultMessage: 'Click this link to return to the list of all processes.',
defaultMessage: 'Click this link to return to the list of all processes.', })}
})} </EuiButtonEmpty>
</EuiButtonEmpty> </>
</> );
); });
}); PanelContentError.displayName = 'TableServiceError';
PanelContentError.displayName = 'TableServiceError';

View file

@ -31,7 +31,8 @@ import {
import { CubeForProcess } from './cube_for_process'; import { CubeForProcess } from './cube_for_process';
import { ResolverEvent } from '../../../../common/endpoint/types'; import { ResolverEvent } from '../../../../common/endpoint/types';
import { useResolverTheme } from '../assets'; import { useResolverTheme } from '../assets';
import { CrumbInfo, ResolverState } from '../../types'; import { ResolverState } from '../../types';
import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';
const StyledDescriptionList = styled(EuiDescriptionList)` const StyledDescriptionList = styled(EuiDescriptionList)`
&.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title {
@ -49,10 +50,8 @@ const StyledTitle = styled('h4')`
*/ */
export const ProcessDetails = memo(function ProcessDetails({ export const ProcessDetails = memo(function ProcessDetails({
processEvent, processEvent,
pushToQueryParams,
}: { }: {
processEvent: ResolverEvent; processEvent: ResolverEvent;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
}) { }) {
const processName = event.eventName(processEvent); const processName = event.eventName(processEvent);
const entityId = event.entityId(processEvent); const entityId = event.entityId(processEvent);
@ -127,6 +126,8 @@ export const ProcessDetails = memo(function ProcessDetails({
return processDescriptionListData; return processDescriptionListData;
}, [processEvent]); }, [processEvent]);
const pushToQueryParams = useReplaceBreadcrumbParameters();
const crumbs = useMemo(() => { const crumbs = useMemo(() => {
return [ return [
{ {

View file

@ -16,7 +16,7 @@ import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/ty
import * as selectors from '../../store/selectors'; import * as selectors from '../../store/selectors';
import { useResolverDispatch } from '../use_resolver_dispatch'; import { useResolverDispatch } from '../use_resolver_dispatch';
import { RelatedEventLimitWarning } from '../limit_warnings'; import { RelatedEventLimitWarning } from '../limit_warnings';
import { CrumbInfo } from '../../types'; import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';
/** /**
* This view presents a list of related events of a given type for a given process. * This view presents a list of related events of a given type for a given process.
@ -129,10 +129,8 @@ export const ProcessEventList = memo(function ProcessEventList({
processEvent, processEvent,
eventType, eventType,
relatedStats, relatedStats,
pushToQueryParams,
}: { }: {
processEvent: ResolverEvent; processEvent: ResolverEvent;
pushToQueryParams: (arg0: CrumbInfo) => unknown;
eventType: string; eventType: string;
relatedStats: ResolverNodeStats; relatedStats: ResolverNodeStats;
}) { }) {
@ -169,6 +167,8 @@ export const ProcessEventList = memo(function ProcessEventList({
} }
}, [relatedsReady, dispatch, processEntityId]); }, [relatedsReady, dispatch, processEntityId]);
const pushToQueryParams = useReplaceBreadcrumbParameters();
const waitCrumbs = useMemo(() => { const waitCrumbs = useMemo(() => {
return [ return [
{ {

View file

@ -3,6 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
/* eslint-disable react/display-name */
import React, { memo, useContext, useCallback, useMemo } from 'react'; import React, { memo, useContext, useCallback, useMemo } from 'react';
import { import {
EuiBasicTableColumn, EuiBasicTableColumn,
@ -22,7 +25,7 @@ import { SideEffectContext } from '../side_effect_context';
import { CubeForProcess } from './cube_for_process'; import { CubeForProcess } from './cube_for_process';
import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { LimitWarning } from '../limit_warnings'; import { LimitWarning } from '../limit_warnings';
import { CrumbInfo } from '../../types'; import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';
const StyledLimitWarning = styled(LimitWarning)` const StyledLimitWarning = styled(LimitWarning)`
flex-flow: row wrap; flex-flow: row wrap;
@ -46,14 +49,8 @@ const StyledLimitWarning = styled(LimitWarning)`
/** /**
* The "default" view for the panel: A list of all the processes currently in the graph. * The "default" view for the panel: A list of all the processes currently in the graph.
*
* @param {function} pushToQueryparams A function to update the hash value in the URL to control panel state
*/ */
export const ProcessListWithCounts = memo(function ProcessListWithCounts({ export const ProcessListWithCounts = memo(() => {
pushToQueryParams,
}: {
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
}) {
interface ProcessTableView { interface ProcessTableView {
name?: string; name?: string;
timestamp?: Date; timestamp?: Date;
@ -63,6 +60,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({
const dispatch = useResolverDispatch(); const dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext); const { timestamp } = useContext(SideEffectContext);
const isProcessTerminated = useSelector(selectors.isProcessTerminated); const isProcessTerminated = useSelector(selectors.isProcessTerminated);
const pushToQueryParams = useReplaceBreadcrumbParameters();
const handleBringIntoViewClick = useCallback( const handleBringIntoViewClick = useCallback(
(processTableViewItem) => { (processTableViewItem) => {
dispatch({ dispatch({

View file

@ -16,7 +16,8 @@ import { ResolverEvent } from '../../../../common/endpoint/types';
import * as selectors from '../../store/selectors'; import * as selectors from '../../store/selectors';
import { useResolverDispatch } from '../use_resolver_dispatch'; import { useResolverDispatch } from '../use_resolver_dispatch';
import { PanelContentError } from './panel_content_error'; import { PanelContentError } from './panel_content_error';
import { CrumbInfo, ResolverState } from '../../types'; import { ResolverState } from '../../types';
import { useReplaceBreadcrumbParameters } from '../use_replace_breadcrumb_parameters';
// Adding some styles to prevent horizontal scrollbars, per request from UX review // Adding some styles to prevent horizontal scrollbars, per request from UX review
const StyledDescriptionList = memo(styled(EuiDescriptionList)` const StyledDescriptionList = memo(styled(EuiDescriptionList)`
@ -76,15 +77,13 @@ function entriesForDisplay(entries: Array<{ title: string; description: string }
* This view presents a detailed view of all the available data for a related event, split and titled by the "section" * This view presents a detailed view of all the available data for a related event, split and titled by the "section"
* it appears in the underlying ResolverEvent * it appears in the underlying ResolverEvent
*/ */
export const RelatedEventDetail = memo(function RelatedEventDetail({ export const RelatedEventDetail = memo(function ({
relatedEventId, relatedEventId,
parentEvent, parentEvent,
pushToQueryParams,
countForParent, countForParent,
}: { }: {
relatedEventId: string; relatedEventId: string;
parentEvent: ResolverEvent; parentEvent: ResolverEvent;
pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
countForParent: number | undefined; countForParent: number | undefined;
}) { }) {
const processName = (parentEvent && event.eventName(parentEvent)) || '*'; const processName = (parentEvent && event.eventName(parentEvent)) || '*';
@ -130,6 +129,8 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(processEntityId, relatedEventId) selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(processEntityId, relatedEventId)
); );
const pushToQueryParams = useReplaceBreadcrumbParameters();
const waitCrumbs = useMemo(() => { const waitCrumbs = useMemo(() => {
return [ return [
{ {
@ -247,9 +248,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
defaultMessage: 'Related event not found.', defaultMessage: 'Related event not found.',
} }
); );
return ( return <PanelContentError translatedErrorMessage={errString} />;
<PanelContentError translatedErrorMessage={errString} pushToQueryParams={pushToQueryParams} />
);
} }
return ( return (

View file

@ -18,7 +18,7 @@ import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types
import { useResolverDispatch } from './use_resolver_dispatch'; import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event'; import * as eventModel from '../../../common/endpoint/models/event';
import * as selectors from '../store/selectors'; import * as selectors from '../store/selectors';
import { useResolverQueryParams } from './use_resolver_query_params'; import { useReplaceBreadcrumbParameters } from './use_replace_breadcrumb_parameters';
interface StyledActionsContainer { interface StyledActionsContainer {
readonly color: string; readonly color: string;
@ -242,7 +242,7 @@ const UnstyledProcessEventDot = React.memo(
}); });
}, [dispatch, nodeID]); }, [dispatch, nodeID]);
const { pushToQueryParams } = useResolverQueryParams(); const pushToQueryParams = useReplaceBreadcrumbParameters();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (animationTarget.current?.beginElement) { if (animationTarget.current?.beginElement) {

View file

@ -4,12 +4,17 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { useQueryStringKeys } from './use_query_string_keys'; import { useQueryStringKeys } from './use_query_string_keys';
import { CrumbInfo } from '../types'; import { CrumbInfo } from '../types';
export function useResolverQueryParams() { /**
* @deprecated
* Update the browser's `search` with data from `queryStringState`. The URL search parameter names
* will include Resolver's `resolverComponentInstanceID`.
*/
export function useReplaceBreadcrumbParameters(): (queryStringState: CrumbInfo) => void {
/** /**
* This updates the breadcrumb nav and the panel view. It's supplied to each * This updates the breadcrumb nav and the panel view. It's supplied to each
* panel content view to allow them to dispatch transitions to each other. * panel content view to allow them to dispatch transitions to each other.
@ -17,7 +22,7 @@ export function useResolverQueryParams() {
const history = useHistory(); const history = useHistory();
const urlSearch = useLocation().search; const urlSearch = useLocation().search;
const { idKey, eventKey } = useQueryStringKeys(); const { idKey, eventKey } = useQueryStringKeys();
const pushToQueryParams = useCallback( return useCallback(
(queryStringState: CrumbInfo) => { (queryStringState: CrumbInfo) => {
const urlSearchParams = new URLSearchParams(urlSearch); const urlSearchParams = new URLSearchParams(urlSearch);
@ -39,17 +44,4 @@ export function useResolverQueryParams() {
}, },
[history, urlSearch, idKey, eventKey] [history, urlSearch, idKey, eventKey]
); );
const queryParams: CrumbInfo = useMemo(() => {
const urlSearchParams = new URLSearchParams(urlSearch);
return {
// Use `''` for backwards compatibility with deprecated code.
crumbEvent: urlSearchParams.get(eventKey) ?? '',
crumbId: urlSearchParams.get(idKey) ?? '',
};
}, [urlSearch, idKey, eventKey]);
return {
pushToQueryParams,
queryParams,
};
} }

View file

@ -5,6 +5,7 @@
*/ */
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useResolverDispatch } from './use_resolver_dispatch'; import { useResolverDispatch } from './use_resolver_dispatch';
/** /**
@ -22,10 +23,11 @@ export function useStateSyncingActions({
resolverComponentInstanceID: string; resolverComponentInstanceID: string;
}) { }) {
const dispatch = useResolverDispatch(); const dispatch = useResolverDispatch();
const locationSearch = useLocation().search;
useLayoutEffect(() => { useLayoutEffect(() => {
dispatch({ dispatch({
type: 'appReceivedNewExternalProperties', type: 'appReceivedNewExternalProperties',
payload: { databaseDocumentID, resolverComponentInstanceID }, payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch },
}); });
}, [dispatch, databaseDocumentID, resolverComponentInstanceID]); }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch]);
} }