Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ethan2 #2

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"tailwindcss",
"tanstack",
"tlenaii",
"tupled",
"turbopack",
"zeropoint"
]
Expand Down
5 changes: 5 additions & 0 deletions app/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { RpcRouter } from "@/app/api/route";
import * as Resolver from "@effect/rpc-http/HttpRpcResolver";

// FIXME: Find way to not hardcode the URL
export const rpcClient = Resolver.makeClient<RpcRouter>("http://localhost:5001/api");
34 changes: 34 additions & 0 deletions app/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RpcRouter } from "@effect/rpc";
import { Chunk, Layer, ManagedRuntime, Stream } from "effect";
import { NextRequest } from "next/server";

import { Database } from "@/services/Database";
import { databaseRouter, verboseLogsRouter } from "@/services/rpcs";
import { VerboseLogs } from "@/services/VerboseLogs";

const router = RpcRouter.make(databaseRouter, verboseLogsRouter);
export type RpcRouter = typeof router;

const handler = RpcRouter.toHandler(router);
const runtime = ManagedRuntime.make(Layer.mergeAll(Database.Default, VerboseLogs.Default));

export async function POST(request: NextRequest) {
const data = await request.json();
const stream = handler(data);

const responseStream = await Stream.toReadableStreamEffect(
stream.pipe(
Stream.chunks,
Stream.map((_) => `${JSON.stringify(Chunk.toReadonlyArray(_))}\n`),
Stream.encodeText
)
).pipe(runtime.runPromise);

return new Response(responseStream, {
status: 200,
headers: {
"Transfer-Encoding": "chunked",
"Content-Type": "application/ndjson; charset=utf-8",
},
});
}
114 changes: 113 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,115 @@
"use client";

import { useState /*useEffect*/ } from "react";
import Link from "next/link";
import { ThemeToggle } from "@/components/ThemeToggle";
import { AlignJustifyIcon, Telescope } from "lucide-react";
import { useTheme } from "next-themes";

const Header: React.FC = () => {
// State to track dropdown visibility
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

// Toggle dropdown visibility
const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen);

const { theme } = useTheme();
//const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
// const headerColor =
// theme == "dark" ? "#151515" : theme == "light" ? "#f2f2f2" : systemTheme == "dark" ? "#151515" : "#f2f2f2";

return (
<div
style={{
padding: "16px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: theme == "light" ? "#f2f2f2" : "#151515",
}}
>
<h1>
<ThemeToggle />
</h1>

{/* Dropdown button */}
<div style={{ position: "relative" }}>
<button onClick={toggleDropdown} style={{ padding: "8px 16px", fontSize: "16px", cursor: "pointer" }}>
<AlignJustifyIcon />
</button>

{/* Dropdown menu */}
{isDropdownOpen && (
<div
style={{
position: "absolute",
top: "100%",
right: 0,
marginTop: "8px",
backgroundColor: "#fff",
boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
borderRadius: "4px",
zIndex: 1000,
minWidth: "150px",
}}
>
<ul style={{ listStyle: "none", margin: 0, padding: "8px 0" }}>
<li>
<Link
href="/pipeline-health"
style={{
display: "block",
padding: "8px 16px",
textDecoration: "none",
color: "#333",
}}
>
Pipeline Health
</Link>
</li>
{/* <li>
<Link
href="/logs"
style={{
display: "block",
padding: "8px 16px",
textDecoration: "none",
color: "#333",
}}
>
Logs
</Link>
</li> */}
{/* <li>
<Link
href="/images"
style={{
display: "block",
padding: "8px 16px",
textDecoration: "none",
color: "#333",
}}
>
Images
</Link>
</li> */}
</ul>
</div>
)}
</div>
</div>
);
};

export default function Home() {
return <p>Hey there :)</p>;
return (
<>
<Header />
<main style={{ padding: "16px", textAlign: "center", alignContent: "center" }}>
<h2>Welcome to the Home Page</h2>
<h2>Links are in the top right</h2>
<Telescope size="large" />
</main>
</>
);
}
2 changes: 1 addition & 1 deletion app/pipeline-health/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PipelineHealth } from "@/components/PipelineHealth";
import { ThemeToggle } from "@/components/ThemeToggle";

