UNPKG

@zendesk/react-measure-timing-hooks

Version:

react hooks for measuring time to interactive and time to render of components

221 lines 13.4 kB
"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