app-ui

created pr with 46.1 on 2025-02-06T15:20:57Z · by c8ef7d19
cmds
checkout latest patchset:
ssh pr.pico.sh print 46 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 46.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 46

Patchset 46.1 on 2025-02-06T15:20:57Z · commit 2a401b2

Install react-use-websocket
Eric Abruzzese 2025-01-15T14:28:15Z
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
Eric Abruzzese 2025-01-15T14:29:34Z
Collect events on the diagnostic details page and construct a dashboard state
Eric Abruzzese 2025-01-15T14:30:03Z
Formatting and cleanup
Eric Abruzzese 2025-01-15T14:34:02Z
Show dashboard messages, resources, operations, and plots
Michael Peterson 2025-01-28T20:23:45Z
Support async plot annotations
Eric Abruzzese 2025-01-31T17:53:30Z
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
Eric Abruzzese 2025-02-03T20:19:05Z
Add VITE_APTIBLE_AI_URL to .env.example
Eric Abruzzese 2025-02-04T16:30:13Z
Update VITE_APTIBLE_AI_URL references to point to Hotshot
Eric Abruzzese 2025-02-05T17:50:02Z
Update the analysis using PlotAnnotated events
Eric Abruzzese 2025-02-05T17:51:11Z
Install react-use-websocket
Eric Abruzzese 2025-01-15T14:28:15Z
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
Eric Abruzzese 2025-01-15T14:29:34Z
Collect events on the diagnostic details page and construct a dashboard state
Eric Abruzzese 2025-01-15T14:30:03Z
Formatting and cleanup
Eric Abruzzese 2025-01-15T14:34:02Z
Show dashboard messages, resources, operations, and plots
Michael Peterson 2025-01-28T20:23:45Z
Support async plot annotations
Eric Abruzzese 2025-01-31T17:53:30Z
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
Eric Abruzzese 2025-02-03T20:19:05Z
Add VITE_APTIBLE_AI_URL to .env.example
Eric Abruzzese 2025-02-04T16:30:13Z
Update VITE_APTIBLE_AI_URL references to point to Hotshot
Eric Abruzzese 2025-02-05T17:50:02Z
Update the analysis using PlotAnnotated events
Eric Abruzzese 2025-02-05T17:51:11Z
Refactor charts
Michael Peterson 2025-02-05T18:25:59Z
Refactor OperationsTimeline, chart hover state
Michael Peterson 2025-02-05T18:42:20Z
Extract types into aptible-ai/index, useDashboard hook
Michael Peterson 2025-02-05T19:21:56Z
Fix a linter error
Eric Abruzzese 2025-02-05T19:40:48Z
Fix linter errors
Eric Abruzzese 2025-02-05T19:50:07Z
+179 -74 src/ui/pages/diagnostics-detail.tsx #
......@@ -1,76 +1,192 @@
11 import { selectAptibleAiUrl } from "@app/config";
22 import { useSelector } from "@app/react";
3-import { diagnosticsCreateUrl, diagnosticsDetailUrl } from "@app/routes";
3+import { diagnosticsCreateUrl } from "@app/routes";
44 import { selectAccessToken } from "@app/token";
55 import { useEffect, useState } from "react";
6-import { useParams } from "react-router-dom";
6+import { Link, useSearchParams } from "react-router-dom";
77 import { AppSidebarLayout } from "../layouts";
8-import { Breadcrumbs, Loading, LoadingSpinner } from "../shared";
9-import { Button } from "../shared/button";
8+import { Breadcrumbs, PreText } from "../shared";
9+import useWebSocket, { ReadyState } from "react-use-websocket";
10+
11+type Message = {
12+ id: string;
13+ severity: string;
14+ message: string;
15+};
16+
17+type Operation = {
18+ id: number;
19+ status: string;
20+ created_at: Date;
21+ description: string;
22+ log_lines: string[];
23+};
24+
25+type Point = {
26+ timestamp: Date;
27+ value: number;
28+};
29+
30+type Annotation = {
31+ label: string;
32+ description: string;
33+ x_min: number;
34+ x_max: number;
35+ y_min: number;
36+ y_max: number;
37+};
38+
39+type Series = {
40+ label: string;
41+ description: string;
42+ interpretation: string;
43+ annotations: Annotation[];
44+ points: Point[];
45+};
46+
47+type Plot = {
48+ id: string;
49+ title: string;
50+ description: string;
51+ interpretation: string;
52+ analysis: string;
53+ unit: string;
54+ series: Series[];
55+ annotations: Annotation[];
56+};
57+
58+type Resource = {
59+ id: string;
60+ type: string;
61+ notes: string;
62+ plots: {
63+ [key: string]: Plot;
64+ };
65+ operations: Operation[];
66+};
67+
68+type Dashboard = {
69+ resources: {
70+ [key: string]: Resource;
71+ };
72+ messages: Message[];
73+};
1074
11-const loadingMessages = [
12- "Consulting the tech support crystal ball...",
13- "Teaching hamsters to debug code...",
14- "Bribing the servers with virtual cookies...",
15- "Performing diagnostic interpretive dance...",
16- "Teaching the AI to be less artificial and more intelligent...",
17-];
1875
1976 export const DiagnosticsDetailPage = () => {
20- const { id } = useParams();
21- const aptibleAiUrl = useSelector(selectAptibleAiUrl);
22- const dashboardUrl = `${aptibleAiUrl}/app/dashboards/${id}/`;
23- const [messageIndex, setMessageIndex] = useState(0);
24- const [isDashboardReady, setIsDashboardReady] = useState(false);
77+ // Parse the investigation parameters from the query string.
78+ const [searchParams, setSearchParams] = useSearchParams();
2579 const accessToken = useSelector(selectAccessToken);
80+ const appId = searchParams.get("app_id");
81+ const symptomDescription = searchParams.get("symptom_description");
82+ const startTime = searchParams.get("start_time");
83+ const endTime = searchParams.get("end_time");
2684
27- useEffect(() => {
28- const checkDashboard = async () => {
29- try {
30- // TODO: Figure out how to get a status code from an action, so that we
31- // can swap out this fetch implementation.
32- const response = await fetch(dashboardUrl, {
33- // Credentials are included to allow aptible-ai to set the session
34- // cookie, effectively logging us in.
35- credentials: "include",
36- headers: {
37- Authorization: `Bearer ${accessToken}`,
38- },
39- });
40- if (response.ok) {
41- setIsDashboardReady(true);
42- return true;
43- }
44- } catch (error) {
45- // Ignore errors - we'll try again
46- }
47- return false;
48- };
49-
50- let interval: NodeJS.Timeout;
51-
52- const initialize = async () => {
53- // Check immediately, in case the dashboard has already been created.
54- const isReady = await checkDashboard();
55-
56- // Only set up the interval if the dashboard isn't ready yet.
57- if (!isReady) {
58- interval = setInterval(async () => {
59- setMessageIndex((current) => (current + 1) % loadingMessages.length);
60- const ready = await checkDashboard();
61- if (ready) {
62- clearInterval(interval);
63- }
64- }, 3000);
85+ // If any of the parameters are missing, display an error message with a link
86+ // to the diagnostics create page.
87+ if (!appId || !symptomDescription || !startTime || !endTime) {
88+ return (
89+ <div>
90+ <p>Error: Missing parameters</p>
91+ <Link to={diagnosticsCreateUrl()}>Go back to diagnostics page</Link>
92+ </div>
93+ );
94+ }
95+
96+ // Connect to the Aptible AI WebSocket.
97+ const aptibleAiUrl = useSelector(selectAptibleAiUrl);
98+ const [socketConnected, setSocketConnected] = useState(true);
99+ const { lastJsonMessage: event, readyState } = useWebSocket<Record<string, any>>(
100+ `${aptibleAiUrl}/troubleshoot`,
101+ {
102+ queryParams: {
103+ token: accessToken,
104+ resource_id: appId,
105+ symptom_description: symptomDescription,
106+ start_time: startTime,
107+ end_time: endTime,
65108 }
66- };
109+ },
110+ socketConnected
111+ );
67112
68- initialize();
113+ // If the socket is closed, set the socketConnected state to false (this is
114+ // mostly helpful for hot reloading, since the socket will typically close on
115+ // its own under normal circumstances).
116+ useEffect(() => {
117+ if (readyState === ReadyState.CLOSED) {
118+ setSocketConnected(false);
119+ }
120+ }, [readyState]);
121+
122+ const [dashboard, setDashboard] = useState<Dashboard>({
123+ resources: {},
124+ messages: [],
125+ });
69126
70- return () => {
71- if (interval) clearInterval(interval);
72- };
73- }, [id, dashboardUrl, accessToken]);
127+ // Process each event from the websocket, and update the dashboard state.
128+ useEffect(() => {
129+ if (event?.type === "ResourceDiscovered") {
130+ setDashboard((prev) => ({
131+ ...prev,
132+ resources: {
133+ ...prev.resources,
134+ [event.resource_id]: {
135+ id: event.resource_id,
136+ type: event.resource_type,
137+ notes: event.notes,
138+ metrics: [],
139+ operations: [],
140+ },
141+ },
142+ }));
143+ } else if (event?.type === "ResourceMetricsRetrieved") {
144+ setDashboard((prev) => ({
145+ ...prev,
146+ resources: {
147+ ...prev.resources,
148+ [event.resource_id]: {
149+ ...prev.resources[event.resource_id],
150+ plots: {
151+ ...prev.resources[event.resource_id].plots,
152+ [event.metric_name]: {
153+ name: event.metric_name,
154+ plot: event.plot,
155+ },
156+ },
157+ },
158+ },
159+ }));
160+ } else if (event?.type === "ResourceOperationsRetrieved") {
161+ setDashboard((prev) => ({
162+ ...prev,
163+ resources: {
164+ ...prev.resources,
165+ [event.resource_id]: {
166+ ...prev.resources[event.resource_id],
167+ operations: [
168+ ...prev.resources[event.resource_id].operations,
169+ ...event.operations,
170+ ],
171+ },
172+ },
173+ }));
174+ } else if (event?.type === "Message") {
175+ setDashboard((prev) => ({
176+ ...prev,
177+ messages: [
178+ ...prev.messages,
179+ {
180+ id: event.id,
181+ severity: event.severity,
182+ message: event.message,
183+ },
184+ ],
185+ }));
186+ } else {
187+ console.log(`Unhandled event type ${event?.type}`, event);
188+ }
189+ }, [JSON.stringify(event)]);
74190
75191 return (
76192 <AppSidebarLayout>
......@@ -81,25 +197,14 @@ export const DiagnosticsDetailPage = () => {
81197 to: diagnosticsCreateUrl(),
82198 },
83199 {
84- name: `${id}`,
85- to: diagnosticsDetailUrl(`${id}`),
200+ name: `${appId} (${symptomDescription})`,
201+ to: window.location.href,
86202 },
87203 ]}
88204 />
89205
90206 <div className="flex flex-row items-center justify-center flex-1 min-h-[500px]">
91- <div className="scale-150 flex flex-row items-center gap-3">
92- {!isDashboardReady ? (
93- <>
94- <LoadingSpinner />
95- <Loading text={loadingMessages[messageIndex]} />
96- </>
97- ) : (
98- <form action={dashboardUrl} target="_blank" method="post">
99- <Button type="submit">View Dashboard</Button>
100- </form>
101- )}
102- </div>
207+ <PreText className="max-w-7xl overflow-x-auto overflow-y-auto" text={JSON.stringify(dashboard, null, 2)} allowCopy />
103208 </div>
104209 </AppSidebarLayout>
105210 );
Back to top