Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DATABASE_URL="file:./evolver.db"
SESSION_SECRET="evolver4ever"
DEFAULT_DEVICE_PORT="8080"
EXCLUDED_PROPERTIES="name,vial"
11 changes: 7 additions & 4 deletions app/__tests__/unit/root.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More descriptive naming of the purpose of this cookie (reight now it persists their selected theme between page refresh / visits), .server tells remix framework this code will only run on the server.


describe("root loader", () => {
it("should return an object with the default theme and publically visible (to the client) env variables", async () => {
Expand All @@ -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 },
});
Expand Down Expand Up @@ -44,14 +44,17 @@ 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,
});

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" });
});
});
23 changes: 2 additions & 21 deletions app/components/EditJson.client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useRouteLoaderData } from "@remix-run/react";
import clsx from "clsx";
import Ajv from "ajv";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ripped out AJV. Not going to do much / any client side validation as long as the Evolver-NG continues to respond with sane validation error messaging.

import {
FilterFunction,
JsonData,
Expand All @@ -11,25 +10,21 @@ 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;
}) {
const { theme } = useRouteLoaderData<typeof rootLoader>("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) {
Expand Down Expand Up @@ -146,7 +141,7 @@ export function EditJson({
}
if (mode === "edit") {
if (level === 0) {
return true;
return false;
}
switch (key) {
case "vials":
Expand Down Expand Up @@ -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)
}
Expand Down
37 changes: 0 additions & 37 deletions app/components/ErrorNotifs.tsx

This file was deleted.

89 changes: 89 additions & 0 deletions app/components/HardwareTable.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<Link
key={vial}
className={clsx(
"btn",
"btn-xs",
"btn-outline",
activeVial && "btn-active",
)}
to={linkTo}
>
{vial}
</Link>
);

return vialButtons;
});
const allButton = (
<Link
className={clsx(
"btn",
"btn-xs",
"btn-outline",
allVials && key === hardwareName && "btn-active",
)}
key={"all"}
to={`/devices/${id}/hardware/${key}/history`}
>
{" "}
all
</Link>
);

return (
<tr key={key} className={clsx(hardwareName === key && "bg-base-300")}>
<td>{key}</td>
<td className="flex gap-2">
{vialsWithLinks}
{allButton}
</td>
</tr>
);
});

return (
<table className="table">
<thead>
<tr>
<th>name</th>
<th>vials</th>
</tr>
</thead>
<tbody>{TableRows}</tbody>
</table>
);
}
56 changes: 56 additions & 0 deletions app/components/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div className="divider"></div>
<div>property: </div>
<div className="stat-value font-mono">{property}</div>
<div className="divider"></div>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={formattedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Legend />
{vials.map((vial: number, index: number) => (
<Line
key={vial}
type="monotone"
dataKey={`vial_${vial}`}
stroke={vialColors[index % vialColors.length]}
name={`Vial ${vial}`}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
};
15 changes: 9 additions & 6 deletions app/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import clsx from "clsx";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just style tweaks in this file.

export default function Navbar({ pathname = "" as string }): JSX.Element {
return (
<div className="mb-8">
<div className="px-4 mx-auto max-w-6xl flex flex-wrap justify-between gap-10 min-h-16 items-center">
<div className="">
<div className="mx-auto max-w-6xl flex flex-wrap justify-between gap-10 min-h-16 items-center">
<div className="flex-1">
<div className="flex items-center space-x-1">
<Link to="/" className="text-3xl">
<Link to="/" className="text-3xl text-primary">
Evolver
</Link>
</div>
Expand All @@ -17,8 +17,11 @@ export default function Navbar({ pathname = "" as string }): JSX.Element {
<Link
tabIndex={0}
role="button"
className={clsx("link", pathname !== "/devices" && "link-hover")}
to="/devices"
className={clsx(
"link",
pathname !== "/devices/list" && "link-hover",
)}
to="/devices/list"
>
Devices
</Link>
Expand All @@ -27,7 +30,7 @@ export default function Navbar({ pathname = "" as string }): JSX.Element {
<ThemeController />
</div>
</div>
<div className="divider divider-neutral h-0 m-0"></div>
<div className="divider h-0 m-0"></div>
</div>
);
}
16 changes: 11 additions & 5 deletions app/components/ThemeController.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader>();

return (
<label className="swap swap-rotate pt-2">
<div className="hidden">switch theme</div>
{/* this hidden checkbox controls the state */}
<input
type="checkbox"
role="switch"
name="theme"
value={theme === "dark" ? "light" : "dark"}
onChange={() => {
const formData = new FormData();
formData.set("theme", theme === "dark" ? "light" : "dark");
const newTheme = theme === "dark" ? "light" : "dark";
formData.set("theme", newTheme);
formData.set("redirectTo", pathname + search);
fetcher.submit(formData, { method: "POST" });
}}
></input>
Expand Down
Loading