Skip to content

Commit

Permalink
add page for running queries (#73)
Browse files Browse the repository at this point in the history
* add page for running queries

* refactor to use a StatusEvent interface; update StatusEvent with polling instead of websocket

* small cleanup

* remove log polluter in websocket

* fix elements needing key warning

* set runTime before the first interval

* use Promise.all for fetching statusEvents concurrently across remoteServers; update comment in StasusEvent updater
  • Loading branch information
eriktaubeneck authored Jul 19, 2024
1 parent 7490840 commit 0bb24ad
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 114 deletions.
2 changes: 1 addition & 1 deletion server/app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Bars3Icon, BellIcon, XMarkIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import beerTap from "@/public/beer-tap.png";

const navigation = [{ name: "Dashboard", href: "/query", current: true }];
const navigation = [{ name: "Queries", href: "/query", current: true }];
const userNavigation = [
{ name: "Your Profile", href: "#" },
{ name: "Settings", href: "#" },
Expand Down
2 changes: 1 addition & 1 deletion server/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default async function Example() {
<div className="mt-10 flex items-center justify-center gap-x-6">
<div className="rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-400">
{isLoggedIn ? (
<Link href="/query">Dashboard</Link>
<Link href="/query">Queries</Link>
) : (
<Link href="/login">Log in</Link>
)}
Expand Down
188 changes: 188 additions & 0 deletions server/app/query/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,199 @@
"use client";

import { useEffect, useState } from "react";
import Link from "next/link";

import { StatusPill, RunTimePill } from "@/app/query/view/[id]/components";
import {
StatusEvent,
RemoteServer,
RemoteServerNames,
IPARemoteServers, //hack until the queryId is stored in a DB
StatusEventByRemoteServer,
initialStatusEventByRemoteServer,
} from "@/app/query/servers";
import { getQueryByUUID, Query } from "@/data/query";

type QueryData = {
statusEvent: StatusEventByRemoteServer;
query: Query;
};
type DataByQuery = {
[queryID: string]: QueryData;
};

export default function Page() {
const [queryIDs, setQueryIDs] = useState<string[]>([]);
const [dataByQuery, setDataByQuery] = useState<DataByQuery>({});

const updateData = (
query: Query,
remoteServer: RemoteServer,
statusEvent: StatusEvent,
) => {
setDataByQuery((prev) => {
let _prev = prev;
if (!Object.hasOwn(prev, query.uuid)) {
// if queryID not in dataByQuery yet,
// add initial status before updating value.
// otherwise prev[query.uuid][statusEvent][remteServer.ServerName]
// doesn't exist, and cannot be updated. we need to fill in the
// nested structure, which `initialStatusEventByRemoteServer` does.
_prev = {
..._prev,
[query.uuid]: {
statusEvent: initialStatusEventByRemoteServer,
query: query,
},
};
}

return {
..._prev,
[query.uuid]: {
..._prev[query.uuid],
statusEvent: {
..._prev[query.uuid].statusEvent,
[remoteServer.remoteServerName]: statusEvent,
},
},
};
});
};

useEffect(() => {
// poll runningQueries every second
(async () => {
const interval = setInterval(async () => {
const _queryIDs: string[] =
await IPARemoteServers[RemoteServerNames.Helper1].runningQueries();

setQueryIDs(_queryIDs);
}, 1000); // 1000 milliseconds = 1 second
return () => clearInterval(interval);
})();
}, []);

useEffect(() => {
(async () => {
setDataByQuery((prev) => {
return Object.fromEntries(
Object.keys(prev)
.filter((queryID) => queryIDs.includes(queryID))
.map((queryID) => [queryID, prev[queryID]]),
);
});

const promises = queryIDs.map(async (queryID) => {
const query: Query = await getQueryByUUID(queryID);
const remoteServerPromises = Object.values(IPARemoteServers).map(
async (remoteServer) => {
const statusEvent: StatusEvent =
await remoteServer.queryStatus(queryID);
updateData(query, remoteServer, statusEvent);
},
);
await Promise.all(remoteServerPromises);
});
await Promise.all(promises);
})();
}, [queryIDs]);

return (
<>
<div className="md:flex md:items-center md:justify-between">
<div className="min-w-0 flex-1">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
Current Queries
</h2>

{Object.keys(dataByQuery).length == 0 && (
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
None currently running.
</h3>
)}

{Object.entries(dataByQuery).map(([queryID, queryData]) => {
const statusEventByRemoteServer = queryData.statusEvent;
const query = queryData.query;

return (
<div
className="mx-auto mt-10 w-full max-w-7xl overflow-hidden rounded-lg bg-slate-50 text-left shadow hover:bg-slate-200 dark:bg-slate-950 dark:hover:bg-slate-800"
key={queryID}
>
<Link href={`/query/view/${query.displayId}`}>
<div className="size-full border-b border-gray-300 px-4 py-2 font-bold text-slate-900 sm:p-2 dark:border-gray-700 dark:text-slate-100">
<h3 className="py-2 pl-2 text-base font-semibold leading-6 text-gray-900 dark:text-gray-100">
Query: {query.displayId}
</h3>
<div className="my-2 flex justify-end text-center">
<dl className="mb-2 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
{Object.values(IPARemoteServers).map(
(remoteServer: RemoteServer) => {
const statusEvent: StatusEvent | null =
statusEventByRemoteServer[
remoteServer.remoteServerName
];
if (statusEvent === null) {
return (
<div key={remoteServer.remoteServerName}></div>
);
}

return (
<div
key={remoteServer.remoteServerName}
className="w-48 overflow-hidden rounded-lg bg-white px-4 py-2 shadow dark:bg-slate-900"
>
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-300">
{remoteServer.toString()} Run Time
</dt>
<dd>
<RunTimePill statusEvent={statusEvent} />
</dd>
</div>
);
},
)}
</dl>
</div>
<div className="my-2 flex justify-end text-center">
<dl className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
{Object.values(IPARemoteServers).map(
(remoteServer: RemoteServer) => {
const statusEvent: StatusEvent | null =
statusEventByRemoteServer[
remoteServer.remoteServerName
];
if (statusEvent === null) {
return (
<div key={remoteServer.remoteServerName}></div>
);
}

return (
<div
key={remoteServer.remoteServerName}
className="w-48 overflow-hidden rounded-lg bg-white px-4 py-2 shadow dark:bg-slate-900"
>
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-300">
{remoteServer.remoteServerNameStr} Status
</dt>
<dd>
<StatusPill status={statusEvent.status} />
</dd>
</div>
);
},
)}
</dl>
</div>
</div>
</Link>
</div>
);
})}
</div>
</div>
</>
Expand Down
106 changes: 59 additions & 47 deletions server/app/query/servers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ function getStatusFromString(statusString: string): Status {
}
}

export interface StatsDataPoint {
timestamp: string;
memoryRSSUsage: number;
cpuUsage: number;
export interface StatusEvent {
status: Status;
startTime: number;
endTime: number | null;
}

function buildStatusEventFromJSON(statusJSON: any): StatusEvent {
return {
status: getStatusFromString(statusJSON.status),
startTime: statusJSON.start_time,
endTime: statusJSON.end_time ?? null,
};
}

export type StatusByRemoteServer = {
Expand All @@ -50,15 +58,6 @@ export const initialStatusByRemoteServer: StatusByRemoteServer =
Object.values(RemoteServerNames).map((serverName) => [[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;
};
Expand All @@ -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]),
Expand All @@ -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;
Expand All @@ -96,6 +119,26 @@ 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<StatusEvent> {
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);
}

async runningQueries(): Promise<string[]> {
const queries_response = await fetch(this.runningQueriesURL());
const queriesJSON = await queries_response.json();
return queriesJSON.running_queries;
}

logURL(id: string): URL {
return new URL(`/start/${id}/log-file`, this.baseURL);
}
Expand Down Expand Up @@ -187,44 +230,13 @@ export class RemoteServer {

openStatusSocket(
id: string,
setStatus: React.Dispatch<React.SetStateAction<StatusByRemoteServer>>,
setStartTime: React.Dispatch<React.SetStateAction<StartTimeByRemoteServer>>,
setEndTime: React.Dispatch<React.SetStateAction<EndTimeByRemoteServer>>,
setStatusEvent: (statusEvent: StatusEvent) => void,
): WebSocket {
const ws = this.statusSocket(id);

const updateStatus = (status: Status) => {
setStatus((prevStatus) => ({
...prevStatus,
[this.remoteServerName]: status,
}));
};

const updateStartTime = (runTime: number) => {
setStartTime((prevStartTime) => {
return {
...prevStartTime,
[this.remoteServerName]: runTime,
};
});
};

const updateEndTime = (runTime: number) => {
setEndTime((prevEndTime) => {
return {
...prevEndTime,
[this.remoteServerName]: runTime,
};
});
};

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) => {
Expand Down
Loading

0 comments on commit 0bb24ad

Please sign in to comment.