Skip to content

Commit

Permalink
Resolver zoom, pan, and center controls
Browse files Browse the repository at this point in the history
  • Loading branch information
peluja1012 committed Jan 17, 2020
1 parent a1fe536 commit 255aa37
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Vector2 } from '../../types';
import { Vector2, PanDirection } from '../../types';

interface UserSetZoomLevel {
readonly type: 'userSetZoomLevel';
Expand All @@ -14,6 +14,14 @@ interface UserSetZoomLevel {
readonly payload: number;
}

interface UserClickedZoomOut {
readonly type: 'userClickedZoomOut';
}

interface UserClickedZoomIn {
readonly type: 'userClickedZoomIn';
}

interface UserZoomed {
readonly type: 'userZoomed';
/**
Expand Down Expand Up @@ -56,6 +64,14 @@ interface UserStoppedPanning {
readonly type: 'userStoppedPanning';
}

interface UserClickedPanControl {
readonly type: 'userClickedPanControl';
/**
* String that represents the direction in which Resolver can be panned
*/
readonly payload: PanDirection;
}

interface UserMovedPointer {
readonly type: 'userMovedPointer';
/**
Expand All @@ -72,4 +88,7 @@ export type CameraAction =
| UserStartedPanning
| UserStoppedPanning
| UserZoomed
| UserMovedPointer;
| UserMovedPointer
| UserClickedZoomOut
| UserClickedZoomIn
| UserClickedPanControl;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
...state,
scalingFactor: clamp(action.payload, 0, 1),
};
} else if (action.type === 'userClickedZoomIn') {
return {
...state,
scalingFactor: clamp(state.scalingFactor + 0.1, 0, 1),
};
} else if (action.type === 'userClickedZoomOut') {
return {
...state,
scalingFactor: clamp(state.scalingFactor - 0.1, 0, 1),
};
} else if (action.type === 'userZoomed') {
const stateWithNewScaling: CameraState = {
...state,
Expand Down Expand Up @@ -100,6 +110,29 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
} else {
return state;
}
} else if (action.type === 'userClickedPanControl') {
const panDirection = action.payload;
const deltaAmount = (1 + state.scalingFactor) * 20;
let delta: Vector;
if (panDirection === 'north') {
delta = [0, deltaAmount];
} else if (panDirection === 'south') {
delta = [0, -deltaAmount];
} else if (panDirection === 'east') {
delta = [deltaAmount, 0];
} else if (panDirection === 'west') {
delta = [-deltaAmount, 0];
} else {
delta = [0, 0];
}

return {
...state,
translationNotCountingCurrentPanning: [
state.translationNotCountingCurrentPanning[0] + delta[0],
state.translationNotCountingCurrentPanning[1] + delta[1],
],
};
} else if (action.type === 'userSetRasterSize') {
/**
* Handle resizes of the Resolver component. We need to know the size in order to convert between screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ export const scale = (state: CameraState): Vector2 => {
return [value, value];
};

/**
* Scales the coordinate system, used for zooming. Should always be between 0 and 1
*/
export const scalingFactor = (state: CameraState): CameraState['scalingFactor'] => {
return state.scalingFactor;
};

/**
* Whether or not the user is current panning the map.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export const inverseProjectionMatrix = composeSelectors(
*/
export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale);

/**
* Scales the coordinate system, used for zooming. Should always be between 0 and 1
*/
export const scalingFactor = composeSelectors(cameraStateSelector, cameraSelectors.scalingFactor);

/**
* Whether or not the user is current panning the map.
*/
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,8 @@ export type ProcessWithWidthMetadata = {
firstChildWidth: null;
}
);

/**
* String that represents the direction in which Resolver can be panned
*/
export type PanDirection = 'north' | 'south' | 'east' | 'west';
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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 React, { useCallback, ChangeEvent } from 'react';
import styled from 'styled-components';
import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui';
import { useSelector, useDispatch } from 'react-redux';
import { ResolverAction, PanDirection } from '../types';
import * as selectors from '../store/selectors';

