diff --git a/.env.example b/.env.example index a69f4c5..0f05eff 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ DATABASE_URL="file:./evolver.db" -SESSION_SECRET="evolver4ever" DEFAULT_DEVICE_PORT="8080" +EXCLUDED_PROPERTIES="name,vial" diff --git a/app/__tests__/unit/root.test.tsx b/app/__tests__/unit/root.test.tsx index f80bca5..fe537df 100644 --- a/app/__tests__/unit/root.test.tsx +++ b/app/__tests__/unit/root.test.tsx @@ -1,6 +1,6 @@ import { expect, describe, it } from "vitest"; import { loader, action } from "~/root"; -import { prefs } from "~/prefs-cookie"; +import { userPrefs } from "~/cookies.server"; describe("root loader", () => { it("should return an object with the default theme and publically visible (to the client) env variables", async () => { @@ -10,11 +10,11 @@ describe("root loader", () => { context: {}, }); const data = await response.json(); - expect(data).toEqual({ theme: "dark", ENV: { VITEST: true } }); + expect(data).toEqual({ theme: "dark", ENV: { NODE_ENV: "test" } }); }); it("should return the theme from the request cookie", async () => { - const prefsCookie = await prefs.serialize({ theme: "light" }); + const prefsCookie = await userPrefs.serialize({ theme: "light" }); const request = new Request("http://127.0.0.1:8000", { headers: { Cookie: prefsCookie }, }); @@ -44,6 +44,7 @@ describe("root action", () => { it("should return valid json if theme form data is included in the request", async () => { const formData = new FormData(); formData.set("theme", "dark"); + const request = new Request("http://127.0.0.1:8000", { method: "POST", body: formData, @@ -51,7 +52,9 @@ describe("root action", () => { const response = await action({ request, params: {}, context: {} }); const cookie = response?.headers.get("Set-Cookie"); - const setCookieValue = await prefs.parse(cookie ?? null); + + const setCookieValue = await userPrefs.parse(cookie ?? null); + expect(setCookieValue).toEqual({ theme: "dark" }); }); }); diff --git a/app/components/EditJson.client.tsx b/app/components/EditJson.client.tsx index d68993e..a0069bc 100644 --- a/app/components/EditJson.client.tsx +++ b/app/components/EditJson.client.tsx @@ -1,6 +1,5 @@ import { useRouteLoaderData } from "@remix-run/react"; import clsx from "clsx"; -import Ajv from "ajv"; import { FilterFunction, JsonData, @@ -11,15 +10,12 @@ import { import { loader as rootLoader } from "~/root"; import { checkType } from "~/utils/checkType"; import { EvolverConfigWithoutDefaults } from "client"; -import { SomeJSONSchema } from "ajv/dist/types/json-schema"; export function EditJson({ data, mode, - schema, setData, }: { - schema: SomeJSONSchema; data: EvolverConfigWithoutDefaults; mode: "edit" | "view"; setData: (arg0: EvolverConfigWithoutDefaults) => void; @@ -27,9 +23,8 @@ export function EditJson({ const { theme } = useRouteLoaderData("root") ?? { theme: "dark", }; + const editorTheme = theme === "dark" ? "githubDark" : "githubLight"; - const ajv = new Ajv(); - const validate = ajv.compile(schema); const customizeText = ({ key, value }: NodeData) => { switch (key) { @@ -146,7 +141,7 @@ export function EditJson({ } if (mode === "edit") { if (level === 0) { - return true; + return false; } switch (key) { case "vials": @@ -211,20 +206,6 @@ export function EditJson({ restrictEdit={restrictEdit} restrictAdd={restrictAdd} restrictDelete={restrictDelete} - onUpdate={({ newData }) => { - const valid = validate(newData); - if (!valid) { - const errorMessage = validate.errors - ?.map( - (error) => - `${error.instancePath}${error.instancePath ? ": " : ""}${error.message}`, - ) - .join("\n"); - // This string is returned to and displayed in json-edit-react UI - return errorMessage; - } - // data is valid, do nothing - }} setData={(data: JsonData) => setData(data as EvolverConfigWithoutDefaults) } diff --git a/app/components/ErrorNotifs.tsx b/app/components/ErrorNotifs.tsx deleted file mode 100644 index aa87639..0000000 --- a/app/components/ErrorNotifs.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; -import { useEffect, useState } from "react"; - -export function ErrorNotifs(messages: string[]) { - const [errorMessages, setErrorMessages] = useState([]); - - useEffect(() => { - setErrorMessages(messages); - }, [messages]); - - const dismissAlert = (msg: string) => { - const filteredErrorMessages = errorMessages.filter((errorMessage) => { - return errorMessage !== msg; - }); - - setErrorMessages(filteredErrorMessages); - }; - - const errorAlerts = errorMessages.map((errorMessage) => { - return ( -
- -
{errorMessage}
-
- -
-
- ); - }); - return ( -
- {errorAlerts} -
- ); -} diff --git a/app/components/HardwareTable.tsx b/app/components/HardwareTable.tsx new file mode 100644 index 0000000..f4e8e20 --- /dev/null +++ b/app/components/HardwareTable.tsx @@ -0,0 +1,89 @@ +import { Link } from "@remix-run/react"; +import { EvolverConfigWithoutDefaults } from "client"; +import clsx from "clsx"; + +export function HardwareTable({ + evolverConfig, + id, + hardwareName, + queryParams, +}: { + evolverConfig: EvolverConfigWithoutDefaults; + id: string; + hardwareName: string; + queryParams: URLSearchParams; +}) { + let currentVials: string[] = []; + let allVials = false; + if (queryParams.has("vials")) { + currentVials = queryParams.get("vials")?.split(","); + } else { + allVials = true; + } + const evolverHardware = evolverConfig.hardware; + + const TableRows = Object.keys(evolverHardware).map((key) => { + const { + config: { vials }, + } = evolverHardware[key]; + + const vialsWithLinks = vials.map((vial) => { + const linkTo = `/devices/${id}/hardware/${key}/history?vials=${vial}`; + const activeVial = + currentVials.includes(vial.toString()) && hardwareName === key; + const vialButtons = ( + + {vial} + + ); + + return vialButtons; + }); + const allButton = ( + + {" "} + all + + ); + + return ( + + {key} + + {vialsWithLinks} + {allButton} + + + ); + }); + + return ( + + + + + + + + {TableRows} +
namevials
+ ); +} diff --git a/app/components/LineChart.tsx b/app/components/LineChart.tsx new file mode 100644 index 0000000..bbbe3fa --- /dev/null +++ b/app/components/LineChart.tsx @@ -0,0 +1,56 @@ +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import { vialColors } from "~/utils/chart/colors"; + +const processData = (data, vials, property) => { + return data.map((entry) => { + const processedEntry: { [key: string]: string } = { + timestamp: new Date(entry.timestamp * 1000).toLocaleTimeString(), // Convert timestamp to readable time + }; + + vials.forEach((vial) => { + processedEntry[`vial_${vial}`] = entry.data[vial]?.[property]; + }); + + return processedEntry; + }); +}; + +export const HardwareLineChart = ({ vials, rawData, property = "raw" }) => { + const formattedData = processData(rawData, vials, property); + + return ( +
+
+
property:
+
{property}
+
+ + + + + + + + {vials.map((vial: number, index: number) => ( + + ))} + + +
+ ); +}; diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index 80aa6f6..3cc5a49 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -4,11 +4,11 @@ import clsx from "clsx"; export default function Navbar({ pathname = "" as string }): JSX.Element { return ( -
-
+
+
- + Evolver
@@ -17,8 +17,11 @@ export default function Navbar({ pathname = "" as string }): JSX.Element { Devices @@ -27,7 +30,7 @@ export default function Navbar({ pathname = "" as string }): JSX.Element {
-
+
); } diff --git a/app/components/ThemeController.tsx b/app/components/ThemeController.tsx index cd3a33d..e31687d 100644 --- a/app/components/ThemeController.tsx +++ b/app/components/ThemeController.tsx @@ -1,22 +1,28 @@ import { MoonIcon, SunIcon } from "@heroicons/react/24/solid"; -import { useFetcher, useLoaderData } from "@remix-run/react"; -import { loader } from "../root"; +import { useFetcher, useLoaderData, useLocation } from "@remix-run/react"; +import { loader } from "~/root"; export default function ThemeController() { + const { pathname, search } = useLocation(); + + // depends on root.tsx action returning a redirect with Set-Cookie header const fetcher = useFetcher(); + + // depends on root.tsx loader returning a theme const { theme } = useLoaderData(); return (