mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Resolver] Origin process (#72382)
Co-authored-by: Brent Kimmel <brent.kimmel@elastic.co> * Center the origin node * Nodes appear selected when they are selected. also the aria attributes are working. * Reposition the submenu when the user pans.
This commit is contained in:
parent
bb7d12864f
commit
b930cef489
13 changed files with 214 additions and 139 deletions
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { IsometricTaxiLayout } from '../../types';
|
||||
import { LegacyEndpointEvent } from '../../../../common/endpoint/types';
|
||||
import { isometricTaxiLayout } from './isometric_taxi_layout';
|
||||
import { isometricTaxiLayoutFactory } from './isometric_taxi_layout';
|
||||
import { mockProcessEvent } from '../../models/process_event_test_helpers';
|
||||
import { factory } from './index';
|
||||
|
||||
|
@ -107,7 +107,7 @@ describe('resolver graph layout', () => {
|
|||
unique_ppid: 0,
|
||||
},
|
||||
});
|
||||
layout = () => isometricTaxiLayout(factory(events));
|
||||
layout = () => isometricTaxiLayoutFactory(factory(events));
|
||||
events = [];
|
||||
});
|
||||
describe('when rendering no nodes', () => {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as vector2 from '../../models/vector2';
|
||||
import {
|
||||
IndexedProcessTree,
|
||||
Vector2,
|
||||
|
@ -17,14 +16,17 @@ import {
|
|||
} from '../../types';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { ResolverEvent } from '../../../../common/endpoint/types';
|
||||
import * as model from './index';
|
||||
import * as vector2 from '../vector2';
|
||||
import * as indexedProcessTreeModel from './index';
|
||||
import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date';
|
||||
import { uniquePidForProcess } from '../process_event';
|
||||
|
||||
/**
|
||||
* Graph the process tree
|
||||
*/
|
||||
export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): IsometricTaxiLayout {
|
||||
export function isometricTaxiLayoutFactory(
|
||||
indexedProcessTree: IndexedProcessTree
|
||||
): IsometricTaxiLayout {
|
||||
/**
|
||||
* Walk the tree in reverse level order, calculating the 'width' of subtrees.
|
||||
*/
|
||||
|
@ -83,8 +85,8 @@ export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): Iso
|
|||
*/
|
||||
function ariaLevels(indexedProcessTree: IndexedProcessTree): Map<ResolverEvent, number> {
|
||||
const map: Map<ResolverEvent, number> = new Map();
|
||||
for (const node of model.levelOrder(indexedProcessTree)) {
|
||||
const parentNode = model.parent(indexedProcessTree, node);
|
||||
for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) {
|
||||
const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node);
|
||||
if (parentNode === undefined) {
|
||||
// nodes at the root have a level of 1
|
||||
map.set(node, 1);
|
||||
|
@ -143,16 +145,19 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map<ResolverEvent,
|
|||
function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths {
|
||||
const widths = new Map<ResolverEvent, number>();
|
||||
|
||||
if (model.size(indexedProcessTree) === 0) {
|
||||
if (indexedProcessTreeModel.size(indexedProcessTree) === 0) {
|
||||
return widths;
|
||||
}
|
||||
|
||||
const processesInReverseLevelOrder: ResolverEvent[] = [
|
||||
...model.levelOrder(indexedProcessTree),
|
||||
...indexedProcessTreeModel.levelOrder(indexedProcessTree),
|
||||
].reverse();
|
||||
|
||||
for (const process of processesInReverseLevelOrder) {
|
||||
const children = model.children(indexedProcessTree, uniquePidForProcess(process));
|
||||
const children = indexedProcessTreeModel.children(
|
||||
indexedProcessTree,
|
||||
uniquePidForProcess(process)
|
||||
);
|
||||
|
||||
const sumOfWidthOfChildren = function sumOfWidthOfChildren() {
|
||||
return children.reduce(function sum(currentValue, child) {
|
||||
|
@ -229,7 +234,10 @@ function processEdgeLineSegments(
|
|||
metadata: edgeLineMetadata,
|
||||
};
|
||||
|
||||
const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent));
|
||||
const siblings = indexedProcessTreeModel.children(
|
||||
indexedProcessTree,
|
||||
uniquePidForProcess(parent)
|
||||
);
|
||||
const isFirstChild = process === siblings[0];
|
||||
|
||||
if (metadata.isOnlyChild) {
|
||||
|
@ -384,8 +392,8 @@ function* levelOrderWithWidths(
|
|||
tree: IndexedProcessTree,
|
||||
widths: ProcessWidths
|
||||
): Iterable<ProcessWithWidthMetadata> {
|
||||
for (const process of model.levelOrder(tree)) {
|
||||
const parent = model.parent(tree, process);
|
||||
for (const process of indexedProcessTreeModel.levelOrder(tree)) {
|
||||
const parent = indexedProcessTreeModel.parent(tree, process);
|
||||
const width = widths.get(process);
|
||||
|
||||
if (width === undefined) {
|
||||
|
@ -423,7 +431,7 @@ function* levelOrderWithWidths(
|
|||
parentWidth,
|
||||
};
|
||||
|
||||
const siblings = model.children(tree, uniquePidForProcess(parent));
|
||||
const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent));
|
||||
if (siblings.length === 1) {
|
||||
metadata.isOnlyChild = true;
|
||||
metadata.lastChildWidth = width;
|
||||
|
@ -479,3 +487,32 @@ const distanceBetweenNodesInUnits = 2;
|
|||
* The distance in pixels (at scale 1) between nodes. Change this to space out nodes more
|
||||
*/
|
||||
const distanceBetweenNodes = distanceBetweenNodesInUnits * unit;
|
||||
|
||||
export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined {
|
||||
return model.processNodePositions.get(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a clone of `model` with all positions incremented by `translation`.
|
||||
* Use this to move the layout around.
|
||||
* e.g.
|
||||
* ```
|
||||
* translated(layout, [100, -200]) // return a copy of `layout`, thats been moved 100 to the right and 200 up
|
||||
* ```
|
||||
*/
|
||||
export function translated(model: IsometricTaxiLayout, translation: Vector2): IsometricTaxiLayout {
|
||||
return {
|
||||
processNodePositions: new Map(
|
||||
[...model.processNodePositions.entries()].map(([node, position]) => [
|
||||
node,
|
||||
vector2.add(position, translation),
|
||||
])
|
||||
),
|
||||
edgeLineSegments: model.edgeLineSegments.map(({ points, metadata }) => ({
|
||||
points: points.map((point) => vector2.add(point, translation)),
|
||||
metadata,
|
||||
})),
|
||||
// these are unchanged
|
||||
ariaLevels: model.ariaLevels,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -69,12 +69,9 @@ interface AppDetectedMissingEventData {
|
|||
*/
|
||||
interface UserFocusedOnResolverNode {
|
||||
readonly type: 'userFocusedOnResolverNode';
|
||||
readonly payload: {
|
||||
/**
|
||||
* Used to identify the process node that the user focused on (in the DOM)
|
||||
*/
|
||||
readonly nodeId: string;
|
||||
};
|
||||
|
||||
/** focused nodeID */
|
||||
readonly payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,16 +82,10 @@ interface UserFocusedOnResolverNode {
|
|||
*/
|
||||
interface UserSelectedResolverNode {
|
||||
readonly type: 'userSelectedResolverNode';
|
||||
readonly payload: {
|
||||
/**
|
||||
* The HTML ID used to identify the process node's element that the user selected
|
||||
*/
|
||||
readonly nodeId: string;
|
||||
/**
|
||||
* The process entity_id for the process the node represents
|
||||
*/
|
||||
readonly selectedProcessId: string;
|
||||
};
|
||||
/**
|
||||
* The nodeID (aka entity_id) that was select.
|
||||
*/
|
||||
readonly payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,8 +30,9 @@ import {
|
|||
ResolverRelatedEvents,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import * as resolverTreeModel from '../../models/resolver_tree';
|
||||
import { isometricTaxiLayout } from '../../models/indexed_process_tree/isometric_taxi_layout';
|
||||
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
|
||||
import { allEventCategories } from '../../../../common/endpoint/models/event';
|
||||
import * as vector2 from '../../models/vector2';
|
||||
|
||||
/**
|
||||
* If there is currently a request.
|
||||
|
@ -70,6 +71,21 @@ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* the node ID of the node representing the databaseDocumentID.
|
||||
* NB: this could be stale if the last response is stale
|
||||
*/
|
||||
export const originID: (state: DataState) => string | undefined = createSelector(
|
||||
resolverTreeResponse,
|
||||
function (resolverTree?) {
|
||||
if (resolverTree) {
|
||||
// This holds the entityID (aka nodeID) of the node related to the last fetched `_id`
|
||||
return resolverTree.entityID;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Process events that will be displayed as terminated.
|
||||
*/
|
||||
|
@ -317,13 +333,45 @@ export function databaseDocumentIDToFetch(state: DataState): string | null {
|
|||
}
|
||||
}
|
||||
|
||||
export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments(
|
||||
/* eslint-disable no-shadow */
|
||||
indexedProcessTree
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return isometricTaxiLayout(indexedProcessTree);
|
||||
});
|
||||
export const layout = createSelector(
|
||||
tree,
|
||||
originID,
|
||||
function processNodePositionsAndEdgeLineSegments(
|
||||
/* eslint-disable no-shadow */
|
||||
indexedProcessTree,
|
||||
originID
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
// use the isometric taxi layout as a base
|
||||
const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree);
|
||||
|
||||
if (!originID) {
|
||||
// no data has loaded.
|
||||
return taxiLayout;
|
||||
}
|
||||
|
||||
// find the origin node
|
||||
const originNode = indexedProcessTreeModel.processEvent(indexedProcessTree, originID);
|
||||
|
||||
if (!originNode) {
|
||||
// this should only happen if the `ResolverTree` from the server has an entity ID with no matching lifecycle events.
|
||||
throw new Error('Origin node not found in ResolverTree');
|
||||
}
|
||||
|
||||
// Find the position of the origin, we'll center the map on it intrinsically
|
||||
const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, originNode);
|
||||
// adjust the position of everything so that the origin node is at `(0, 0)`
|
||||
|
||||
if (originPosition === undefined) {
|
||||
// not sure how this could happen.
|
||||
return taxiLayout;
|
||||
}
|
||||
|
||||
// Take the origin position, and multipy it by -1, then move the layout by that amount.
|
||||
// This should center the layout around the origin.
|
||||
return isometricTaxiLayoutModel.translated(taxiLayout, vector2.scale(originPosition, -1));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Given a nodeID (aka entity_id) get the indexed process event.
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
import { animateProcessIntoView } from './methods';
|
||||
import { cameraReducer } from './camera/reducer';
|
||||
import { dataReducer } from './data/reducer';
|
||||
|
@ -12,51 +11,38 @@ import { ResolverAction } from './actions';
|
|||
import { ResolverState, ResolverUIState } from '../types';
|
||||
import { uniquePidForProcess } from '../models/process_event';
|
||||
|
||||
/**
|
||||
* Despite the name "generator", this function is entirely determinant
|
||||
* (i.e. it will return the same html id given the same prefix 'resolverNode'
|
||||
* and nodeId)
|
||||
*/
|
||||
const resolverNodeIdGenerator = htmlIdGenerator('resolverNode');
|
||||
|
||||
const uiReducer: Reducer<ResolverUIState, ResolverAction> = (
|
||||
uiState = {
|
||||
activeDescendantId: null,
|
||||
selectedDescendantId: null,
|
||||
processEntityIdOfSelectedDescendant: null,
|
||||
state = {
|
||||
ariaActiveDescendant: null,
|
||||
selectedNode: null,
|
||||
},
|
||||
action
|
||||
) => {
|
||||
if (action.type === 'userFocusedOnResolverNode') {
|
||||
return {
|
||||
...uiState,
|
||||
activeDescendantId: action.payload.nodeId,
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
ariaActiveDescendant: action.payload,
|
||||
};
|
||||
return next;
|
||||
} else if (action.type === 'userSelectedResolverNode') {
|
||||
return {
|
||||
...uiState,
|
||||
selectedDescendantId: action.payload.nodeId,
|
||||
processEntityIdOfSelectedDescendant: action.payload.selectedProcessId,
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
selectedNode: action.payload,
|
||||
};
|
||||
return next;
|
||||
} else if (
|
||||
action.type === 'userBroughtProcessIntoView' ||
|
||||
action.type === 'appDetectedNewIdFromQueryParams'
|
||||
) {
|
||||
/**
|
||||
* This action has a process payload (instead of a processId), so we use
|
||||
* `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant
|
||||
* html id of the node being brought into view.
|
||||
*/
|
||||
const processEntityId = uniquePidForProcess(action.payload.process);
|
||||
const processNodeId = resolverNodeIdGenerator(processEntityId);
|
||||
return {
|
||||
...uiState,
|
||||
activeDescendantId: processNodeId,
|
||||
selectedDescendantId: processNodeId,
|
||||
processEntityIdOfSelectedDescendant: processEntityId,
|
||||
const nodeID = uniquePidForProcess(action.payload.process);
|
||||
const next: ResolverUIState = {
|
||||
...state,
|
||||
ariaActiveDescendant: nodeID,
|
||||
selectedNode: nodeID,
|
||||
};
|
||||
return next;
|
||||
} else {
|
||||
return uiState;
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -144,26 +144,15 @@ export const relatedEventInfoByEntityId = composeSelectors(
|
|||
/**
|
||||
* Returns the id of the "current" tree node (fake-focused)
|
||||
*/
|
||||
export const uiActiveDescendantId = composeSelectors(
|
||||
export const ariaActiveDescendant = composeSelectors(
|
||||
uiStateSelector,
|
||||
uiSelectors.activeDescendantId
|
||||
uiSelectors.ariaActiveDescendant
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components)
|
||||
* Returns the nodeID of the selected node
|
||||
*/
|
||||
export const uiSelectedDescendantId = composeSelectors(
|
||||
uiStateSelector,
|
||||
uiSelectors.selectedDescendantId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the entity_id of the "selected" tree node's process
|
||||
*/
|
||||
export const uiSelectedDescendantProcessId = composeSelectors(
|
||||
uiStateSelector,
|
||||
uiSelectors.selectedDescendantProcessId
|
||||
);
|
||||
export const selectedNode = composeSelectors(uiStateSelector, uiSelectors.selectedNode);
|
||||
|
||||
/**
|
||||
* Returns the camera state from within ResolverState
|
||||
|
@ -251,6 +240,14 @@ export const ariaLevel: (
|
|||
dataSelectors.ariaLevel
|
||||
);
|
||||
|
||||
/**
|
||||
* the node ID of the node representing the databaseDocumentID
|
||||
*/
|
||||
export const originID: (state: ResolverState) => string | undefined = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.originID
|
||||
);
|
||||
|
||||
/**
|
||||
* Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null
|
||||
* If the node has a flowto candidate that is currently visible, that will be returned, otherwise null.
|
||||
|
|
|
@ -10,32 +10,21 @@ import { ResolverUIState } from '../../types';
|
|||
/**
|
||||
* id of the "current" tree node (fake-focused)
|
||||
*/
|
||||
export const activeDescendantId = createSelector(
|
||||
export const ariaActiveDescendant = createSelector(
|
||||
(uiState: ResolverUIState) => uiState,
|
||||
/* eslint-disable no-shadow */
|
||||
({ activeDescendantId }) => {
|
||||
return activeDescendantId;
|
||||
({ ariaActiveDescendant }) => {
|
||||
return ariaActiveDescendant;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* id of the currently "selected" tree node
|
||||
*/
|
||||
export const selectedDescendantId = createSelector(
|
||||
export const selectedNode = createSelector(
|
||||
(uiState: ResolverUIState) => uiState,
|
||||
/* eslint-disable no-shadow */
|
||||
({ selectedDescendantId }) => {
|
||||
return selectedDescendantId;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* id of the currently "selected" tree node
|
||||
*/
|
||||
export const selectedDescendantProcessId = createSelector(
|
||||
(uiState: ResolverUIState) => uiState,
|
||||
/* eslint-disable no-shadow */
|
||||
({ processEntityIdOfSelectedDescendant }: ResolverUIState) => {
|
||||
return processEntityIdOfSelectedDescendant;
|
||||
({ selectedNode }: ResolverUIState) => {
|
||||
return selectedNode;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -34,17 +34,13 @@ export interface ResolverState {
|
|||
*/
|
||||
export interface ResolverUIState {
|
||||
/**
|
||||
* The ID attribute of the resolver's aria-activedescendent.
|
||||
* The nodeID for the process that is selected (in the aria-activedescendent sense of being selected.)
|
||||
*/
|
||||
readonly activeDescendantId: string | null;
|
||||
readonly ariaActiveDescendant: string | null;
|
||||
/**
|
||||
* The ID attribute of the resolver's currently selected descendant.
|
||||
* nodeID of the selected node
|
||||
*/
|
||||
readonly selectedDescendantId: string | null;
|
||||
/**
|
||||
* The entity_id of the process for the resolver's currently selected descendant.
|
||||
*/
|
||||
readonly processEntityIdOfSelectedDescendant: string | null;
|
||||
readonly selectedNode: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -422,7 +422,7 @@ const processTypeToCube: Record<ResolverProcessType, keyof NodeStyleMap> = {
|
|||
export const useResolverTheme = (): {
|
||||
colorMap: ColorMap;
|
||||
nodeAssets: NodeStyleMap;
|
||||
cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig;
|
||||
cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig;
|
||||
} => {
|
||||
const isDarkMode = useUiSetting<boolean>(DEFAULT_DARK_MODE);
|
||||
const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
|
||||
|
@ -497,10 +497,14 @@ export const useResolverTheme = (): {
|
|||
},
|
||||
};
|
||||
|
||||
function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) {
|
||||
function cubeAssetsForNode(isProcessTerminated: boolean, isProcessTrigger: boolean) {
|
||||
if (isProcessTerminated) {
|
||||
return nodeAssets[processTypeToCube.processTerminated];
|
||||
} else if (isProcessOrigin) {
|
||||
if (isProcessTrigger) {
|
||||
return nodeAssets.terminatedTriggerCube;
|
||||
} else {
|
||||
return nodeAssets[processTypeToCube.processTerminated];
|
||||
}
|
||||
} else if (isProcessTrigger) {
|
||||
return nodeAssets[processTypeToCube.processCausedAlert];
|
||||
} else {
|
||||
return nodeAssets[processTypeToCube.processRan];
|
||||
|
|
|
@ -64,7 +64,7 @@ export const ResolverMap = React.memo(function ({
|
|||
const { projectionMatrix, ref, onMouseDown } = useCamera();
|
||||
const isLoading = useSelector(selectors.isLoading);
|
||||
const hasError = useSelector(selectors.hasError);
|
||||
const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
|
||||
const activeDescendantId = useSelector(selectors.ariaActiveDescendant);
|
||||
const { colorMap } = useResolverTheme();
|
||||
|
||||
return (
|
||||
|
@ -110,7 +110,6 @@ export const ResolverMap = React.memo(function ({
|
|||
projectionMatrix={projectionMatrix}
|
||||
event={processEvent}
|
||||
isProcessTerminated={terminatedProcesses.has(processEntityId)}
|
||||
isProcessOrigin={false}
|
||||
timeAtRender={timeAtRender}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -60,10 +60,10 @@ const PanelContent = memo(function PanelContent() {
|
|||
// The "selected" node (and its corresponding event) in the tree control.
|
||||
// It may need to be synchronized with the ID indicated as selected via the `idFromParams`
|
||||
// memo above. When this is the case, it is handled by the layout effect below.
|
||||
const selectedDescendantProcessId = useSelector(selectors.uiSelectedDescendantProcessId);
|
||||
const selectedNode = useSelector(selectors.selectedNode);
|
||||
const uiSelectedEvent = useMemo(() => {
|
||||
return graphableProcesses.find((evt) => event.entityId(evt) === selectedDescendantProcessId);
|
||||
}, [graphableProcesses, selectedDescendantProcessId]);
|
||||
return graphableProcesses.find((evt) => event.entityId(evt) === selectedNode);
|
||||
}, [graphableProcesses, selectedNode]);
|
||||
|
||||
// Until an event is dispatched during update, the event indicated as selected by params may
|
||||
// be different than the one in state.
|
||||
|
|
|
@ -72,7 +72,6 @@ const UnstyledProcessEventDot = React.memo(
|
|||
event,
|
||||
projectionMatrix,
|
||||
isProcessTerminated,
|
||||
isProcessOrigin,
|
||||
timeAtRender,
|
||||
}: {
|
||||
/**
|
||||
|
@ -95,10 +94,6 @@ const UnstyledProcessEventDot = React.memo(
|
|||
* Whether or not to show the process as terminated.
|
||||
*/
|
||||
isProcessTerminated: boolean;
|
||||
/**
|
||||
* Whether or not to show the process as the originating event.
|
||||
*/
|
||||
isProcessOrigin: boolean;
|
||||
|
||||
/**
|
||||
* The time (unix epoch) at render.
|
||||
|
@ -117,8 +112,8 @@ const UnstyledProcessEventDot = React.memo(
|
|||
const [xScale] = projectionMatrix;
|
||||
|
||||
// Node (html id=) IDs
|
||||
const activeDescendantId = useSelector(selectors.uiActiveDescendantId);
|
||||
const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId);
|
||||
const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant);
|
||||
const selectedNode = useSelector(selectors.selectedNode);
|
||||
const nodeID = processEventModel.uniquePidForProcess(event);
|
||||
const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID);
|
||||
|
||||
|
@ -212,23 +207,26 @@ const UnstyledProcessEventDot = React.memo(
|
|||
isLabelFilled,
|
||||
labelButtonFill,
|
||||
strokeColor,
|
||||
} = cubeAssetsForNode(isProcessTerminated, isProcessOrigin);
|
||||
} = cubeAssetsForNode(
|
||||
isProcessTerminated,
|
||||
/**
|
||||
* There is no definition for 'trigger process' yet. return false.
|
||||
*/ false
|
||||
);
|
||||
|
||||
const labelHTMLID = htmlIdGenerator('resolver')(`${nodeID}:label`);
|
||||
|
||||
const isAriaCurrent = nodeID === activeDescendantId;
|
||||
const isAriaSelected = nodeID === selectedDescendantId;
|
||||
const isAriaCurrent = nodeID === ariaActiveDescendant;
|
||||
const isAriaSelected = nodeID === selectedNode;
|
||||
|
||||
const dispatch = useResolverDispatch();
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'userFocusedOnResolverNode',
|
||||
payload: {
|
||||
nodeId: nodeHTMLID(nodeID),
|
||||
},
|
||||
payload: nodeID,
|
||||
});
|
||||
}, [dispatch, nodeHTMLID, nodeID]);
|
||||
}, [dispatch, nodeID]);
|
||||
|
||||
const handleRelatedEventRequest = useCallback(() => {
|
||||
dispatch({
|
||||
|
@ -247,13 +245,10 @@ const UnstyledProcessEventDot = React.memo(
|
|||
}
|
||||
dispatch({
|
||||
type: 'userSelectedResolverNode',
|
||||
payload: {
|
||||
nodeId: nodeHTMLID(nodeID),
|
||||
selectedProcessId: nodeID,
|
||||
},
|
||||
payload: nodeID,
|
||||
});
|
||||
pushToQueryParams({ crumbId: nodeID, crumbEvent: '' });
|
||||
}, [animationTarget, dispatch, pushToQueryParams, nodeID, nodeHTMLID]);
|
||||
}, [animationTarget, dispatch, pushToQueryParams, nodeID]);
|
||||
|
||||
/**
|
||||
* Enumerates the stats for related events to display with the node as options,
|
||||
|
@ -422,6 +417,7 @@ const UnstyledProcessEventDot = React.memo(
|
|||
buttonFill={colorMap.resolverBackground}
|
||||
menuAction={handleRelatedEventRequest}
|
||||
menuTitle={subMenuAssets.relatedEvents.title}
|
||||
projectionMatrix={projectionMatrix}
|
||||
optionsWithActions={relatedEventStatusOrOptions}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { ReactNode, useState, useMemo, useCallback } from 'react';
|
||||
import React, { ReactNode, useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
|
||||
import {
|
||||
EuiI18nNumber,
|
||||
EuiSelectable,
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { Matrix3 } from '../types';
|
||||
|
||||
/**
|
||||
* i18n-translated titles for submenus and identifiers for display of states:
|
||||
|
@ -133,6 +134,7 @@ const NodeSubMenuComponents = React.memo(
|
|||
menuAction,
|
||||
optionsWithActions,
|
||||
className,
|
||||
projectionMatrix,
|
||||
}: {
|
||||
menuTitle: string;
|
||||
className?: string;
|
||||
|
@ -140,9 +142,16 @@ const NodeSubMenuComponents = React.memo(
|
|||
buttonBorderColor: ButtonColor;
|
||||
buttonFill: string;
|
||||
count?: number;
|
||||
/**
|
||||
* Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself.
|
||||
*/
|
||||
projectionMatrix: Matrix3;
|
||||
} & {
|
||||
optionsWithActions?: ResolverSubmenuOptionList | string | undefined;
|
||||
}) => {
|
||||
// keep a ref to the popover so we can call its reposition method
|
||||
const popoverRef = useRef<EuiPopover>(null);
|
||||
|
||||
const [menuIsOpen, setMenuOpen] = useState(false);
|
||||
const handleMenuOpenClick = useCallback(
|
||||
(clickEvent: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
|
@ -169,6 +178,28 @@ const NodeSubMenuComponents = React.memo(
|
|||
|
||||
const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData';
|
||||
|
||||
// The last projection matrix that was used to position the popover
|
||||
const projectionMatrixAtLastRender = useRef<Matrix3>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
/**
|
||||
* If there is a popover component reference,
|
||||
* and this isn't the first render,
|
||||
* and the projectionMatrix has changed since last render,
|
||||
* then force the popover to reposition itself.
|
||||
*/
|
||||
popoverRef.current &&
|
||||
!projectionMatrixAtLastRender.current &&
|
||||
projectionMatrixAtLastRender.current !== projectionMatrix
|
||||
) {
|
||||
popoverRef.current.positionPopoverFixed();
|
||||
}
|
||||
|
||||
// no matter what, keep track of the last project matrix that was used to size the popover
|
||||
projectionMatrixAtLastRender.current = projectionMatrix;
|
||||
}, [projectionMatrixAtLastRender, projectionMatrix]);
|
||||
|
||||
if (!optionsWithActions) {
|
||||
/**
|
||||
* When called with a `menuAction`
|
||||
|
@ -216,6 +247,7 @@ const NodeSubMenuComponents = React.memo(
|
|||
isOpen={menuIsOpen}
|
||||
closePopover={closePopover}
|
||||
repositionOnScroll
|
||||
ref={popoverRef}
|
||||
>
|
||||
{menuIsOpen && typeof optionsWithActions === 'object' && (
|
||||
<OptionList isLoading={isMenuLoading} subMenuOptions={optionsWithActions} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue