@crowandraven/evalkeeper 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { useRouter } from "next/navigation";
4
- import { useEffect } from "react";
4
+ import { useEffect, useTransition } from "react";
5
5
 
6
- /** Refreshes the page every few seconds while something runs. */
6
+ /**
7
+ * Refreshes the page every few seconds while something runs, waiting for
8
+ * each refresh to land before scheduling the next. A refresh on a timer
9
+ * cancels one still in flight, so when the page takes longer to render
10
+ * than the interval (the board against a remote database), no refresh ever
11
+ * lands and the page never updates (#25).
12
+ */
7
13
  export function RefreshWhile({
8
14
  active,
9
15
  ms = 2000,
@@ -12,14 +18,17 @@ export function RefreshWhile({
12
18
  ms?: number;
13
19
  }) {
14
20
  const router = useRouter();
21
+ const [refreshing, startTransition] = useTransition();
15
22
  useEffect(() => {
16
- if (!active) return;
17
- const t = setInterval(() => {
18
- router.refresh();
23
+ if (!active || refreshing) return;
24
+ const t = setTimeout(() => {
25
+ startTransition(() => {
26
+ router.refresh();
27
+ });
19
28
  }, ms);
20
29
  return () => {
21
- clearInterval(t);
30
+ clearTimeout(t);
22
31
  };
23
- }, [active, ms, router]);
32
+ }, [active, refreshing, ms, router]);
24
33
  return null;
25
34
  }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { rollBackSettings } from "@/lib/evalkeeper/actions";
5
+ import { Submit } from "./Submit";
6
+ import { Tip } from "./Tip";
7
+
8
+ /**
9
+ * Rolls a feature back to an earlier version. The button stays off until
10
+ * there's a note saying why: the note is kept with the version, for
11
+ * whoever reads the history next.
12
+ */
13
+ export function RollBack({
14
+ suite,
15
+ versionId,
16
+ number,
17
+ }: {
18
+ suite: string;
19
+ versionId: string;
20
+ number: number;
21
+ }) {
22
+ const [reason, setReason] = useState("");
23
+ const missing = reason.trim() === "";
24
+ const button = (
25
+ <Submit variant="quiet" disabled={missing}>
26
+ Roll back to v{number}
27
+ </Submit>
28
+ );
29
+ return (
30
+ <form
31
+ action={rollBackSettings}
32
+ className="flex flex-wrap items-center gap-2"
33
+ >
34
+ <input type="hidden" name="suite" value={suite} />
35
+ <input type="hidden" name="version" value={versionId} />
36
+ <label htmlFor={`why-${versionId}`} className="sr-only">
37
+ Why roll back to v{number}?
38
+ </label>
39
+ <input
40
+ id={`why-${versionId}`}
41
+ name="reason"
42
+ required
43
+ value={reason}
44
+ onChange={(e) => {
45
+ setReason(e.target.value);
46
+ }}
47
+ placeholder="Why roll back?"
48
+ className="rounded-md border border-line bg-card px-2 py-1 text-xs focus:border-accent focus:outline-none"
49
+ />
50
+ {missing ? (
51
+ <Tip
52
+ text="Say why you're rolling back first. The note is kept with the version, for whoever reads the history next."
53
+ id={`rollback-${versionId}`}
54
+ >
55
+ {button}
56
+ </Tip>
57
+ ) : (
58
+ button
59
+ )}
60
+ </form>
61
+ );
62
+ }
@@ -5,8 +5,7 @@ import {
5
5
  type HistoryEntry,
6
6
  type Settings,
7
7
  } from "@crowandraven/evalkeeper";
8
- import { rollBackSettings } from "@/lib/evalkeeper/actions";
9
- import { Submit } from "./Submit";
8
+ import { RollBack } from "./RollBack";
10
9
  import { InfoTip, Tip } from "./Tip";
11
10
  import { Chip } from "./ui";
12
11
  import { evalsPath } from "@/lib/evalkeeper/paths";
@@ -163,24 +162,11 @@ export function HistoryEntries({
163
162
  </Link>
164
163
  )}
165
164
  {!isLive && (
166
- <form
167
- action={rollBackSettings}
168
- className="flex flex-wrap items-center gap-2"
169
- >
170
- <input type="hidden" name="suite" value={suite} />
171
- <input type="hidden" name="version" value={e.to.id} />
172
- <label htmlFor={`why-${e.id}`} className="sr-only">
173
- Why roll back to v{e.to.number}?
174
- </label>
175
- <input
176
- id={`why-${e.id}`}
177
- name="reason"
178
- required
179
- placeholder="Why roll back?"
180
- className="rounded-md border border-line bg-card px-2 py-1 text-xs focus:border-accent focus:outline-none"
181
- />
182
- <Submit variant="quiet">Roll back to v{e.to.number}</Submit>
183
- </form>
165
+ <RollBack
166
+ suite={suite}
167
+ versionId={e.to.id}
168
+ number={e.to.number}
169
+ />
184
170
  )}
185
171
  </div>
186
172
  </article>
@@ -3,23 +3,32 @@
3
3
  import { LoaderCircle } from "lucide-react";
4
4
  import { useFormStatus } from "react-dom";
5
5
 
6
+ /**
7
+ * A button that isn't the page's main action: the app's accent as an
8
+ * outline, so it still reads as a button in the app's colors (set
9
+ * `--accent` in evalkeeper.css).
10
+ */
11
+ export const QUIET_BUTTON =
12
+ "border border-accent/40 bg-card text-accent hover:bg-accent/10";
13
+
6
14
  /** A submit button that spins while its form's action runs. */
7
15
  export function Submit({
8
16
  children,
9
17
  variant = "primary",
18
+ disabled = false,
10
19
  }: {
11
20
  children: React.ReactNode;
12
21
  variant?: "primary" | "quiet";
22
+ /** Off until the form is ready; wrap it in a `<Tip>` saying why. */
23
+ disabled?: boolean;
13
24
  }) {
14
25
  const { pending } = useFormStatus();
15
26
  return (
16
27
  <button
17
28
  type="submit"
18
- disabled={pending}
29
+ disabled={pending || disabled}
19
30
  className={`inline-flex items-center gap-2 whitespace-nowrap rounded-md px-3 py-1.5 text-sm disabled:opacity-60 ${
20
- variant === "primary"
21
- ? "bg-accent text-white"
22
- : "border border-line bg-card text-ink hover:bg-tint"
31
+ variant === "primary" ? "bg-accent text-white" : QUIET_BUTTON
23
32
  }`}
24
33
  >
25
34
  {pending && <Spinner />}