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
Fix linter errors
Eric Abruzzese
2025-02-05T19:50:07ZSemantic diff summary
0 added,
42 modified,
0 signature changed,
0 removed
across 10 analyzed files
+14
-15
src/chart/chartjs-plugin-annotations.ts
#
| ... | ... | @@ -1,9 +1,8 @@ | |
| 1 | - | import { Chart as ChartJS } from "chart.js"; | |
| 2 | - | import { type Annotation } from "@app/aptible-ai"; | |
| 3 | - | ||
| 1 | + | import type { Annotation } from "@app/aptible-ai"; | |
| 2 | + | import type { Chart as ChartJS } from "chart.js"; | |
| 4 | 3 | ||
| 5 | 4 | // Update ChartJS interface to include annotations | |
| 6 | - | declare module 'chart.js' { | |
| 5 | + | declare module "chart.js" { | |
| 7 | 6 | interface Chart { | |
| 8 | 7 | annotationAreas?: Array<{ | |
| 9 | 8 | x1: number; |
| ... | ... | @@ -21,7 +20,7 @@ declare module 'chart.js' { | |
| 21 | 20 | ||
| 22 | 21 | // ChartJS plugin to draw annotations on a line chart | |
| 23 | 22 | export const annotationsPlugin = { | |
| 24 | - | id: 'annotations', | |
| 23 | + | id: "annotations", | |
| 25 | 24 | afterDraw: (chart: ChartJS, args: any, options: any) => { | |
| 26 | 25 | const ctx = chart.ctx; | |
| 27 | 26 | const annotations = chart.options?.plugins?.annotations || []; |
| ... | ... | @@ -43,17 +42,17 @@ export const annotationsPlugin = { | |
| 43 | 42 | ||
| 44 | 43 | // Draw annotation rectangle | |
| 45 | 44 | ctx.save(); | |
| 46 | - | ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; | |
| 45 | + | ctx.fillStyle = "rgba(255, 0, 0, 0.5)"; | |
| 47 | 46 | ctx.fillRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 48 | 47 | ||
| 49 | 48 | // Rectangle border | |
| 50 | - | ctx.strokeStyle = 'rgb(200, 0, 0)'; | |
| 49 | + | ctx.strokeStyle = "rgb(200, 0, 0)"; | |
| 51 | 50 | ctx.strokeRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1); | |
| 52 | 51 | ||
| 53 | 52 | // Annotation label | |
| 54 | 53 | ctx.save(); | |
| 55 | 54 | const padding = 4; | |
| 56 | - | ctx.font = '10px monospace'; | |
| 55 | + | ctx.font = "10px monospace"; | |
| 57 | 56 | const textMetrics = ctx.measureText(annotation.label); | |
| 58 | 57 | const textHeight = 12; | |
| 59 | 58 | const radius = 4; |
| ... | ... | @@ -63,25 +62,25 @@ export const annotationsPlugin = { | |
| 63 | 62 | const boxWidth = textMetrics.width + padding * 2; | |
| 64 | 63 | const boxHeight = textHeight + padding * 2; | |
| 65 | 64 | ||
| 66 | - | ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; | |
| 65 | + | ctx.shadowColor = "rgba(0, 0, 0, 0.3)"; | |
| 67 | 66 | ctx.shadowBlur = 4; | |
| 68 | 67 | ctx.shadowOffsetX = 2; | |
| 69 | 68 | ctx.shadowOffsetY = 2; | |
| 70 | 69 | ||
| 71 | - | ctx.fillStyle = 'rgba(200, 0, 0, 0.75)'; | |
| 70 | + | ctx.fillStyle = "rgba(200, 0, 0, 0.75)"; | |
| 72 | 71 | ctx.beginPath(); | |
| 73 | 72 | ctx.roundRect(boxX, boxY, boxWidth, boxHeight, radius); | |
| 74 | 73 | ctx.fill(); | |
| 75 | 74 | ||
| 76 | - | ctx.shadowColor = 'transparent'; | |
| 77 | - | ctx.strokeStyle = 'rgb(200, 0, 0)'; | |
| 75 | + | ctx.shadowColor = "transparent"; | |
| 76 | + | ctx.strokeStyle = "rgb(200, 0, 0)"; | |
| 78 | 77 | ctx.lineWidth = 1; | |
| 79 | 78 | ctx.stroke(); | |
| 80 | 79 | ||
| 81 | - | ctx.fillStyle = 'white'; | |
| 82 | - | ctx.textBaseline = 'bottom'; | |
| 80 | + | ctx.fillStyle = "white"; | |
| 81 | + | ctx.textBaseline = "bottom"; | |
| 83 | 82 | ctx.fillText(annotation.label, pixelX1 + padding * 2, pixelY1 - padding); | |
| 84 | 83 | ctx.restore(); | |
| 85 | 84 | }); | |
| 86 | - | } | |
| 85 | + | }, | |
| 87 | 86 | }; |
+4
-4
src/chart/chartjs-plugin-vertical-line.ts
#
| ... | ... | @@ -1,8 +1,8 @@ | |
| 1 | - | import { Chart as ChartJS } from "chart.js"; | |
| 1 | + | import type { Chart as ChartJS } from "chart.js"; | |
| 2 | 2 | ||
| 3 | 3 | // ChartJS plugin to draw a vertical line on hover | |
| 4 | 4 | export const verticalLinePlugin = { | |
| 5 | - | id: 'verticalLine', | |
| 5 | + | id: "verticalLine", | |
| 6 | 6 | beforeDraw: (chart: ChartJS) => { | |
| 7 | 7 | if (chart.tooltip?.getActiveElements()?.length) { | |
| 8 | 8 | const activePoint = chart.tooltip.getActiveElements()[0]; |
| ... | ... | @@ -16,10 +16,10 @@ export const verticalLinePlugin = { | |
| 16 | 16 | ctx.moveTo(x, topY); | |
| 17 | 17 | ctx.lineTo(x, bottomY); | |
| 18 | 18 | ctx.lineWidth = 1; | |
| 19 | - | ctx.strokeStyle = '#94a3b8'; | |
| 19 | + | ctx.strokeStyle = "#94a3b8"; | |
| 20 | 20 | ctx.setLineDash([5, 5]); | |
| 21 | 21 | ctx.stroke(); | |
| 22 | 22 | ctx.restore(); | |
| 23 | 23 | } | |
| 24 | - | } | |
| 24 | + | }, | |
| 25 | 25 | }; |
+22
-10
src/ui/hooks/use-dashboard.ts
#
| ... | ... | @@ -1,9 +1,9 @@ | |
| 1 | - | import { useEffect, useState } from "react"; | |
| 2 | - | import { useSelector } from "@app/react"; | |
| 1 | + | import type { Message, Resource } from "@app/aptible-ai"; | |
| 3 | 2 | import { selectAptibleAiUrl } from "@app/config"; | |
| 3 | + | import { useSelector } from "@app/react"; | |
| 4 | 4 | import { selectAccessToken } from "@app/token"; | |
| 5 | + | import { useEffect, useState } from "react"; | |
| 5 | 6 | import useWebSocket, { ReadyState } from "react-use-websocket"; | |
| 6 | - | import { type Message, type Resource } from "@app/aptible-ai"; | |
| 7 | 7 | ||
| 8 | 8 | type Dashboard = { | |
| 9 | 9 | resources: { |
| ... | ... | @@ -19,7 +19,10 @@ type UseDashboardParams = { | |
| 19 | 19 | endTime: string; | |
| 20 | 20 | }; | |
| 21 | 21 | ||
| 22 | - | const handleDashboardEvent = (dashboard: Dashboard, event: Record<string, any>): Dashboard => { | |
| 22 | + | const handleDashboardEvent = ( | |
| 23 | + | dashboard: Dashboard, | |
| 24 | + | event: Record<string, any>, | |
| 25 | + | ): Dashboard => { | |
| 23 | 26 | switch (event?.type) { | |
| 24 | 27 | case "ResourceDiscovered": | |
| 25 | 28 | return { |
| ... | ... | @@ -108,7 +111,12 @@ const handleDashboardEvent = (dashboard: Dashboard, event: Record<string, any>): | |
| 108 | 111 | } | |
| 109 | 112 | }; | |
| 110 | 113 | ||
| 111 | - | export const useDashboard = ({ appId, symptomDescription, startTime, endTime }: UseDashboardParams) => { | |
| 114 | + | export const useDashboard = ({ | |
| 115 | + | appId, | |
| 116 | + | symptomDescription, | |
| 117 | + | startTime, | |
| 118 | + | endTime, | |
| 119 | + | }: UseDashboardParams) => { | |
| 112 | 120 | const aptibleAiUrl = useSelector(selectAptibleAiUrl); | |
| 113 | 121 | const accessToken = useSelector(selectAccessToken); | |
| 114 | 122 | const [socketConnected, setSocketConnected] = useState(true); |
| ... | ... | @@ -118,7 +126,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }: | |
| 118 | 126 | }); | |
| 119 | 127 | const [hasShownCompletion, setHasShownCompletion] = useState(false); | |
| 120 | 128 | ||
| 121 | - | const { lastJsonMessage: event, readyState } = useWebSocket<Record<string, any>>( | |
| 129 | + | const { lastJsonMessage: event, readyState } = useWebSocket< | |
| 130 | + | Record<string, any> | |
| 131 | + | >( | |
| 122 | 132 | `${aptibleAiUrl}/troubleshoot`, | |
| 123 | 133 | { | |
| 124 | 134 | queryParams: { |
| ... | ... | @@ -140,7 +150,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }: | |
| 140 | 150 | ||
| 141 | 151 | useEffect(() => { | |
| 142 | 152 | if (event) { | |
| 143 | - | setDashboard(prevDashboard => handleDashboardEvent(prevDashboard, event)); | |
| 153 | + | setDashboard((prevDashboard) => | |
| 154 | + | handleDashboardEvent(prevDashboard, event), | |
| 155 | + | ); | |
| 144 | 156 | } | |
| 145 | 157 | }, [JSON.stringify(event)]); | |
| 146 | 158 |
| ... | ... | @@ -153,9 +165,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }: | |
| 153 | 165 | messages: [ | |
| 154 | 166 | ...prev.messages, | |
| 155 | 167 | { | |
| 156 | - | id: 'completion-message', | |
| 157 | - | severity: 'info', | |
| 158 | - | message: 'Analysis complete.', | |
| 168 | + | id: "completion-message", | |
| 169 | + | severity: "info", | |
| 170 | + | message: "Analysis complete.", | |
| 159 | 171 | }, | |
| 160 | 172 | ], | |
| 161 | 173 | })); |
+16
-12
src/ui/pages/diagnostics-detail.tsx
#
| ... | ... | @@ -1,12 +1,12 @@ | |
| 1 | 1 | import { diagnosticsCreateUrl } from "@app/routes"; | |
| 2 | 2 | import { useState } from "react"; | |
| 3 | 3 | import { useSearchParams } from "react-router-dom"; | |
| 4 | + | import { useDashboard } from "../hooks/use-dashboard"; | |
| 4 | 5 | import { AppSidebarLayout } from "../layouts"; | |
| 5 | 6 | import { Breadcrumbs } from "../shared"; | |
| 6 | 7 | import { HoverContext } from "../shared/diagnostics/hover"; | |
| 7 | 8 | import { DiagnosticsMessages } from "../shared/diagnostics/messages"; | |
| 8 | 9 | import { DiagnosticsResource } from "../shared/diagnostics/resource"; | |
| 9 | - | import { useDashboard } from "../hooks/use-dashboard"; | |
| 10 | 10 | ||
| 11 | 11 | export const DiagnosticsDetailPage = () => { | |
| 12 | 12 | const [searchParams] = useSearchParams(); |
| ... | ... | @@ -45,7 +45,9 @@ export const DiagnosticsDetailPage = () => { | |
| 45 | 45 | /> | |
| 46 | 46 | ||
| 47 | 47 | <div className="flex flex-col gap-4 p-4"> | |
| 48 | - | <HoverContext.Provider value={{ timestamp: hoverTimestamp, setTimestamp: setHoverTimestamp }}> | |
| 48 | + | <HoverContext.Provider | |
| 49 | + | value={{ timestamp: hoverTimestamp, setTimestamp: setHoverTimestamp }} | |
| 50 | + | > | |
| 49 | 51 | <DiagnosticsMessages | |
| 50 | 52 | messages={dashboard.messages} | |
| 51 | 53 | showAllMessages={showAllMessages} |
| ... | ... | @@ -54,16 +56,18 @@ export const DiagnosticsDetailPage = () => { | |
| 54 | 56 | ||
| 55 | 57 | <h2 className="text-lg font-semibold mb-2">Resources</h2> | |
| 56 | 58 | <div className="space-y-4"> | |
| 57 | - | {Object.entries(dashboard.resources).map(([resourceId, resource]) => ( | |
| 58 | - | <DiagnosticsResource | |
| 59 | - | key={resourceId} | |
| 60 | - | resourceId={resourceId} | |
| 61 | - | resource={resource} | |
| 62 | - | startTime={startTime!} | |
| 63 | - | endTime={endTime!} | |
| 64 | - | synchronizedHoverContext={HoverContext} | |
| 65 | - | /> | |
| 66 | - | ))} | |
| 59 | + | {Object.entries(dashboard.resources).map( | |
| 60 | + | ([resourceId, resource]) => ( | |
| 61 | + | <DiagnosticsResource | |
| 62 | + | key={resourceId} | |
| 63 | + | resourceId={resourceId} | |
| 64 | + | resource={resource} | |
| 65 | + | startTime={startTime} | |
| 66 | + | endTime={endTime} | |
| 67 | + | synchronizedHoverContext={HoverContext} | |
| 68 | + | /> | |
| 69 | + | ), | |
| 70 | + | )} | |
| 67 | 71 | </div> | |
| 68 | 72 | </HoverContext.Provider> | |
| 69 | 73 | </div> |