app-ui
created pr with
46.1
cmds
checkout latest patchset:
ssh pr.pico.sh print 46 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 46.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 46
Patchset
46.1
Install react-use-websocket
Eric Abruzzese
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
2025-01-15T14:28:15ZEric Abruzzese
Collect events on the diagnostic details page and construct a dashboard state
2025-01-15T14:29:34ZEric Abruzzese
Formatting and cleanup
2025-01-15T14:30:03ZEric Abruzzese
Show dashboard messages, resources, operations, and plots
2025-01-15T14:34:02ZMichael Peterson
Support async plot annotations
2025-01-28T20:23:45ZEric Abruzzese
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
2025-01-31T17:53:30ZEric Abruzzese
Add VITE_APTIBLE_AI_URL to .env.example
2025-02-03T20:19:05ZEric Abruzzese
Update VITE_APTIBLE_AI_URL references to point to Hotshot
2025-02-04T16:30:13ZEric Abruzzese
Update the analysis using PlotAnnotated events
2025-02-05T17:50:02ZEric Abruzzese
Install react-use-websocket
2025-02-05T17:51:11ZEric Abruzzese
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
2025-01-15T14:28:15ZEric Abruzzese
Collect events on the diagnostic details page and construct a dashboard state
2025-01-15T14:29:34ZEric Abruzzese
Formatting and cleanup
2025-01-15T14:30:03ZEric Abruzzese
Show dashboard messages, resources, operations, and plots
2025-01-15T14:34:02ZMichael Peterson
Support async plot annotations
2025-01-28T20:23:45ZEric Abruzzese
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
2025-01-31T17:53:30ZEric Abruzzese
Add VITE_APTIBLE_AI_URL to .env.example
2025-02-03T20:19:05ZEric Abruzzese
Update VITE_APTIBLE_AI_URL references to point to Hotshot
2025-02-04T16:30:13ZEric Abruzzese
Update the analysis using PlotAnnotated events
2025-02-05T17:50:02ZEric Abruzzese
→ Refactor charts
2025-02-05T17:51:11ZMichael Peterson
Refactor OperationsTimeline, chart hover state
2025-02-05T18:25:59ZMichael Peterson
Extract types into aptible-ai/index, useDashboard hook
2025-02-05T18:42:20ZMichael Peterson
Fix a linter error
2025-02-05T19:21:56ZEric Abruzzese
Fix linter errors
2025-02-05T19:40:48ZEric Abruzzese
2025-02-05T19:50:07Z
Refactor charts
Michael Peterson
2025-02-05T18:25:59ZSemantic diff summary
3 added,
4 modified,
0 signature changed,
2 removed
across 4 analyzed files
+95
-0
src/chart/chartjs-plugin-annoations.ts
#
| ... | ... | @@ -0,0 +1,95 @@ | |
| 1 | + | import { Chart as ChartJS } from "chart.js"; | |
| 2 | + | ||
| 3 | + | ||
| 4 | + | // Update ChartJS interface to include annotations | |
| 5 | + | declare module 'chart.js' { | |
| 6 | + | interface Chart { | |
| 7 | + | annotationAreas?: Array<{ | |
| 8 | + | x1: number; | |
| 9 | + | x2: number; | |
| 10 | + | y1: number; | |
| 11 | + | y2: number; | |
| 12 | + | description: string; | |
| 13 | + | }>; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | interface PluginOptionsByType<TType> { | |
| 17 | + | annotations?: Annotation[]; | |
| 18 | + | } | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | export type Annotation = { | |
| 22 | + | label: string; | |
| 23 | + | description: string; | |
| 24 | + | x_min: number; | |
| 25 | + | x_max: number; | |
| 26 | + | y_min: number; | |
| 27 | + | y_max: number; | |
| 28 | + | }; | |
| 29 | + | ||
| 30 | + | // ChartJS plugin to draw annotations on a line chart | |
| 31 | + | export const annotationsPlugin = { | |
| 32 | + | id: 'annotations', | |
| 33 | + | afterDraw: (chart: ChartJS, args: any, options: any) => { | |
| 34 | + | const ctx = chart.ctx; | |
| 35 | + | const annotations = chart.options?.plugins?.annotations || []; | |
| 36 | + | ||
| 37 | + | annotations.forEach((annotation: any) => { | |
| 38 | + | // Convert timestamps to numbers for the time scale | |
| 39 | + | const xScale = chart.scales.x; | |
| 40 | + | const yScale = chart.scales.y; | |
| 41 | + | ||
| 42 | + | // Parse the timestamps into Date objects and get their timestamps | |
| 43 | + | const x1 = new Date(annotation.x_min).getTime(); | |
| 44 | + | const x2 = new Date(annotation.x_max).getTime(); | |
| 45 | + | ||
| 46 | + | // Convert to pixel coordinates | |
| 47 | + | const pixelX1 = xScale.getPixelForValue(x1); | |
| 48 | + | const pixelX2 = xScale.getPixelForValue(x2); | |
| 49 | + | const pixelY1 = yScale.getPixelForValue(annotation.y_max); | |
| 50 | + | const pixelY2 = yScale.getPixelForValue(annotation.y_min); | |
| 51 | + | ||
| 52 | + | // Draw annotation rectangle | |
| 53 | + | ctx.save(); | |
| 54 | + | ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; | |
| 55 | + | ctx.fillRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 56 | + | ||
| 57 | + | // Rectangle border | |
| 58 | + | ctx.strokeStyle = 'rgb(200, 0, 0)'; | |
| 59 | + | ctx.strokeRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 60 | + | ||
| 61 | + | // Annotation label | |
| 62 | + | ctx.save(); | |
| 63 | + | const padding = 4; | |
| 64 | + | ctx.font = '10px monospace'; | |
| 65 | + | const textMetrics = ctx.measureText(annotation.label); | |
| 66 | + | const textHeight = 12; | |
| 67 | + | const radius = 4; | |
| 68 | + | ||
| 69 | + | const boxX = pixelX1 + padding; | |
| 70 | + | const boxY = pixelY1 - textHeight - padding * 2; | |
| 71 | + | const boxWidth = textMetrics.width + padding * 2; | |
| 72 | + | const boxHeight = textHeight + padding * 2; | |
| 73 | + | ||
| 74 | + | ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; | |
| 75 | + | ctx.shadowBlur = 4; | |
| 76 | + | ctx.shadowOffsetX = 2; | |
| 77 | + | ctx.shadowOffsetY = 2; | |
| 78 | + | ||
| 79 | + | ctx.fillStyle = 'rgba(200, 0, 0, 0.75)'; | |
| 80 | + | ctx.beginPath(); | |
| 81 | + | ctx.roundRect(boxX, boxY, boxWidth, boxHeight, radius); | |
| 82 | + | ctx.fill(); | |
| 83 | + | ||
| 84 | + | ctx.shadowColor = 'transparent'; | |
| 85 | + | ctx.strokeStyle = 'rgb(200, 0, 0)'; | |
| 86 | + | ctx.lineWidth = 1; | |
| 87 | + | ctx.stroke(); | |
| 88 | + | ||
| 89 | + | ctx.fillStyle = 'white'; | |
| 90 | + | ctx.textBaseline = 'bottom'; | |
| 91 | + | ctx.fillText(annotation.label, pixelX1 + padding * 2, pixelY1 - padding); | |
| 92 | + | ctx.restore(); | |
| 93 | + | }); | |
| 94 | + | } | |
| 95 | + | }; |
+25
-0
src/chart/chartjs-plugin-vertical-line.ts
#
| ... | ... | @@ -0,0 +1,25 @@ | |
| 1 | + | import { Chart as ChartJS } from "chart.js"; | |
| 2 | + | ||
| 3 | + | // ChartJS plugin to draw a vertical line on hover | |
| 4 | + | export const verticalLinePlugin = { | |
| 5 | + | id: 'verticalLine', | |
| 6 | + | beforeDraw: (chart: ChartJS) => { | |
| 7 | + | if (chart.tooltip?.getActiveElements()?.length) { | |
| 8 | + | const activePoint = chart.tooltip.getActiveElements()[0]; | |
| 9 | + | const ctx = chart.ctx; | |
| 10 | + | const x = activePoint.element.x; | |
| 11 | + | const topY = chart.scales.y.top; | |
| 12 | + | const bottomY = chart.scales.y.bottom; | |
| 13 | + | ||
| 14 | + | ctx.save(); | |
| 15 | + | ctx.beginPath(); | |
| 16 | + | ctx.moveTo(x, topY); | |
| 17 | + | ctx.lineTo(x, bottomY); | |
| 18 | + | ctx.lineWidth = 1; | |
| 19 | + | ctx.strokeStyle = '#94a3b8'; | |
| 20 | + | ctx.setLineDash([5, 5]); | |
| 21 | + | ctx.stroke(); | |
| 22 | + | ctx.restore(); | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | }; |
+3
-338
src/ui/pages/diagnostics-detail.tsx
#
| ... | ... | @@ -17,148 +17,8 @@ import { | |
| 17 | 17 | IconSource, | |
| 18 | 18 | IconInfo, | |
| 19 | 19 | } from "../shared/icons"; | |
| 20 | - | import { | |
| 21 | - | CategoryScale, | |
| 22 | - | Chart as ChartJS, | |
| 23 | - | Colors, | |
| 24 | - | Legend, | |
| 25 | - | LineElement, | |
| 26 | - | LinearScale, | |
| 27 | - | PointElement, | |
| 28 | - | TimeScale, | |
| 29 | - | type TimeUnit, | |
| 30 | - | Title, | |
| 31 | - | Tooltip, | |
| 32 | - | } from "chart.js"; | |
| 33 | - | import "chartjs-adapter-luxon"; | |
| 34 | - | import { Line } from "react-chartjs-2"; | |
| 35 | 20 | import { StreamingText } from "../shared/llm"; | |
| 36 | - | ||
| 37 | - | // Chart.js plugin to draw a vertical line on hover | |
| 38 | - | const verticalLinePlugin = { | |
| 39 | - | id: 'verticalLine', | |
| 40 | - | beforeDraw: (chart: ChartJS) => { | |
| 41 | - | if (chart.tooltip?.getActiveElements()?.length) { | |
| 42 | - | const activePoint = chart.tooltip.getActiveElements()[0]; | |
| 43 | - | const ctx = chart.ctx; | |
| 44 | - | const x = activePoint.element.x; | |
| 45 | - | const topY = chart.scales.y.top; | |
| 46 | - | const bottomY = chart.scales.y.bottom; | |
| 47 | - | ||
| 48 | - | ctx.save(); | |
| 49 | - | ctx.beginPath(); | |
| 50 | - | ctx.moveTo(x, topY); | |
| 51 | - | ctx.lineTo(x, bottomY); | |
| 52 | - | ctx.lineWidth = 1; | |
| 53 | - | ctx.strokeStyle = '#94a3b8'; | |
| 54 | - | ctx.setLineDash([5, 5]); | |
| 55 | - | ctx.stroke(); | |
| 56 | - | ctx.restore(); | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | }; | |
| 60 | - | ||
| 61 | - | // ChartJS plugin to draw annotations | |
| 62 | - | declare module 'chart.js' { | |
| 63 | - | interface Chart { | |
| 64 | - | annotationAreas?: Array<{ | |
| 65 | - | x1: number; | |
| 66 | - | x2: number; | |
| 67 | - | y1: number; | |
| 68 | - | y2: number; | |
| 69 | - | description: string; | |
| 70 | - | }>; | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | interface PluginOptionsByType<TType> { | |
| 74 | - | annotations?: Annotation[]; | |
| 75 | - | } | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | const annotationsPlugin = { | |
| 79 | - | id: 'annotations', | |
| 80 | - | afterDraw: (chart: ChartJS, args: any, options: any) => { | |
| 81 | - | const ctx = chart.ctx; | |
| 82 | - | const annotations = chart.options?.plugins?.annotations || []; | |
| 83 | - | ||
| 84 | - | annotations.forEach((annotation: any) => { | |
| 85 | - | // Convert timestamps to numbers for the time scale | |
| 86 | - | const xScale = chart.scales.x; | |
| 87 | - | const yScale = chart.scales.y; | |
| 88 | - | ||
| 89 | - | // Parse the timestamps into Date objects and get their timestamps | |
| 90 | - | const x1 = new Date(annotation.x_min).getTime(); | |
| 91 | - | const x2 = new Date(annotation.x_max).getTime(); | |
| 92 | - | ||
| 93 | - | // Convert to pixel coordinates | |
| 94 | - | const pixelX1 = xScale.getPixelForValue(x1); | |
| 95 | - | const pixelX2 = xScale.getPixelForValue(x2); | |
| 96 | - | const pixelY1 = yScale.getPixelForValue(annotation.y_max); | |
| 97 | - | const pixelY2 = yScale.getPixelForValue(annotation.y_min); | |
| 98 | - | ||
| 99 | - | // Draw annotation rectangle | |
| 100 | - | ctx.save(); | |
| 101 | - | ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // Solid red with 50% opacity | |
| 102 | - | ctx.fillRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 103 | - | ||
| 104 | - | // Rectangle border | |
| 105 | - | ctx.strokeStyle = 'rgb(200, 0, 0)'; // Solid darker red | |
| 106 | - | ctx.strokeRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 107 | - | ||
| 108 | - | // Annotation label | |
| 109 | - | ctx.save(); | |
| 110 | - | const padding = 4; | |
| 111 | - | ctx.font = '10px monospace'; // Reduced font size | |
| 112 | - | const textMetrics = ctx.measureText(annotation.label); | |
| 113 | - | const textHeight = 12; // Reduced height to match smaller font | |
| 114 | - | const radius = 4; // Border radius | |
| 115 | - | ||
| 116 | - | // Calculate label box dimensions | |
| 117 | - | const boxX = pixelX1 + padding; | |
| 118 | - | const boxY = pixelY1 - textHeight - padding * 2; | |
| 119 | - | const boxWidth = textMetrics.width + padding * 2; | |
| 120 | - | const boxHeight = textHeight + padding * 2; | |
| 121 | - | ||
| 122 | - | // Label box shadow | |
| 123 | - | ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; | |
| 124 | - | ctx.shadowBlur = 4; | |
| 125 | - | ctx.shadowOffsetX = 2; | |
| 126 | - | ctx.shadowOffsetY = 2; | |
| 127 | - | ||
| 128 | - | // Label box background | |
| 129 | - | ctx.fillStyle = 'rgba(200, 0, 0, 0.75)'; | |
| 130 | - | ctx.beginPath(); | |
| 131 | - | ctx.roundRect(boxX, boxY, boxWidth, boxHeight, radius); | |
| 132 | - | ctx.fill(); | |
| 133 | - | ||
| 134 | - | // Border | |
| 135 | - | ctx.shadowColor = 'transparent'; | |
| 136 | - | ctx.strokeStyle = 'rgb(200, 0, 0)'; | |
| 137 | - | ctx.lineWidth = 1; | |
| 138 | - | ctx.stroke(); | |
| 139 | - | ||
| 140 | - | // Label text | |
| 141 | - | ctx.fillStyle = 'white'; | |
| 142 | - | ctx.textBaseline = 'bottom'; | |
| 143 | - | ctx.fillText(annotation.label, pixelX1 + padding * 2, pixelY1 - padding); | |
| 144 | - | ctx.restore(); | |
| 145 | - | }); | |
| 146 | - | } | |
| 147 | - | }; | |
| 148 | - | ||
| 149 | - | ChartJS.register( | |
| 150 | - | CategoryScale, | |
| 151 | - | Colors, | |
| 152 | - | LinearScale, | |
| 153 | - | PointElement, | |
| 154 | - | LineElement, | |
| 155 | - | TimeScale, | |
| 156 | - | Title, | |
| 157 | - | Tooltip, | |
| 158 | - | Legend, | |
| 159 | - | verticalLinePlugin, | |
| 160 | - | annotationsPlugin | |
| 161 | - | ); | |
| 21 | + | import { DiagnosticsLineChart } from "../shared/diagnostics/line-chart"; | |
| 162 | 22 | ||
| 163 | 23 | type Message = { | |
| 164 | 24 | id: string; |
| ... | ... | @@ -526,204 +386,9 @@ const DiagnosticsResource = ({ | |
| 526 | 386 | ); | |
| 527 | 387 | }; | |
| 528 | 388 | ||
| 529 | - | const SynchronizedHoverLineChartWrapper = ({ | |
| 530 | - | showLegend = true, | |
| 531 | - | keyId, | |
| 532 | - | chart: { labels, datasets: originalDatasets, title }, | |
| 533 | - | xAxisUnit, | |
| 534 | - | yAxisLabel, | |
| 535 | - | yAxisUnit, | |
| 536 | - | annotations = [], | |
| 537 | - | synchronizedHoverContext, | |
| 538 | - | }: { | |
| 539 | - | showLegend?: boolean; | |
| 540 | - | keyId: string; | |
| 541 | - | chart: { | |
| 542 | - | title: string; | |
| 543 | - | labels: string[]; | |
| 544 | - | datasets: Array<{ | |
| 545 | - | label: string; | |
| 546 | - | data: number[]; | |
| 547 | - | }>; | |
| 548 | - | }; | |
| 549 | - | xAxisUnit: TimeUnit; | |
| 550 | - | yAxisLabel?: string; | |
| 551 | - | yAxisUnit?: string; | |
| 552 | - | annotations?: Annotation[]; | |
| 553 | - | synchronizedHoverContext: React.Context<HoverState>; | |
| 554 | - | }) => { | |
| 555 | - | const { timestamp, setTimestamp } = useContext(synchronizedHoverContext); | |
| 556 | - | const chartRef = React.useRef<ChartJS<"line">>(); | |
| 557 | - | ||
| 558 | - | // Truncate sha256 resource names to 8 chars | |
| 559 | - | const datasets = originalDatasets.map(dataset => ({ | |
| 560 | - | ...dataset, | |
| 561 | - | label: dataset.label.length === 64 ? dataset.label.slice(0, 8) : dataset.label | |
| 562 | - | })); | |
| 563 | - | ||
| 564 | - | if (!datasets || !title) return null; | |
| 565 | - | ||
| 566 | - | React.useEffect(() => { | |
| 567 | - | const chart = chartRef.current; | |
| 568 | - | if (!chart) return; | |
| 569 | - | ||
| 570 | - | if (!timestamp) { | |
| 571 | - | chart.setActiveElements([]); | |
| 572 | - | chart.tooltip?.setActiveElements([], { x: 0, y: 0 }); | |
| 573 | - | chart.update(); | |
| 574 | - | return; | |
| 575 | - | } | |
| 576 | - | ||
| 577 | - | const timestampIndex = labels.indexOf(timestamp); | |
| 578 | - | if (timestampIndex === -1) return; | |
| 579 | - | ||
| 580 | - | const activeElements = datasets.reduce<{ | |
| 581 | - | datasetIndex: number; | |
| 582 | - | index: number; | |
| 583 | - | }[]>((acc, dataset, datasetIndex) => { | |
| 584 | - | if (!dataset.data[timestampIndex]) return acc; | |
| 585 | - | ||
| 586 | - | return [ | |
| 587 | - | ...acc, | |
| 588 | - | { | |
| 589 | - | datasetIndex, | |
| 590 | - | index: timestampIndex, | |
| 591 | - | }, | |
| 592 | - | ]; | |
| 593 | - | }, []); | |
| 594 | - | ||
| 595 | - | chart.setActiveElements(activeElements); | |
| 596 | - | chart.tooltip?.setActiveElements(activeElements, { x: 0, y: 0 }); | |
| 597 | - | chart.update(); | |
| 598 | - | }, [timestamp, labels, datasets]); | |
| 599 | - | ||
| 600 | - | const formatYAxisTick = (value: number, unit?: string) => { | |
| 601 | - | if (!unit) return value; | |
| 602 | - | ||
| 603 | - | unit = unit.trim(); | |
| 604 | - | if (unit === '%') return `${value}%`; | |
| 605 | - | if (unit.endsWith('B')) return `${value}${unit}`; | |
| 606 | - | ||
| 607 | - | return value; | |
| 608 | - | }; | |
| 609 | - | ||
| 610 | - | return ( | |
| 611 | - | <Line | |
| 612 | - | ref={chartRef} | |
| 613 | - | datasetIdKey={keyId} | |
| 614 | - | data={{ | |
| 615 | - | labels, | |
| 616 | - | datasets, | |
| 617 | - | }} | |
| 618 | - | options={{ | |
| 619 | - | responsive: true, | |
| 620 | - | maintainAspectRatio: false, | |
| 621 | - | animation: false, | |
| 622 | - | plugins: { | |
| 623 | - | tooltip: { | |
| 624 | - | enabled: true, | |
| 625 | - | mode: 'index', | |
| 626 | - | intersect: false, | |
| 627 | - | }, | |
| 628 | - | colors: { | |
| 629 | - | forceOverride: true, | |
| 630 | - | }, | |
| 631 | - | legend: { | |
| 632 | - | display: showLegend, | |
| 633 | - | labels: { | |
| 634 | - | usePointStyle: true, | |
| 635 | - | boxHeight: 5, | |
| 636 | - | boxWidth: 3, | |
| 637 | - | padding: 20, | |
| 638 | - | }, | |
| 639 | - | }, | |
| 640 | - | title: { | |
| 641 | - | font: { | |
| 642 | - | size: 16, | |
| 643 | - | weight: "normal", | |
| 644 | - | }, | |
| 645 | - | color: "#595E63", | |
| 646 | - | align: "start", | |
| 647 | - | display: false, | |
| 648 | - | text: title, | |
| 649 | - | padding: showLegend | |
| 650 | - | ? undefined | |
| 651 | - | : { | |
| 652 | - | top: 10, | |
| 653 | - | bottom: 30, | |
| 654 | - | }, | |
| 655 | - | }, | |
| 656 | - | annotations: annotations, | |
| 657 | - | }, | |
| 658 | - | interaction: { | |
| 659 | - | mode: 'index', | |
| 660 | - | intersect: false, | |
| 661 | - | }, | |
| 662 | - | onHover: (event, elements, chart) => { | |
| 663 | - | if (!event.native) return; | |
| 664 | - | ||
| 665 | - | if (elements && elements.length > 0) { | |
| 666 | - | const timestamp = labels[elements[0].index]; | |
| 667 | - | setTimestamp(timestamp); | |
| 668 | - | } else { | |
| 669 | - | setTimestamp(null); | |
| 670 | - | } | |
| 671 | - | }, | |
| 672 | - | scales: { | |
| 673 | - | x: { | |
| 674 | - | border: { | |
| 675 | - | color: "#111920", | |
| 676 | - | }, | |
| 677 | - | grid: { | |
| 678 | - | display: false, | |
| 679 | - | }, | |
| 680 | - | ticks: { | |
| 681 | - | color: "#111920", | |
| 682 | - | maxRotation: 0, | |
| 683 | - | minRotation: 0, | |
| 684 | - | autoSkip: true, | |
| 685 | - | maxTicksLimit: 5, | |
| 686 | - | }, | |
| 687 | - | adapters: { | |
| 688 | - | date: { | |
| 689 | - | zone: "UTC", | |
| 690 | - | }, | |
| 691 | - | }, | |
| 692 | - | time: { | |
| 693 | - | tooltipFormat: "yyyy-MM-dd HH:mm:ss 'UTC'", | |
| 694 | - | unit: xAxisUnit, | |
| 695 | - | displayFormats: { | |
| 696 | - | minute: "HH:mm 'UTC'", | |
| 697 | - | day: "MMM dd", | |
| 698 | - | }, | |
| 699 | - | }, | |
| 700 | - | type: "time", | |
| 701 | - | }, | |
| 702 | - | y: { | |
| 703 | - | min: 0, | |
| 704 | - | border: { | |
| 705 | - | display: false, | |
| 706 | - | }, | |
| 707 | - | title: yAxisLabel | |
| 708 | - | ? { | |
| 709 | - | display: true, | |
| 710 | - | text: yAxisLabel, | |
| 711 | - | } | |
| 712 | - | : undefined, | |
| 713 | - | ticks: { | |
| 714 | - | callback: (value) => formatYAxisTick(value as number, yAxisUnit), | |
| 715 | - | color: "#111920", | |
| 716 | - | }, | |
| 717 | - | }, | |
| 718 | - | }, | |
| 719 | - | }} | |
| 720 | - | /> | |
| 721 | - | ); | |
| 722 | - | }; | |
| 723 | - | ||
| 724 | 389 | export const DiagnosticsDetailPage = () => { | |
| 725 | 390 | // Parse the investigation parameters from the query string. | |
| 726 | - | const [searchParams, setSearchParams] = useSearchParams(); | |
| 391 | + | const [searchParams] = useSearchParams(); | |
| 727 | 392 | const accessToken = useSelector(selectAccessToken); | |
| 728 | 393 | const appId = searchParams.get("appId"); | |
| 729 | 394 | const symptomDescription = searchParams.get("symptomDescription"); |