@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
221 lines • 13.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable no-magic-numbers */
/* eslint-disable import/no-extraneous-dependencies */
const react_1 = __importStar(require("react"));
const axis_1 = require("@visx/axis");
const brush_1 = require("@visx/brush");
const grid_1 = require("@visx/grid");
const group_1 = require("@visx/group");
const legend_1 = require("@visx/legend");
const pattern_1 = require("@visx/pattern");
const scale_1 = require("@visx/scale");
const tooltip_1 = require("@visx/tooltip");
const constants_1 = require("../constants");
const FilterGroup_1 = require("./FilterGroup");
const InteractiveSpan_1 = __importDefault(require("./InteractiveSpan"));
const Legend_1 = require("./Legend");
const SpanDetails_1 = __importDefault(require("./SpanDetails"));
const styled_1 = require("./styled");
const DEFAULT_MARGIN = { top: 50, left: 200, right: 20, bottom: 0 };
const GROUP_HEIGHT = 20;
const FOOTER_HEIGHT = 100;
const FOOTER_SCALE_HEIGHT = 30;
const MINIMAP_HEIGHT = 25;
// Define a custom handle component
function BrushHandle({ x, height, isBrushActive }) {
const pathWidth = 8;
const pathHeight = 15;
if (!isBrushActive) {
return null;
}
return (react_1.default.createElement(group_1.Group, { left: x + pathWidth / 2, top: (height - pathHeight) / 2 },
react_1.default.createElement("path", { fill: "#f2f2f2", d: "M -4.5 0.5 L 3.5 0.5 L 3.5 15.5 L -4.5 15.5 L -4.5 0.5 M -1.5 4 L -1.5 12 M 0.5 4 L 0.5 12", stroke: "#999999", strokeWidth: "1", style: { cursor: 'ew-resize' } })));
}
const OperationVisualization = ({ width: containerWidth, operation, displayOptions, setDisplayOptions, margin = DEFAULT_MARGIN, }) => {
const { spanEvents, spanTypes, uniqueGroups, spansWithDuration } = operation;
const [selectedSpan, setSelectedSpan] = (0, react_1.useState)(null);
// Add new state to control zoom domain
const [zoomDomain, setZoomDomain] = (0, react_1.useState)([
0,
operation.duration + 10,
]);
// Adjust width when panel is open
const width = selectedSpan
? containerWidth - constants_1.DETAILS_PANEL_WIDTH
: containerWidth;
// Render proportions
const height = uniqueGroups.length * GROUP_HEIGHT + margin.top + margin.bottom;
const xMax = width - margin.left - margin.right;
const yMax = height - margin.bottom - margin.top;
// Brush scale for the minimap
const xMinimapScale = (0, react_1.useMemo)(() => (0, scale_1.scaleLinear)({
domain: [0, operation.duration + 10],
range: [0, width - margin.left - margin.right],
}), [operation.duration, width, margin.left, margin.right]);
// Update domain on brush
const handleMinimapBrushChange = (domain) => {
if (!domain)
return;
setZoomDomain([domain.x0, domain.x1]);
};
const handleMinimapReset = () => {
setZoomDomain([0, operation.duration + 10]);
};
// Make main xScale use zoomDomain
const xScale = (0, scale_1.scaleLinear)({
domain: zoomDomain,
range: [0, xMax],
});
const yScale = (0, react_1.useMemo)(() => (0, scale_1.scaleBand)({
domain: uniqueGroups,
range: [0, yMax],
padding: 0.2,
}), [uniqueGroups, yMax]);
const colorScale = (0, scale_1.scaleOrdinal)({
domain: [...spanTypes],
range: [...spanTypes].map((kind) => constants_1.BAR_FILL_COLOR[kind]),
});
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip, } = (0, tooltip_1.useTooltip)();
const handleSpanClick = (span) => {
setSelectedSpan(span);
};
const getBarOpacity = (entry) => {
if (selectedSpan &&
selectedSpan.span.name === entry.span.name &&
selectedSpan.span.startTime === entry.span.startTime) {
return 0.8; // Selected state
}
return 0.4; // Default state
};
// Add ref for scroll container
const scrollContainerRef = react_1.default.useRef(null);
// Handle escape key
(0, react_1.useEffect)(() => {
const handleEscape = (event) => {
if (event.key === 'Escape' && selectedSpan) {
setSelectedSpan(null);
}
};
document.addEventListener('keydown', handleEscape);
return () => void document.removeEventListener('keydown', handleEscape);
}, [selectedSpan]);
// Handle click outside
const handleContainerClick = (event) => {
// Only handle clicks directly on the SVG or main container
if (event.target === event.currentTarget ||
event.target.tagName === 'svg') {
setSelectedSpan(null);
}
};
return (react_1.default.createElement(styled_1.Container, null,
react_1.default.createElement(styled_1.ScrollContainer, { ref: scrollContainerRef, onClick: selectedSpan ? handleContainerClick : undefined },
react_1.default.createElement(styled_1.Header, null,
react_1.default.createElement(styled_1.Title, null,
"Operation: ",
operation.name)),
react_1.default.createElement("main", { style: {
marginTop: `-${Math.round(margin.top / 2)}px`,
} },
react_1.default.createElement("svg", { width: width, height: height, style: { display: 'block' }, onClick: selectedSpan ? handleContainerClick : undefined },
react_1.default.createElement(group_1.Group, { top: margin.top, left: margin.left },
react_1.default.createElement(grid_1.Grid, { xScale: xScale, yScale: yScale, width: xMax, height: yMax, numTicksRows: uniqueGroups.length }),
spanEvents.map((entry, index) => (react_1.default.createElement(InteractiveSpan_1.default, { key: `spanEvent-${index}`, type: "line", data: entry, xScale: xScale, yScale: yScale, yMax: yMax, opacity: 0.8, showTooltip: showTooltip, hideTooltip: hideTooltip, onClick: () => void handleSpanClick(entry), scrollContainerRef: scrollContainerRef }))),
spansWithDuration.map((entry, i) => (react_1.default.createElement(react_1.default.Fragment, { key: `entry-${i}` },
react_1.default.createElement(InteractiveSpan_1.default, { type: "bar", data: entry, xScale: xScale, yScale: yScale, yMax: yMax, opacity: getBarOpacity(entry), showTooltip: showTooltip, hideTooltip: hideTooltip, onClick: () => void handleSpanClick(entry), scrollContainerRef: scrollContainerRef }),
(entry.annotation.markedComplete ||
entry.annotation.markedPageInteractive) && (react_1.default.createElement(InteractiveSpan_1.default, { type: "line", data: entry, xScale: xScale, yScale: yScale, yMax: yMax, annotateAt: "top", title: entry.annotation.markedComplete &&
entry.annotation.markedPageInteractive
? 'complete & interactive'
: entry.annotation.markedPageInteractive
? 'interactive'
: 'complete', opacity: 0.8, showTooltip: showTooltip, hideTooltip: hideTooltip, onClick: () => void handleSpanClick(entry), scrollContainerRef: scrollContainerRef }))))),
react_1.default.createElement(axis_1.AxisLeft, { scale: yScale, numTicks: uniqueGroups.length, tickLabelProps: {
fill: '#888',
fontSize: 10,
textAnchor: 'end',
dy: '0.33em',
width: 100,
}, tickFormat: (value) => value })))),
react_1.default.createElement(styled_1.Footer, { width: width, height: FOOTER_HEIGHT + FOOTER_SCALE_HEIGHT + MINIMAP_HEIGHT },
react_1.default.createElement("svg", { width: width, height: FOOTER_SCALE_HEIGHT + MINIMAP_HEIGHT, style: { display: 'block' } },
react_1.default.createElement(axis_1.Axis, { scale: xScale, top: 1, left: margin.left }),
react_1.default.createElement(group_1.Group, { top: FOOTER_SCALE_HEIGHT, left: margin.left },
react_1.default.createElement(pattern_1.PatternLines, { id: "brush_pattern", height: 8, width: 8, stroke: "#f6acc8", strokeWidth: 1, orientation: ['diagonal'] }),
react_1.default.createElement(brush_1.Brush, { xScale: xMinimapScale, yScale: (0, scale_1.scaleLinear)({
domain: [0, 1],
range: [MINIMAP_HEIGHT, 0],
}), margin: {
left: margin.left,
right: margin.right,
}, width: xMinimapScale.range()[1], height: MINIMAP_HEIGHT, handleSize: 8, selectedBoxStyle: {
fill: 'url(#brush_pattern)',
stroke: 'red',
}, onChange: handleMinimapBrushChange, onClick: handleMinimapReset, resizeTriggerAreas: ['left', 'right'], brushDirection: "horizontal", useWindowMoveEvents: true, renderBrushHandle: (props) => react_1.default.createElement(BrushHandle, { ...props }) }))),
react_1.default.createElement(styled_1.FooterContent, null,
react_1.default.createElement(FilterGroup_1.FilterGroup, { setState: setDisplayOptions, state: displayOptions }),
react_1.default.createElement(Legend_1.LegendGroup, null,
react_1.default.createElement(legend_1.LegendOrdinal, { scale: colorScale, labelFormat: (label) => `${label.toUpperCase()}` }, (labels) => (react_1.default.createElement("div", { style: { display: 'flex', flexDirection: 'row' } }, labels.map((label, i) => (react_1.default.createElement(legend_1.LegendItem, { key: `legend-${i}`, margin: "0 5px" },
react_1.default.createElement("svg", { width: 15, height: 15 },
react_1.default.createElement(styled_1.StyledRect, { fill: label.value, width: 15, height: 15 })),
react_1.default.createElement(legend_1.LegendLabel, { align: "left", margin: "0 0 0 4px" }, label.text)))))))))),
tooltipOpen && tooltipData && (react_1.default.createElement(styled_1.StyledTooltip, { top: tooltipTop, left: tooltipLeft },
react_1.default.createElement("div", null,
react_1.default.createElement(styled_1.TooltipTitle, null, tooltipData.span.name),
react_1.default.createElement(styled_1.TooltipContent, null,
react_1.default.createElement("div", null,
"kind: ",
tooltipData.type),
react_1.default.createElement("div", null,
"occurrence: ",
tooltipData.annotation.occurrence),
react_1.default.createElement("div", null,
"start:",
' ',
tooltipData.annotation.operationRelativeStartTime.toFixed(2),
"ms"),
react_1.default.createElement("div", null,
"duration: ",
tooltipData.span.duration.toFixed(2),
"ms")))))),
react_1.default.createElement(SpanDetails_1.default, { span: selectedSpan, onClose: () => void setSelectedSpan(null) })));
};
// eslint-disable-next-line import/no-default-export
exports.default = OperationVisualization;
//# sourceMappingURL=OperationVisualization.js.map