Skip to content

Commit

Permalink
Feature/441 "focus on this subtree" (#480)
Browse files Browse the repository at this point in the history
* feat(webapp): allow focusing on a subtree

allows focusing on a subtree of a flamegraph
currently it's triggered by a menu item in the context menu (right click on the canvas)

Co-authored-by: Ryan Perry <[email protected]>
Co-authored-by: Dmitry Filimonov <[email protected]>
  • Loading branch information
3 people authored Oct 31, 2021
1 parent 38554ac commit cfe0ae8
Show file tree
Hide file tree
Showing 37 changed files with 1,275 additions and 673 deletions.
28 changes: 25 additions & 3 deletions cypress/integration/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,27 @@ describe('basic test', () => {
});
});

describe('focus', () => {
it('it toggles when clicked again', () => {
cy.intercept('**/render*', {
fixture: 'simple-golang-app-cpu.json',
times: 1,
}).as('render');

cy.visit('/');

// click once once
cy.findByTestId('flamegraph-canvas').click(0, BAR_HEIGHT * 2);

// click again
cy.findByTestId('flamegraph-canvas').click(0, BAR_HEIGHT * 2);

cy.findByTestId('flamegraph-canvas').matchImageSnapshot(
`simple-golang-app-focus-toggle`
);
});
});