/**
* Controls for zooming, panning, and centering in Resolver
*/
export const GraphControls = styled(
React.memo(
({
className,
}: {
/**
* A className string provided by `styled`
*/
className?: string;
}) => {
const dispatch: (action: ResolverAction) => unknown = useDispatch();
const scalingFactor = useSelector(selectors.scalingFactor);

const handleZoomAmountChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const valueAsNumber = parseFloat(event.target.value);
if (isNaN(valueAsNumber) === false) {
dispatch({
type: 'userSetZoomLevel',
payload: valueAsNumber,
});
}
},
[dispatch]
);

const handleCenterClick = useCallback(() => {
dispatch({
type: 'userSetPositionOfCamera',
payload: [0, 0],
});
}, [dispatch]);

const handleZoomOutClick = useCallback(() => {
dispatch({
type: 'userClickedZoomOut',
});
}, [dispatch]);

const handleZoomInClick = useCallback(() => {
dispatch({
type: 'userClickedZoomIn',
});
}, [dispatch]);

const handlePanClick = useCallback(
(panDirection: PanDirection) => {
return () => {
dispatch({
type: 'userClickedPanControl',
payload: panDirection,
});
};
},
[dispatch]
);

return (
<div className={className}>
<EuiPanel className="panning-controls" paddingSize="none" hasShadow>
<div className="panning-controls-top">
<button className="north-button" title="North" onClick={handlePanClick('north')}>
<EuiIcon type="arrowUp" />
</button>
</div>
<div className="panning-controls-middle">
<button className="west-button" title="West" onClick={handlePanClick('west')}>
<EuiIcon type="arrowLeft" />
</button>
<button className="center-button" title="Center" onClick={handleCenterClick}>
<EuiIcon type="bullseye" />
</button>
<button className="east-button" title="East" onClick={handlePanClick('east')}>
<EuiIcon type="arrowRight" />
</button>
</div>
<div className="panning-controls-bottom">
<button className="south-button" title="South" onClick={handlePanClick('south')}>
<EuiIcon type="arrowDown" />
</button>
</div>
</EuiPanel>
<EuiPanel className="zoom-controls" paddingSize="none" hasShadow>
<button title="Zoom In" onClick={handleZoomInClick}>
<EuiIcon type="plusInCircle" />
</button>
<EuiRange
className="zoom-slider"
min={0}
max={1}
step={0.01}
value={scalingFactor}
onChange={handleZoomAmountChange}
/>
<button title="Zoom Out" onClick={handleZoomOutClick}>
<EuiIcon type="minusInCircle" />
</button>
</EuiPanel>
</div>
);
}
)
)`
position: absolute;
top: 5px;
left: 5px;
z-index: 1;
background-color: #d4d4d4;
color: #333333;
.zoom-controls {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px 0px;
.zoom-slider {
width: 20px;
height: 150px;
margin: 5px 0px 2px 0px;
input[type='range'] {
width: 150px;
height: 20px;
transform-origin: 75px 75px;
transform: rotate(-90deg);
}
}
}
.panning-controls {
text-align: center;
}
`;
28 changes: 16 additions & 12 deletions x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect';
import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler';
import { ProcessEventDot } from './process_event_dot';
import { EdgeLine } from './edge_line';
import { GraphControls } from './graph_controls';

export const AppRoot = React.memo(({ store }: { store: Store<ResolverState, ResolverAction> }) => {
return (
Expand Down Expand Up @@ -138,18 +139,16 @@ const Resolver = styled(
useNonPassiveWheelHandler(handleWheel, ref);

return (
<div
data-test-subj="resolverEmbeddable"
className={className}
ref={refCallback}
onMouseDown={handleMouseDown}
>
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot key={index} position={position} event={processEvent} />
))}
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine key={index} startPosition={startPosition} endPosition={endPosition} />
))}
<div data-test-subj="resolverEmbeddable" className={className}>
<GraphControls />
<div className="resolver-graph" onMouseDown={handleMouseDown} ref={refCallback}>
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot key={index} position={position} event={processEvent} />
))}
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine key={index} startPosition={startPosition} endPosition={endPosition} />
))}
</div>
</div>
);
})
Expand All @@ -167,4 +166,9 @@ const Resolver = styled(
* Prevent partially visible components from showing up outside the bounds of Resolver.
*/
overflow: hidden;
.resolver-graph {
display: flex;
flex-grow: 1;
}
`;

0 comments on commit 255aa37

Please sign in to comment.