diff --git a/server/app/query/page.tsx b/server/app/query/page.tsx index fca22de..3262b94 100644 --- a/server/app/query/page.tsx +++ b/server/app/query/page.tsx @@ -5,23 +5,17 @@ import Link from "next/link"; import { StatusPill, RunTimePill } from "@/app/query/view/[id]/components"; import { - Status, + StatusEvent, RemoteServer, RemoteServerNames, IPARemoteServers, //hack until the queryId is stored in a DB - StatusByRemoteServer, - StartTimeByRemoteServer, - EndTimeByRemoteServer, - initialStatusByRemoteServer, - initialStartTimeByRemoteServer, - initialEndTimeByRemoteServer, + StatusEventByRemoteServer, + initialStatusEventByRemoteServer, } from "@/app/query/servers"; import { getQueryByUUID, Query } from "@/data/query"; type QueryData = { - status: StatusByRemoteServer; - startTime: StartTimeByRemoteServer; - endTime: EndTimeByRemoteServer; + statusEvent: StatusEventByRemoteServer; query: Query; }; type DataByQuery = { @@ -35,8 +29,7 @@ export default function Page() { const updateData = ( query: Query, remoteServer: RemoteServer, - key: keyof QueryData, - value: Status | number, + statusEvent: StatusEvent, ) => { setDataByQuery((prev) => { let _prev = prev; @@ -46,9 +39,7 @@ export default function Page() { _prev = { ..._prev, [query.uuid]: { - status: initialStatusByRemoteServer, - startTime: initialStartTimeByRemoteServer, - endTime: initialEndTimeByRemoteServer, + statusEvent: initialStatusEventByRemoteServer, query: query, }, }; @@ -58,9 +49,9 @@ export default function Page() { ..._prev, [query.uuid]: { ..._prev[query.uuid], - [key]: { - ..._prev[query.uuid][key], - [remoteServer.remoteServerName]: value, + statusEvent: { + ..._prev[query.uuid].statusEvent, + [remoteServer.remoteServerName]: statusEvent, }, }, }; @@ -82,37 +73,25 @@ export default function Page() { useEffect(() => { (async () => { - let webSockets: WebSocket[] = []; - - // remove queries when no longer running - const filteredDataByQuery = Object.fromEntries( - Object.keys(dataByQuery) - .filter((queryID) => queryIDs.includes(queryID)) - .map((queryID) => [queryID, dataByQuery[queryID]]), - ); - setDataByQuery(filteredDataByQuery); + setDataByQuery((prev) => { + return Object.fromEntries( + Object.keys(prev) + .filter((queryID) => queryIDs.includes(queryID)) + .map((queryID) => [queryID, prev[queryID]]), + ); + }); for (const queryID of queryIDs) { const query: Query = await getQueryByUUID(queryID); for (const remoteServer of Object.values(IPARemoteServers)) { - const statusWs = remoteServer.openStatusSocket( - queryID, - (status) => updateData(query, remoteServer, "status", status), - (startTime) => - updateData(query, remoteServer, "startTime", startTime), - (endTime) => updateData(query, remoteServer, "endTime", endTime), - ); - webSockets = [...webSockets, statusWs]; + const statusEvent: StatusEvent = + await remoteServer.queryStatus(queryID); + updateData(query, remoteServer, statusEvent); } } - return () => { - for (const ws of webSockets) { - ws.close(); - } - }; })(); - }, [queryIDs, dataByQuery]); + }, [queryIDs]); return ( <> @@ -122,10 +101,14 @@ export default function Page() { Current Queries + {Object.keys(dataByQuery).length == 0 && ( +

+ None currently running. +

+ )} + {Object.entries(dataByQuery).map(([queryID, queryData]) => { - const statusByRemoteServer = queryData.status; - const startTimeByRemoteServer = queryData.startTime; - const endTimeByRemoteServer = queryData.endTime; + const statusEventByRemoteServer = queryData.statusEvent; const query = queryData.query; return ( @@ -142,19 +125,16 @@ export default function Page() {
{Object.values(IPARemoteServers).map( (remoteServer: RemoteServer) => { - const startTime = - startTimeByRemoteServer[ + const statusEvent: StatusEvent | null = + statusEventByRemoteServer[ remoteServer.remoteServerName ]; - const endTime = - endTimeByRemoteServer[ - remoteServer.remoteServerName - ]; - - const status = - statusByRemoteServer[ - remoteServer.remoteServerName - ] ?? Status.UNKNOWN; + if (statusEvent === null) { + return <>; + } + const status = statusEvent.status; + const startTime = statusEvent.startTime; + const endTime = statusEvent.endTime; return (
- +
); @@ -181,10 +157,14 @@ export default function Page() {
{Object.values(IPARemoteServers).map( (remoteServer: RemoteServer) => { - const status = - statusByRemoteServer[ + const statusEvent: StatusEvent | null = + statusEventByRemoteServer[ remoteServer.remoteServerName - ] ?? Status.UNKNOWN; + ]; + if (statusEvent === null) { + return <>; + } + const status = statusEvent.status; return (
[[serverName], null]), ); -export type StatsByRemoteServer = { - [key in RemoteServerNames]: StatsDataPoint[]; -}; - -export const initialStatsByRemoteServer: StatsByRemoteServer = - Object.fromEntries( - Object.values(RemoteServerNames).map((serverName) => [[serverName], []]), - ); - export type StartTimeByRemoteServer = { [key in RemoteServerNames]: number | null; }; @@ -67,6 +66,10 @@ export type EndTimeByRemoteServer = { [key in RemoteServerNames]: number | null; }; +export type StatusEventByRemoteServer = { + [key in RemoteServerNames]: StatusEvent | null; +}; + export const initialStartTimeByRemoteServer: StartTimeByRemoteServer = Object.fromEntries( Object.values(RemoteServerNames).map((serverName) => [[serverName], null]), @@ -77,6 +80,26 @@ export const initialEndTimeByRemoteServer: StartTimeByRemoteServer = Object.values(RemoteServerNames).map((serverName) => [[serverName], null]), ); +export const initialStatusEventByRemoteServer: StatusEventByRemoteServer = + Object.fromEntries( + Object.values(RemoteServerNames).map((serverName) => [[serverName], null]), + ); + +export interface StatsDataPoint { + timestamp: string; + memoryRSSUsage: number; + cpuUsage: number; +} + +export type StatsByRemoteServer = { + [key in RemoteServerNames]: StatsDataPoint[]; +}; + +export const initialStatsByRemoteServer: StatsByRemoteServer = + Object.fromEntries( + Object.values(RemoteServerNames).map((serverName) => [[serverName], []]), + ); + export class RemoteServer { protected baseURL: URL; remoteServerName: RemoteServerNames; @@ -96,6 +119,16 @@ export class RemoteServer { throw new Error("Not Implemented"); } + queryStatusURL(id: string): URL { + return new URL(`/start/${id}/status`, this.baseURL); + } + + async queryStatus(id: string): Promise { + const status_response = await fetch(this.queryStatusURL(id)); + const statusJSON = await status_response.json(); + return buildStatusEventFromJSON(statusJSON); + } + runningQueriesURL(): URL { return new URL(`/start/running-queries`, this.baseURL); } @@ -103,7 +136,7 @@ export class RemoteServer { async runningQueries(): Promise { const queries_response = await fetch(this.runningQueriesURL()); const queriesJSON = await queries_response.json(); - return queriesJSON["running_queries"]; + return queriesJSON.running_queries; } logURL(id: string): URL { @@ -197,31 +230,13 @@ export class RemoteServer { openStatusSocket( id: string, - setStatus: (status: Status) => void, - setStartTime: (startTime: number) => void, - setEndTime: (endTime: number) => void, + setStatusEvent: (statusEvent: StatusEvent) => void, ): WebSocket { const ws = this.statusSocket(id); - const updateStatus = (status: Status) => { - setStatus(status); - }; - - const updateStartTime = (startTime: number) => { - setStartTime(startTime); - }; - - const updateEndTime = (endTime: number) => { - setEndTime(endTime); - }; - ws.onmessage = (event) => { - const eventData = JSON.parse(event.data); - updateStartTime(eventData.start_time); - updateEndTime(eventData.end_time ?? null); - const statusString: string = eventData.status; - const status = getStatusFromString(statusString); - updateStatus(status); + const statusEvent = buildStatusEventFromJSON(JSON.parse(event.data)); + setStatusEvent(statusEvent); }; ws.onclose = (event) => { diff --git a/server/app/query/view/[id]/components.tsx b/server/app/query/view/[id]/components.tsx index 5aaf35a..2411ddd 100644 --- a/server/app/query/view/[id]/components.tsx +++ b/server/app/query/view/[id]/components.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from "react"; import { Source_Code_Pro } from "next/font/google"; import clsx from "clsx"; import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/solid"; -import { Status, ServerLog } from "@/app/query/servers"; +import { Status, StatusEvent, ServerLog } from "@/app/query/servers"; const sourceCodePro = Source_Code_Pro({ subsets: ["latin"] }); @@ -64,15 +64,7 @@ function secondsToTime(e: number) { return h + ":" + m + ":" + s; } -export function RunTimePill({ - status, - startTime, - endTime, -}: { - status: Status; - startTime: number | null; - endTime: number | null; -}) { +export function RunTimePill({ statusEvent }: { statusEvent: StatusEvent }) { const [runTime, setRunTime] = useState(null); const runTimeStr = runTime ? secondsToTime(runTime) : "N/A"; const intervalId = useRef | null>(null); @@ -83,22 +75,27 @@ export function RunTimePill({ // which runs the timer. if a new one is needed, it's created. clearTimeout(intervalId.current); } - if (startTime === null) { + if (statusEvent?.startTime === null) { setRunTime(null); } else { - if (endTime !== null) { - setRunTime(endTime - startTime); + if (statusEvent?.endTime !== null) { + setRunTime(statusEvent.endTime - statusEvent.startTime); } else { let newIntervalId = setInterval(() => { - setRunTime(Date.now() / 1000 - startTime); + setRunTime(Date.now() / 1000 - statusEvent.startTime); }, 1000); intervalId.current = newIntervalId; } } - }, [startTime, endTime]); + }, [statusEvent]); return ( -
+
{runTimeStr}
); diff --git a/server/app/query/view/[id]/page.tsx b/server/app/query/view/[id]/page.tsx index 29cbdf7..02cfbc0 100644 --- a/server/app/query/view/[id]/page.tsx +++ b/server/app/query/view/[id]/page.tsx @@ -9,6 +9,7 @@ import { } from "@/app/query/view/[id]/components"; import { Status, + StatusEvent, ServerLog, RemoteServer, RemoteServerNames, @@ -18,10 +19,12 @@ import { StatsByRemoteServer, StartTimeByRemoteServer, EndTimeByRemoteServer, + StatusEventByRemoteServer, initialStatusByRemoteServer, initialStatsByRemoteServer, initialStartTimeByRemoteServer, initialEndTimeByRemoteServer, + initialStatusEventByRemoteServer, } from "@/app/query/servers"; import { StatsComponent } from "@/app/query/view/[id]/charts"; import { JSONSafeParse } from "@/app/utils"; @@ -46,40 +49,21 @@ export default function QueryPage({ params }: { params: { id: string } }) { selectedRemoteServerLogs.includes(item.remoteServer.remoteServerNameStr), ); - const [statusByRemoteServer, setStatusByRemoteServer] = - useState(initialStatusByRemoteServer); const [statsByRemoteServer, setStatsByRemoteServer] = useState(initialStatsByRemoteServer); - const [startTimeByRemoteServer, setStartTimeByRemoteServer] = - useState(initialStartTimeByRemoteServer); - const [endTimeByRemoteServer, setEndTimeByRemoteServer] = - useState(initialEndTimeByRemoteServer); + const [statusEventByRemoteServer, setStatusEventByRemoteServer] = + useState(initialStatusEventByRemoteServer); - const updateStatus = (remoteServer: RemoteServer, status: Status) => { - setStatusByRemoteServer((prevStatus) => ({ + const updateStatusEvent = ( + remoteServer: RemoteServer, + statusEvent: StatusEvent, + ) => { + setStatusEventByRemoteServer((prevStatus) => ({ ...prevStatus, - [remoteServer.remoteServerName]: status, + [remoteServer.remoteServerName]: statusEvent, })); }; - const updateStartTime = (remoteServer: RemoteServer, runTime: number) => { - setStartTimeByRemoteServer((prevStartTime) => { - return { - ...prevStartTime, - [remoteServer.remoteServerName]: runTime, - }; - }); - }; - - const updateEndTime = (remoteServer: RemoteServer, runTime: number) => { - setEndTimeByRemoteServer((prevEndTime) => { - return { - ...prevEndTime, - [remoteServer.remoteServerName]: runTime, - }; - }); - }; - function flipLogsHidden() { setLogsHidden(!logsHidden); } @@ -136,9 +120,7 @@ export default function QueryPage({ params }: { params: { id: string } }) { const loggingWs = remoteServer.openLogSocket(query.uuid, setLogs); const statusWs = remoteServer.openStatusSocket( query.uuid, - (status) => updateStatus(remoteServer, status), - (startTime) => updateStartTime(remoteServer, startTime), - (endTime) => updateEndTime(remoteServer, endTime), + (statusEvent) => updateStatusEvent(remoteServer, statusEvent), ); const statsWs = remoteServer.openStatsSocket( query.uuid, @@ -242,14 +224,11 @@ export default function QueryPage({ params }: { params: { id: string } }) {
{Object.values(IPARemoteServers).map( (remoteServer: RemoteServer) => { - const startTime = - startTimeByRemoteServer[remoteServer.remoteServerName]; - const endTime = - endTimeByRemoteServer[remoteServer.remoteServerName]; - - const status = - statusByRemoteServer[remoteServer.remoteServerName] ?? - Status.UNKNOWN; + const statusEvent: StatusEvent | null = + statusEventByRemoteServer[remoteServer.remoteServerName]; + if (statusEvent === null) { + return <>; + } return (
- +
); @@ -298,9 +273,11 @@ export default function QueryPage({ params }: { params: { id: string } }) {
{Object.values(IPARemoteServers).map( (remoteServer: RemoteServer) => { - const status = - statusByRemoteServer[remoteServer.remoteServerName] ?? - Status.UNKNOWN; + const statusEvent: StatusEvent | null = + statusEventByRemoteServer[remoteServer.remoteServerName]; + if (statusEvent === null) { + return <>; + } return (
- +
); diff --git a/sidecar/app/query/ipa.py b/sidecar/app/query/ipa.py index eb980a5..044c410 100644 --- a/sidecar/app/query/ipa.py +++ b/sidecar/app/query/ipa.py @@ -251,7 +251,7 @@ def run(self): for sidecar_url in sidecar_urls: url = urlunparse( sidecar_url._replace( - scheme="https", path=f"/start/ipa-helper/{self.query_id}/status" + scheme="https", path=f"/start/{self.query_id}/status" ), ) while True: diff --git a/sidecar/app/routes/start.py b/sidecar/app/routes/start.py index 6d3b169..e65f093 100644 --- a/sidecar/app/routes/start.py +++ b/sidecar/app/routes/start.py @@ -109,13 +109,13 @@ def start_ipa_helper( return {"message": "Process started successfully", "query_id": query_id} -@router.get("/ipa-helper/{query_id}/status") -def get_ipa_helper_status( +@router.get("/{query_id}/status") +def get_query_status( query_id: str, request: Request, ): query = get_query_from_query_id(request.app.state.QUERY_MANAGER, Query, query_id) - return {"status": query.status.name} + return query.status_event_json @router.get("/{query_id}/log-file") diff --git a/sidecar/tests/app/routes/test_start.py b/sidecar/tests/app/routes/test_start.py index 8ee1aae..e7e0478 100644 --- a/sidecar/tests/app/routes/test_start.py +++ b/sidecar/tests/app/routes/test_start.py @@ -130,16 +130,29 @@ def test_start_ipa_query_as_helper(mock_role): ) -def test_get_ipa_helper_status_not_found(): +def test_get_status_not_found(): query_id = str(uuid4()) - response = client.get(f"/start/ipa-helper/{query_id}/status") + response = client.get(f"/start/{query_id}/status") assert response.status_code == 404 -def test_get_ipa_helper_status(running_query): - response = client.get(f"/start/ipa-helper/{running_query.query_id}/status") +def test_get_status_running(running_query): + response = client.get(f"/start/{running_query.query_id}/status") assert response.status_code == 200 - assert response.json() == {"status": str(Status.STARTING.name)} + status_event_json = response.json() + assert status_event_json["status"] == str(Status.STARTING.name) + assert "start_time" in status_event_json + assert "end_time" not in status_event_json + + +def test_get_status_complete(running_query): + running_query.status = Status.COMPLETE + response = client.get(f"/start/{running_query.query_id}/status") + assert response.status_code == 200 + status_event_json = response.json() + assert status_event_json["status"] == str(Status.COMPLETE.name) + assert "start_time" in status_event_json + assert "end_time" in status_event_json def test_get_ipa_helper_log_file_not_found():