describe('contextmenu', () => {
it("it works when 'clear view' is clicked", () => {
cy.intercept('**/render*', {
Expand All @@ -377,9 +398,10 @@ describe('basic test', () => {
// click on the second item
cy.findByTestId('flamegraph-canvas').click(0, BAR_HEIGHT * 2);
cy.findByTestId('flamegraph-canvas').rightclick();
cy.findByRole('menuitem')
.contains('Reset View')
.should('not.have.attr', 'aria-disabled');
cy.findByRole('menuitem', { name: /Reset View/ }).should(
'not.have.attr',
'aria-disabled'
);
cy.findByRole('menuitem', { name: /Reset View/ }).click();
// TODO assert that it was indeed reset?

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"lodash": "^4.17.20",
"moment": "^2.27.0",
"normalize.css": "^8.0.1",
"prelude-ts": "^1.0.3",
"react": "16.12.0",
"react-datepicker": "^3.4.1",
"react-dom": "^16.13.1",
Expand All @@ -138,7 +139,8 @@
"redux-query-sync": "^0.1.10",
"redux-thunk": "^2.3.0",
"sanitize.css": "^11.0.1",
"style-loader": "^3.2.1"
"style-loader": "^3.2.1",
"ts-essentials": "^9.0.0"
},
"optionalDependencies": {
"@size-limit/file": "^6.0.3",
Expand Down
2 changes: 1 addition & 1 deletion scripts/jest-snapshots/run-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ docker run \
-v $PWD:/app \
-v /app/node_modules \
-w /app \
node:current-slim ./scripts/jest-snapshots/run-snapshots.sh
node:14.15-slim ./scripts/jest-snapshots/run-snapshots.sh
6 changes: 5 additions & 1 deletion scripts/jest-snapshots/run-snapshots.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ fi
apt update -y
apt install fontconfig -y

yarn install
# ignore-engines due to
# warning [email protected]: Invalid bin field for "webpack-plugin-serve".
# error [email protected]: The engine "node" is incompatible with this module. Expected version "^16 || ^15 || ^14 || ^13 || ^12 || ^11 || ^10 || ^9 || ^8 || ^7 || ^6". Got "17.0.1"
# error Found incompatible module.
yarn install --ignore-engines
RUN_SNAPSHOTS=true yarn test --testNamePattern='group:snapshot' --verbose "$updateArg"

10 changes: 9 additions & 1 deletion setupAfterEnv.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';

const { toMatchImageSnapshot } = require('jest-image-snapshot');
const {
toMatchImageSnapshot,
configureToMatchImageSnapshot,
} = require('jest-image-snapshot');

expect.extend({
toMatchImageSnapshot(received: any, options: any) {
// If these checks pass, assume we're in a JSDOM environment with the 'canvas' package.
if (process.env.RUN_SNAPSHOTS) {
const customConfig = { threshold: 0.01 };
const toMatchImageSnapshot = configureToMatchImageSnapshot({
customDiffConfig: customConfig,
});

return toMatchImageSnapshot.call(this, received, options);
}

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"jsx": "react",
"lib": ["dom", "esnext"],
"lib": ["dom", "esnext", "ES2015.Iterable"],
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ describe('ContextMenu', () => {
];
};

render(<TestCanvas xyToMenuItems={xyToMenuItems} />);
render(
<TestCanvas
xyToMenuItems={xyToMenuItems}
onClose={() => {}}
onOpen={() => {}}
/>
);

expect(queryByRole('menu')).not.toBeInTheDocument();

Expand All @@ -53,7 +59,13 @@ describe('ContextMenu', () => {
it('shows different items depending on the clicked node', () => {
const xyToMenuItems = jest.fn();

render(<TestCanvas xyToMenuItems={xyToMenuItems} />);
render(
<TestCanvas
xyToMenuItems={xyToMenuItems}
onClose={() => {}}
onOpen={() => {}}
/>
);

expect(queryByRole('menu')).not.toBeInTheDocument();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import React from 'react';
import {
ControlledMenu,
useMenuState,
MenuItem,
SubMenu,
} from '@szhsin/react-menu';

// even though the library support many different types
// we only support these
type SupportedItems = typeof MenuItem | typeof SubMenu;
import { ControlledMenu, useMenuState } from '@szhsin/react-menu';

type xyToMenuItems = (x: number, y: number) => JSX.Element[];

Expand All @@ -21,27 +12,26 @@ export interface ContextMenuProps {
* only MenuItem and SubMenu should be supported
*/
xyToMenuItems: xyToMenuItems;

onClose: () => void;
onOpen: (x: number, y: number) => void;
}

export default function ContextMenu(props: ContextMenuProps) {
const { toggleMenu, openMenu, closeMenu, ...menuProps } = useMenuState(false);
const [anchorPoint, setAnchorPoint] = React.useState({ x: 0, y: 0 });
const { canvasRef } = props;
const [menuItems, setMenuItems] = React.useState<JSX.Element[]>([]);
const {
xyToMenuItems,
onClose: onCloseCallback,
onOpen: onOpenCallback,
} = props;

const onContextMenu = (e: MouseEvent) => {
e.preventDefault();

const items = props.xyToMenuItems(e.offsetX, e.offsetY);
setMenuItems(items);

// TODO
// if the menu becomes too large, it may overflow to outside the screen
const x = e.clientX;
const y = e.clientY + 20;
const onClose = () => {
closeMenu();

setAnchorPoint({ x, y });
openMenu();
onCloseCallback();
};

React.useEffect(() => {
Expand All @@ -55,20 +45,40 @@ export default function ContextMenu(props: ContextMenuProps) {
return () => {};
}

const onContextMenu = (e: MouseEvent) => {
e.preventDefault();

const items = xyToMenuItems(e.offsetX, e.offsetY);
setMenuItems(items);

// console.log('set menu items', items);

// TODO
// if the menu becomes too large, it may overflow to outside the screen
const x = e.clientX;
const y = e.clientY + 20;

setAnchorPoint({ x, y });
openMenu();

onOpenCallback(e.offsetX, e.offsetY);
};

// watch for mouse events on the bar
canvasEl.addEventListener('contextmenu', onContextMenu);

return () => {
canvasEl.removeEventListener('contextmenu', onContextMenu);
};
}, []);
}, [xyToMenuItems]);

return (
<ControlledMenu
menuItemFocus={menuProps.menuItemFocus}
isMounted={menuProps.isMounted}
isOpen={menuProps.isOpen}
anchorPoint={anchorPoint}
onClose={closeMenu}
onClose={onClose}
>
{menuItems}
</ControlledMenu>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.highlightContextMenu {
position: absolute;
pointer-events: none;
/* make it a bit lighter so that it's easier to distinguish when both highlights are on */
background: #ffffff8c;
mix-blend-mode: overlay;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { Option } from 'prelude-ts';
import styles from './ContextMenuHighlight.module.css';

export interface HighlightProps {
// probably the same as the bar height
barHeight: number;

node: Option<{ top: number; left: number; width: number }>;
}

const initialSyle: React.CSSProperties = {
height: '0px',
visibility: 'hidden',
};

/**
* Highlight on the node that triggered the context menu
*/
export default function ContextMenuHighlight(props: HighlightProps) {
const { node, barHeight } = props;
const [style, setStyle] = React.useState(initialSyle);

React.useEffect(
() => {
node.match({
None: () => setStyle(initialSyle),
Some: (data) =>
setStyle({
visibility: 'visible',
height: `${barHeight}px`,
...data,
}),
});
},
// refresh callback functions when they change
[node]
);

return (
<div
className={styles.highlightContextMenu}
style={style}
data-testid="flamegraph-highlight-contextmenu"
/>
);
}
Loading

0 comments on commit cfe0ae8

Please sign in to comment.