export default function page() {
export default function Page() {
return (
<>
<div className="fixed bottom-5 right-5 z-50">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import dynamic from "next/dynamic";

import { LogViewer } from "@/components/LogViewer";
import { SchemaName } from "@/services/Domain";

const page = ({
export default async function Page({
params,
}: {
params: {
params: Promise<{
machine: "tlenaii" | "popcorn";
schemaName: typeof SchemaName.Encoded;
};
}) => {
}>;
}) {
const { machine, schemaName } = await params;

return (
<>
<LogViewer machine={params.machine} schemaName={params.schemaName} />
<LogViewer machine={machine} schemaName={schemaName} />
</>
);
};

// FIXME: bad bad bad bad bad
export default dynamic(() => Promise.resolve(page), { ssr: false });
}
Empty file added components/Error.tsx
Empty file.
65 changes: 33 additions & 32 deletions components/LogViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
"use client";

import { Result, Rx, useRx } from "@effect-rx/rx-react";
import { Rx, useRxSet, useRxSuspenseSuccess } from "@effect-rx/rx-react";
import { FetchHttpClient, HttpClient, HttpClientError } from "@effect/platform";
import { Effect, Scope } from "effect";
import { Scope, Stream } from "effect";

import { rpcClient } from "@/rpc/client";
import { rpcClient } from "@/app/api/client";
import { SchemaName, VerboseLogRequest } from "@/services/Domain";
import { useMemo } from "react";

const runtime = Rx.runtime(FetchHttpClient.layer);

const verboseLogRx: Rx.RxResultFn<
{ schemaName: typeof SchemaName.from.Type; machine: "tlenaii" | "popcorn" },
string,
never
> = runtime.fn(
const machineRx = Rx.make<"tlenaii" | "popcorn">("popcorn" as const);
const schemaNameRx = Rx.make<typeof SchemaName.from.Type>(
"" as `science_turbo_production_pipeline_${number}_${number}_${number}_${number}_${number}_${number}`
);

const verboseLogRx: Rx.Writable<Rx.PullResult<Uint8Array, never>, void> = runtime.pull(
(
{ machine, schemaName }: { schemaName: typeof SchemaName.from.Type; machine: "tlenaii" | "popcorn" },
_context: Rx.Context
): Effect.Effect<string, never, HttpClient.HttpClient<HttpClientError.HttpClientError, Scope.Scope>> =>
Effect.Do.pipe(
Effect.bind("request", () => Effect.succeed(new VerboseLogRequest({ schemaName, machine }))),
Effect.bind("client", () => rpcClient),
Effect.flatMap(({ client, request }) => client(request))
)
context: Rx.Context
): Stream.Stream<Uint8Array, never, HttpClient.HttpClient<HttpClientError.HttpClientError, Scope.Scope>> =>
Stream.Do.pipe(
Stream.let("machine", () => context.get(machineRx)),
Stream.let("schemaName", () => context.get(schemaNameRx)),
Stream.let("request", ({ machine, schemaName }) => new VerboseLogRequest({ schemaName, machine })),
Stream.bind("client", () => rpcClient),
Stream.flatMap(({ client, request }) => client(request))
),
{
disableAccumulation: false,
}
);

export function LogViewer({
Expand All @@ -33,20 +38,16 @@ export function LogViewer({
machine: "tlenaii" | "popcorn";
schemaName: typeof SchemaName.Encoded;
}) {
const [verboseLogs, fetchVerboseLogs] = useRx(verboseLogRx);
useMemo(() => fetchVerboseLogs({ machine, schemaName }), [fetchVerboseLogs, machine, schemaName]);

if (Result.isInitial(verboseLogs)) {
return <p>Loading...</p>;
}

if (Result.isFailure(verboseLogs) || Result.isInterrupted(verboseLogs)) {
return <p>Failed to load logs</p>;
}

if (!Result.isSuccess(verboseLogs)) {
return <p>BAD</p>;
}

return <p>{verboseLogs.value}</p>;
// Sets
const setMachineName = useRxSet(machineRx);
const setSchemaName = useRxSet(schemaNameRx);
useMemo(() => setMachineName(machine), [machine, setMachineName]);
useMemo(() => setSchemaName(schemaName), [schemaName, setSchemaName]);

// Suspenses
const verboseLogs = useRxSuspenseSuccess(verboseLogRx).value;

// Content
const all = verboseLogs.items.map((item) => new TextDecoder().decode(item)).join("\n");
return <p>{all}</p>;
}
29 changes: 21 additions & 8 deletions components/PipelineHealth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Result, useRx, useRxSet, useRxSuspenseSuccess, useRxValue } from "@effect-rx/rx-react";
import { useRx, useRxSet, useRxSuspenseSuccess } from "@effect-rx/rx-react";
import { DateTime } from "effect";
import { Suspense, useMemo } from "react";

Expand All @@ -11,20 +11,26 @@ import { PipelineStepHistogram } from "@/components/PipelineHealth/PipelineStepH
import { AverageProcessingTimeLineChart } from "@/components/PipelineHealth/RunTimeHist";
import { RunsTable } from "@/components/PipelineHealth/Table";
import { fromRx, rowsRx, timeSeriesGroupedRx, totalsRx, untilRx } from "@/components/PipelineHealth/rx";
import { LocaleSelector } from "./PipelineHealth/LocaleSelector";
import { Steps2querySelector } from "./PipelineHealth/StepsFilter";

export function PipelineHealth() {
// Sets
const [_rows, pullRows] = useRx(rowsRx);
useMemo(pullRows, []);
useMemo(pullRows, [pullRows]);

const pullTimeSeriesData = useRxSet(timeSeriesGroupedRx);
useMemo(pullTimeSeriesData, []);
useMemo(pullTimeSeriesData, [pullTimeSeriesData]);

// Gets
const from = useRxValue(fromRx).pipe(Result.getOrThrow);
const until = useRxValue(untilRx).pipe(Result.getOrThrow);
const updateFrom = useRxSet(fromRx);
useMemo(() => updateFrom(new Date("2024-11-19")), [updateFrom]);

const updateUntil = useRxSet(untilRx);
useMemo(() => updateUntil(new Date()), [updateUntil]);

// Suspenses
// Gets
const from = useRxSuspenseSuccess(fromRx).value;
const until = useRxSuspenseSuccess(untilRx).value;
const totals = useRxSuspenseSuccess(totalsRx).value;

return (
Expand All @@ -39,9 +45,16 @@ export function PipelineHealth() {
<div className="mx-1">
<EmptyBucketsToggle />
</div>
<div className="mx-1">
<LocaleSelector />
</div>
<div className="mx-1">
<Steps2querySelector />
</div>
</div>
<span className="flex justify-center my-4 text-sm text-muted-foreground">
Selected {totals.totalRuns} runs between {DateTime.formatIso(from)} and {DateTime.formatIso(until)}
Selected {totals.totalRuns} images between {DateTime.formatIsoZoned(from)} and{" "}
{DateTime.formatIsoZoned(until)}
</span>

<Suspense fallback={<p>Loading...</p>}>
Expand Down
6 changes: 4 additions & 2 deletions components/PipelineHealth/DatePickerRange.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use client";

import { Result, useRx } from "@effect-rx/rx-react";
import { Result, useRx, useRxValue } from "@effect-rx/rx-react";
import { format } from "date-fns";
import { DateTime, Predicate } from "effect";
import { Calendar as CalendarIcon } from "lucide-react";
import { HTMLAttributes, useState } from "react";
import { DateRange } from "react-day-picker";

import { fromRx, untilRx } from "@/components/PipelineHealth/rx";
import { fromRx, localeRx, untilRx } from "@/components/PipelineHealth/rx";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
Expand All @@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
export function DatePickerWithRange({ className }: HTMLAttributes<HTMLDivElement>) {
const [from, updateFrom] = useRx(fromRx);
const [until, updateUntil] = useRx(untilRx);
const _locale = useRxValue(localeRx).pipe(Result.getOrThrow);

const [date, setDate] = useState<DateRange | undefined>({
from: DateTime.toDate(Result.getOrThrow(from)),
Expand Down Expand Up @@ -60,6 +61,7 @@ export function DatePickerWithRange({ className }: HTMLAttributes<HTMLDivElement
if (Predicate.isNotUndefined(dates?.to)) updateUntil(dates.to);
}}
numberOfMonths={2}
// timeZone="America/Chicago"
/>
</PopoverContent>
</Popover>
Expand Down
5 changes: 2 additions & 3 deletions components/PipelineHealth/LocaleSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ export function LocaleSelector() {
<DropdownMenuContent align="end">
<DropdownMenuRadioGroup
value={DateTime.zoneToString(Result.getOrThrow(locale))}
onValueChange={(str) => setLocale(str as "Utc" | "System" | "America/Chicago")}
onValueChange={(str) => setLocale(str as "UTC" | "America/Chicago")}
>
<DropdownMenuRadioItem value={"Utc"}>Utc</DropdownMenuRadioItem>
<DropdownMenuRadioItem value={"System"}>System</DropdownMenuRadioItem>
<DropdownMenuRadioItem value={"UTC"}>Utc</DropdownMenuRadioItem>
<DropdownMenuRadioItem value={"America/Chicago"}>America/Chicago</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
Expand Down
Loading