"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ProcessEventDot = void 0; var _react = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireDefault(require("styled-components")); var _eui = require("@elastic/eui"); var _reactRedux = require("react-redux"); var _i18nReact = require("@kbn/i18n-react"); var _i18n = require("@kbn/i18n"); var _styles = require("./styles"); var _vector = require("../models/vector2"); var _side_effect_context = require("./side_effect_context"); var nodeModel = _interopRequireWildcard(require("../../../common/endpoint/models/node")); var eventModel = _interopRequireWildcard(require("../../../common/endpoint/models/event")); var nodeDataModel = _interopRequireWildcard(require("../models/node_data")); var selectors = _interopRequireWildcard(require("../store/selectors")); var _font_size = require("./font_size"); var _use_cube_assets = require("./use_cube_assets"); var _use_symbol_ids = require("./use_symbol_ids"); var _use_colors = require("./use_colors"); var _use_link_props = require("./use_link_props"); var _actions = require("../store/actions"); var _action = require("../store/data/action"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ const StyledActionsContainer = _styledComponents.default.div` background-color: transparent; color: ${props => props.color}; display: flex; flex-flow: column; font-size: ${props => `${props.fontSize}px`}; left: 20.9%; line-height: 140%; padding: 0.25rem 0 0 0.1rem; position: absolute; top: ${props => `${props.topPct}%`}; width: auto; pointer-events: all; `; const StyledDescriptionText = _styledComponents.default.div` background-color: ${props => props.backgroundColor}; color: ${props => props.color}; display: ${props => props.isDisplaying ? 'block' : 'none'}; font-size: 0.8rem; font-weight: bold; letter-spacing: -0.01px; line-height: 1; margin: 0; padding: 4px 0 0 2px; text-align: left; text-transform: uppercase; width: fit-content; z-index: 45; `; const StyledEuiButtonContent = _styledComponents.default.span` padding: ${props => props.isShowingIcon ? '0px' : '0 12px'}; `; const StyledOuterGroup = _styledComponents.default.g` fill: none; pointer-events: visiblePainted; // The below will apply the loading css to the element that references the cube // when the nodeData is loading for the current node ${props => props.isNodeLoading && ` & .cube { animation-name: pulse; /** * his is a multiple of .6 so it can match up with the EUI button's loading spinner * which is (0.6s). Using .6 here makes it a bit too fast. */ animation-duration: 1.8s; animation-delay: 0; animation-direction: normal; animation-iteration-count: infinite; animation-timing-function: linear; } /** * Animation loading state of the cube. */ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.35; } 100% { opacity: 1; } } `} `; /** * An artifact that represents a process node and the things associated with it in the Resolver */ // eslint-disable-next-line react/display-name const UnstyledProcessEventDot = /*#__PURE__*/_react.default.memo(({ id, className, position, node, nodeID, projectionMatrix, timeAtRender }) => { const resolverComponentInstanceID = id; // This should be unique to each instance of Resolver const htmlIDPrefix = `resolver:${resolverComponentInstanceID}`; const symbolIDs = (0, _use_symbol_ids.useSymbolIDs)({ id }); const { timestamp } = (0, _react.useContext)(_side_effect_context.SideEffectContext); /** * Convert the position, which is in 'world' coordinates, to screen coordinates. */ const [left, top] = (0, _vector.applyMatrix3)(position, projectionMatrix); const [xScale] = projectionMatrix; // Node (html id=) IDs const ariaActiveDescendant = (0, _reactRedux.useSelector)(state => selectors.ariaActiveDescendant(state.analyzer[id])); const selectedNode = (0, _reactRedux.useSelector)(state => selectors.selectedNode(state.analyzer[id])); const originID = (0, _reactRedux.useSelector)(state => selectors.originID(state.analyzer[id])); const nodeStats = (0, _reactRedux.useSelector)(state => selectors.nodeStats(state.analyzer[id])(nodeID)); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes const nodeHTMLID = (0, _react.useCallback)(nodeId => (0, _eui.htmlIdGenerator)(htmlIDPrefix)(`${nodeId}:node`), [htmlIDPrefix]); const ariaLevel = (0, _reactRedux.useSelector)(state => selectors.ariaLevel(state.analyzer[id])(nodeID)); // the node ID to 'flowto' const ariaFlowtoNodeID = (0, _reactRedux.useSelector)(state => selectors.ariaFlowtoNodeID(state.analyzer[id])(timeAtRender)(nodeID)); const isShowingEventActions = xScale > 0.8; const isShowingDescriptionText = xScale >= 0.55; /** * As the resolver zooms and buttons and text change visibility, we look to keep the overall container properly vertically aligned */ const actionableButtonsTopOffset = (isShowingEventActions ? 3.5 : isShowingDescriptionText ? 1 : 21) * xScale + 5; /** * The `left` and `top` values represent the 'center' point of the process node. * Since the view has content to the left and above the 'center' point, offset the * position to accomodate for that. This aligns the logical center of the process node * with the correct position on the map. */ const logicalProcessNodeViewWidth = 360; const logicalProcessNodeViewHeight = 120; /** * As the scale changes and button visibility toggles on the graph, these offsets help scale to keep the nodes centered on the edge */ const nodeXOffsetValue = isShowingEventActions ? -0.147413 : -0.147413 - (xScale - 0.5) * 0.08; const nodeYOffsetValue = isShowingEventActions ? -0.53684 : -0.53684 + -xScale * 0.2 * (1 - xScale) / xScale; const processNodeViewXOffset = nodeXOffsetValue * logicalProcessNodeViewWidth * xScale; const processNodeViewYOffset = nodeYOffsetValue * logicalProcessNodeViewHeight * xScale; const nodeViewportStyle = (0, _react.useMemo)(() => ({ left: `${left + processNodeViewXOffset}px`, top: `${top + processNodeViewYOffset}px`, // Width of symbol viewport scaled to fit width: `${logicalProcessNodeViewWidth * xScale}px`, // Height according to symbol viewbox AR height: `${logicalProcessNodeViewHeight * xScale}px` }), [left, xScale, processNodeViewXOffset, processNodeViewYOffset, top]); /** * Type in non-SVG components scales as follows: * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise */ const scaledTypeSize = (0, _font_size.fontSize)(xScale, 18.75, 12.5); const markerBaseSize = 15; const markerSize = markerBaseSize; const markerPositionYOffset = -markerBaseSize / 2 - 4; const markerPositionXOffset = -markerBaseSize / 2 - 4; /** * An element that should be animated when the node is clicked. */ const animationTarget = /*#__PURE__*/_react.default.createRef(); const colorMap = (0, _use_colors.useColors)(); const nodeState = (0, _reactRedux.useSelector)(state => selectors.nodeDataStatus(state.analyzer[id])(nodeID)); const isNodeLoading = nodeState === 'loading'; const { backingFill, cubeSymbol, descriptionText, isLabelFilled, labelButtonFill, strokeColor } = (0, _use_cube_assets.useCubeAssets)(id, nodeState, /** * There is no definition for 'trigger process' yet. return false. */ false); const labelHTMLID = (0, _eui.htmlIdGenerator)('resolver')(`${nodeID}:label`); const isAriaCurrent = nodeID === ariaActiveDescendant; const isAriaSelected = nodeID === selectedNode; const isOrigin = nodeID === originID; const dispatch = (0, _reactRedux.useDispatch)(); const processDetailNavProps = (0, _use_link_props.useLinkProps)(id, { panelView: 'nodeDetail', panelParameters: { nodeID } }); const handleFocus = (0, _react.useCallback)(() => { dispatch((0, _actions.userFocusedOnResolverNode)({ id, nodeID, time: timestamp() })); }, [dispatch, nodeID, timestamp, id]); const handleClick = (0, _react.useCallback)(clickEvent => { var _animationTarget$curr; if ((_animationTarget$curr = animationTarget.current) !== null && _animationTarget$curr !== void 0 && _animationTarget$curr.beginElement) { animationTarget.current.beginElement(); } if (nodeState === 'error') { dispatch((0, _action.userReloadedResolverNode)({ id, nodeID })); } else { dispatch((0, _actions.userSelectedResolverNode)({ id, nodeID, time: timestamp() })); processDetailNavProps.onClick(clickEvent); } }, [animationTarget, dispatch, nodeID, processDetailNavProps, nodeState, timestamp, id]); const grandTotal = (0, _reactRedux.useSelector)(state => selectors.statsTotalForNode(state.analyzer[id])(node)); const nodeName = nodeModel.nodeName(node); const processEvent = (0, _reactRedux.useSelector)(state => nodeDataModel.firstEvent(selectors.nodeDataForID(state.analyzer[id])(String(node.id)))); const processName = (0, _react.useMemo)(() => { if (processEvent !== undefined) { return eventModel.processNameSafeVersion(processEvent); } else { return nodeName; } }, [processEvent, nodeName]); /* eslint-disable jsx-a11y/click-events-have-key-events */ return /*#__PURE__*/_react.default.createElement("div", { "data-test-subj": "resolver:node", "data-test-resolver-node-id": nodeID, className: `${className} kbn-resetFocusState`, role: "treeitem", "aria-level": ariaLevel === null ? undefined : ariaLevel, "aria-flowto": ariaFlowtoNodeID === null ? undefined : nodeHTMLID(ariaFlowtoNodeID), "aria-labelledby": labelHTMLID, "aria-haspopup": "true", "aria-current": isAriaCurrent ? 'true' : undefined, "aria-selected": isAriaSelected ? 'true' : undefined, style: nodeViewportStyle, id: nodeHTMLID(nodeID), tabIndex: -1 }, /*#__PURE__*/_react.default.createElement("svg", { viewBox: "-15 -15 90 30", preserveAspectRatio: "xMidYMid meet", onClick: clickEvent => { handleFocus(); handleClick(clickEvent); } /* a11y note: this is strictly an alternate to the button, so no tabindex is necessary*/, role: "img", "aria-labelledby": labelHTMLID, fill: "none", style: { display: 'block', width: '100%', height: '100%', position: 'absolute', top: '0', left: '0', outline: 'transparent', border: 'none', pointerEvents: 'none', zIndex: 30 } }, /*#__PURE__*/_react.default.createElement(StyledOuterGroup, { isNodeLoading: isNodeLoading }, /*#__PURE__*/_react.default.createElement("use", { xlinkHref: `#${symbolIDs.processCubeActiveBacking}`, fill: backingFill // Only visible on hover , x: -15.35, y: -15.35, stroke: strokeColor, width: markerSize * 1.5, height: markerSize * 1.5, className: "backing" }), isOrigin && /*#__PURE__*/_react.default.createElement("use", { xlinkHref: `#${symbolIDs.processCubeActiveBacking}`, fill: "transparent" // Transparent so we don't double up on the default hover , x: -15.35, y: -15.35, stroke: strokeColor, strokeOpacity: 0.35, strokeDashoffset: 0, width: markerSize * 1.5, height: markerSize * 1.5, className: "origin" }), /*#__PURE__*/_react.default.createElement("use", { role: "presentation", xlinkHref: cubeSymbol, x: markerPositionXOffset, y: markerPositionYOffset, width: markerSize, height: markerSize, opacity: "1", className: "cube" }, /*#__PURE__*/_react.default.createElement("animateTransform", { attributeType: "XML", attributeName: "transform", type: "scale", values: "1 1; 1 .83; 1 .8; 1 .83; 1 1", dur: "0.2s", repeatCount: "1", className: "squish", ref: animationTarget })))), /*#__PURE__*/_react.default.createElement(StyledActionsContainer, { color: colorMap.full, fontSize: scaledTypeSize, topPct: actionableButtonsTopOffset }, /*#__PURE__*/_react.default.createElement(StyledDescriptionText, { backgroundColor: colorMap.resolverBackground, color: colorMap.descriptionText, isDisplaying: isShowingDescriptionText, "data-test-subj": "resolver:node:description" }, /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.securitySolution.endpoint.resolver.processDescription", defaultMessage: "{isEventBeingAnalyzed, select, true {Analyzed Event \xB7 {descriptionText}} false {{descriptionText}}}", values: { isEventBeingAnalyzed: isOrigin, descriptionText } })), /*#__PURE__*/_react.default.createElement("div", { className: 'euiButton euiButton--small', id: labelHTMLID, onClick: handleClick, onFocus: handleFocus, tabIndex: -1, style: { backgroundColor: colorMap.resolverBackground, alignSelf: 'flex-start', padding: 0, zIndex: 45 } }, /*#__PURE__*/_react.default.createElement(_eui.EuiButton, { iconSide: isNodeLoading ? 'right' : 'left', isLoading: isNodeLoading, color: labelButtonFill, fill: isLabelFilled, iconType: nodeState === 'error' ? 'refresh' : '', size: "s", style: { maxHeight: `${Math.min(26 + xScale * 3, 32)}px`, maxWidth: `${isShowingEventActions ? 400 : 210 * xScale}px` }, tabIndex: -1, title: processName, "data-test-subj": "resolver:node:primary-button", "data-test-resolver-node-id": nodeID }, /*#__PURE__*/_react.default.createElement(StyledEuiButtonContent, { isShowingIcon: nodeState === 'loading' || nodeState === 'error', className: "eui-textTruncate", "data-test-subj": 'euiButton__text' }, _i18n.i18n.translate('xpack.securitySolution.resolver.node_button_name', { defaultMessage: `{nodeState, select, error {Reload {nodeName}} other {{nodeName}}}`, values: { nodeState, nodeName: processName } })))), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexGroup, { justifyContent: "flexStart", gutterSize: "xs", style: { alignSelf: 'flex-start', background: colorMap.resolverBackground, display: `${isShowingEventActions ? 'flex' : 'none'}`, margin: '2px 0 0 0', padding: 0 } }, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false, className: "related-dropdown" }, grandTotal !== null && grandTotal > 0 && /*#__PURE__*/_react.default.createElement(_styles.NodeSubMenu, { id: id, buttonFill: colorMap.resolverBackground, nodeStats: nodeStats, nodeID: nodeID }))))); /* eslint-enable jsx-a11y/click-events-have-key-events */ }); const ProcessEventDot = (0, _styledComponents.default)(UnstyledProcessEventDot)` position: absolute; text-align: left; font-size: 10px; user-select: none; box-sizing: border-box; border-radius: 10%; white-space: nowrap; will-change: left, top, width, height; min-width: 280px; min-height: 90px; overflow-y: visible; pointer-events: none; z-index: auto; //dasharray & dashoffset should be equal to "pull" the stroke back //when it is transitioned. //The value is tuned to look good when animated, but to preserve //the effect, it should always be _at least_ the length of the stroke & .backing { stroke-dasharray: 500; stroke-dashoffset: 500; fill-opacity: 0; } &:hover:not([aria-current]) .backing { transition-property: fill-opacity; transition-duration: 0.25s; fill-opacity: 1; // actual color opacity handled in the fill hex } &[aria-current] .backing { transition-property: stroke-dashoffset; transition-duration: 1s; stroke-dashoffset: 0; } & .euiButton { width: fit-content; } & .euiSelectableList-bordered { border-top-right-radius: 0px; border-top-left-radius: 0px; } & .euiSelectableListItem { background-color: black; } & .euiSelectableListItem path { fill: white; } & .euiSelectableListItem__text { color: white; } `; exports.ProcessEventDot = ProcessEventDot;