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

[Security Solution] Add hook for reading/writing resolver query params #70809

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion x-pack/plugins/security_solution/public/resolver/view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
export const Resolver = React.memo(function ({
className,
databaseDocumentID,
documentLocation,
}: {
/**
* Used by `styled-components`.
Expand All @@ -28,6 +29,11 @@ export const Resolver = React.memo(function ({
* Used as the origin of the Resolver graph.
*/
databaseDocumentID?: string;
/**
* A string literal describing where in the app resolver is located,
* used to prevent collisions in things like query params
*/
documentLocation: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

please add doc comment

Copy link
Contributor

Choose a reason for hiding this comment

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

❔ defer for now, but something like Pick<TimelineModel,'id'> might be a little more descriptive/helpful here.

Copy link
Contributor

Choose a reason for hiding this comment

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

...but maybe also too narrow, carry on actually.

}) {
const context = useKibana<StartServices>();
const store = useMemo(() => {
Expand All @@ -40,7 +46,11 @@ export const Resolver = React.memo(function ({
*/
return (
<Provider store={store}>
<ResolverMap className={className} databaseDocumentID={databaseDocumentID} />
<ResolverMap
className={className}
databaseDocumentID={databaseDocumentID}
documentLocation={documentLocation}
/>
</Provider>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { SideEffectContext } from './side_effect_context';
export const ResolverMap = React.memo(function ({
className,
databaseDocumentID,
documentLocation,
}: {
/**
* Used by `styled-components`.
Expand All @@ -39,6 +40,7 @@ export const ResolverMap = React.memo(function ({
* Used as the origin of the Resolver graph.
*/
databaseDocumentID?: string;
documentLocation: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

please add doc comment

}) {
/**
* This is responsible for dispatching actions that include any external data.
Expand Down Expand Up @@ -111,13 +113,14 @@ export const ResolverMap = React.memo(function ({
relatedEventsStats ? relatedEventsStats.get(entityId(processEvent)) : undefined
}
isProcessTerminated={terminatedProcesses.has(processEntityId)}
documentLocation={documentLocation}
isProcessOrigin={false}
/>
);
})}
</GraphContainer>
)}
<StyledPanel />
<StyledPanel documentLocation={documentLocation} />
<GraphControls />
<SymbolDefinitions />
</StyledMapContainer>
Expand Down
59 changes: 17 additions & 42 deletions x-pack/plugins/security_solution/public/resolver/view/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { memo, useCallback, useMemo, useContext, useLayoutEffect, useState } from 'react';
import React, { memo, useMemo, useContext, useLayoutEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { EuiPanel } from '@elastic/eui';
import { displayNameRecord } from './process_event_dot';
import * as selectors from '../store/selectors';
Expand All @@ -21,7 +18,7 @@ import { EventCountsForProcess } from './panels/panel_content_related_counts';
import { ProcessDetails } from './panels/panel_content_process_detail';
import { ProcessListWithCounts } from './panels/panel_content_process_list';
import { RelatedEventDetail } from './panels/panel_content_related_detail';
import { CrumbInfo } from './panels/panel_content_utilities';
import { useResolverQueryParams } from './use_resolver_query_params';

/**
* The team decided to use this table to determine which breadcrumbs/view to display:
Expand All @@ -38,15 +35,16 @@ import { CrumbInfo } from './panels/panel_content_utilities';
*
* @returns {JSX.Element} The "right" table content to show based on the query params as described above
*/
const PanelContent = memo(function PanelContent() {
const history = useHistory();
const urlSearch = useLocation().search;
const PanelContent = memo(function PanelContent({
documentLocation,
}: {
documentLocation: string;
}) {
const dispatch = useResolverDispatch();

const { timestamp } = useContext(SideEffectContext);
const queryParams: CrumbInfo = useMemo(() => {
return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) };
}, [urlSearch]);

const { pushToQueryParams, queryParams } = useResolverQueryParams(documentLocation);

const graphableProcesses = useSelector(selectors.graphableProcesses);
const graphableProcessEntityIds = useMemo(() => {
Expand Down Expand Up @@ -104,35 +102,6 @@ const PanelContent = memo(function PanelContent() {
}
}, [dispatch, uiSelectedEvent, paramsSelectedEvent, lastUpdatedProcess, timestamp]);

/**
* 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.
*/
const pushToQueryParams = useCallback(
(newCrumbs: CrumbInfo) => {
// Construct a new set of params from the current set (minus empty params)
// by assigning the new set of params provided in `newCrumbs`
const crumbsToPass = {
...querystring.parse(urlSearch.slice(1)),
...newCrumbs,
};

// If either was passed in as empty, remove it from the record
if (crumbsToPass.crumbId === '') {
delete crumbsToPass.crumbId;
}
if (crumbsToPass.crumbEvent === '') {
delete crumbsToPass.crumbEvent;
}

const relativeURL = { search: querystring.stringify(crumbsToPass) };
// We probably don't want to nuke the user's history with a huge
// trail of these, thus `.replace` instead of `.push`
return history.replace(relativeURL);
},
[history, urlSearch]
);

const relatedEventStats = useSelector(selectors.relatedEventsStats);
const { crumbId, crumbEvent } = queryParams;
const relatedStatsForIdFromParams: ResolverNodeStats | undefined =
Expand Down Expand Up @@ -269,10 +238,16 @@ const PanelContent = memo(function PanelContent() {
});
PanelContent.displayName = 'PanelContent';

export const Panel = memo(function Event({ className }: { className?: string }) {
export const Panel = memo(function Event({
className,
documentLocation,
}: {
className?: string;
documentLocation: string;
}) {
return (
<EuiPanel className={className}>
<PanelContent />
<PanelContent documentLocation={documentLocation} />
</EuiPanel>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ const BetaHeader = styled(`header`)`
* The two query parameters we read/write on to control which view the table presents:
*/
export interface CrumbInfo {
readonly crumbId: string;
readonly crumbEvent: string;
readonly [x: string]: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

❔ (defer: do do not this until after merge if you decide to) At this point, you could probably just change it to a Record<string, string> for clarity.

}

const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { htmlIdGenerator, EuiButton, EuiI18nNumber, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { useSelector } from 'react-redux';
import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../models/vector2';
Expand All @@ -22,7 +19,7 @@ import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types
import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event';
import * as selectors from '../store/selectors';
import { CrumbInfo } from './panels/panel_content_utilities';
import { useResolverQueryParams } from './use_resolver_query_params';

/**
* A record of all known event types (in schema format) to translations
Expand Down Expand Up @@ -244,6 +241,7 @@ const UnstyledProcessEventDot = React.memo(
isProcessTerminated,
isProcessOrigin,
relatedEventsStatsForProcess,
documentLocation,
}: {
/**
* A `className` string provided by `styled`
Expand Down Expand Up @@ -279,6 +277,7 @@ const UnstyledProcessEventDot = React.memo(
* Statistics for the number of related events and alerts for this process node
*/
relatedEventsStatsForProcess?: ResolverNodeStats;
documentLocation: string;
}) => {
/**
* Convert the position, which is in 'world' coordinates, to screen coordinates.
Expand Down Expand Up @@ -403,35 +402,7 @@ const UnstyledProcessEventDot = React.memo(
});
}, [dispatch, selfId]);

const history = useHistory();
const urlSearch = history.location.search;

/**
* This updates the breadcrumb nav, the table view
*/
const pushToQueryParams = useCallback(
(newCrumbs: CrumbInfo) => {
// Construct a new set of params from the current set (minus empty params)
// by assigning the new set of params provided in `newCrumbs`
const crumbsToPass = {
...querystring.parse(urlSearch.slice(1)),
...newCrumbs,
};

// If either was passed in as empty, remove it from the record
if (crumbsToPass.crumbId === '') {
delete crumbsToPass.crumbId;
}
if (crumbsToPass.crumbEvent === '') {
delete crumbsToPass.crumbEvent;
}

const relativeURL = { search: querystring.stringify(crumbsToPass) };

return history.replace(relativeURL);
},
[history, urlSearch]
);
const { pushToQueryParams } = useResolverQueryParams(documentLocation);
Copy link
Contributor

Choose a reason for hiding this comment

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

ℹ️ very niiice


const handleClick = useCallback(() => {
if (animationTarget.current !== null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { useCallback, useMemo } from 'react';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { useHistory, useLocation } from 'react-router-dom';
import { CrumbInfo } from './panels/panel_content_utilities';

export function useResolverQueryParams(documentLocation: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of passing this around, put the documentLocation in the action dispatched by useStateSyncingActions, then use useSelector in this hook to get it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are there some cool things you could do by having it passed in as an argument like this, though? Just thinking in the future stuff like "pre-focusing" a Resolver by pushing to query params (I guess if you knew its document location) before you open it?

Copy link
Contributor

Choose a reason for hiding this comment

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

❔ My only question here would be if this is "passable" in a URL they way we envisioned. I think we put it in this way now regardless, but maybe we should test that someone on the same Kibana instance can always open the URL and restore/rehydrate context based on this.

/**
* 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.
*/
const history = useHistory();
const urlSearch = useLocation().search;
const uniqueCrumbIdKey = `${documentLocation}CrumbId`;
const uniqueCrumbEventKey = `${documentLocation}CrumbEvent`;
const pushToQueryParams = useCallback(
(newCrumbs: CrumbInfo) => {
// Construct a new set of params from the current set (minus empty params)
// by assigning the new set of params provided in `newCrumbs`
const crumbsToPass = {
...querystring.parse(urlSearch.slice(1)),
[uniqueCrumbIdKey]: newCrumbs.crumbId,
[uniqueCrumbEventKey]: newCrumbs.crumbEvent,
};

// If either was passed in as empty, remove it from the record
if (crumbsToPass.uniqueCrumbIdKey === '') {
delete crumbsToPass.uniqueCrumbIdKey;
}
if (crumbsToPass.uniqueCrumbEventKey === '') {
delete crumbsToPass.uniqueCrumbEventKey;
}

const relativeURL = { search: querystring.stringify(crumbsToPass) };
// We probably don't want to nuke the user's history with a huge
// trail of these, thus `.replace` instead of `.push`
return history.replace(relativeURL);
},
[history, urlSearch, uniqueCrumbIdKey, uniqueCrumbEventKey]
);
const queryParams: CrumbInfo = useMemo(() => {
const newParams = {
[uniqueCrumbIdKey]: '',
[uniqueCrumbEventKey]: '',
...querystring.parse(urlSearch.slice(1)),
};
newParams.crumbId = newParams[uniqueCrumbIdKey];
Copy link
Contributor

Choose a reason for hiding this comment

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

If you're setting crumbId and crumbEvent here, why is CrumbInfo typed with an index type?

newParams.crumbEvent = newParams[uniqueCrumbEventKey];
return newParams;
}, [urlSearch, uniqueCrumbIdKey, uniqueCrumbEventKey]);

return {
pushToQueryParams,
queryParams,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const GraphOverlayComponent = ({
</EuiFlexGroup>

<EuiHorizontalRule margin="none" />
<StyledResolver databaseDocumentID={graphEventId} />
<StyledResolver databaseDocumentID={graphEventId} documentLocation={currentTimeline.id} />
<AllCasesModal
onCloseCaseModal={onCloseCaseModal}
showCaseModal={showCaseModal}
Expand Down