5.2 KB162 lines
Blame
1"use client";
2
3import Link from "next/link";
4import { useCallback, useEffect, useState } from "react";
5import { ApiError, ring, type RingInstanceSummary } from "@/lib/api";
6import { RingLogo } from "@/app/components/ring-logo";
7import { TimeAgo } from "@/app/components/time-ago";
8
9interface RingInstanceListProps {
10 signedOut?: boolean;
11 groveOrigin?: string;
12}
13
14export function RingInstanceList({
15 signedOut = false,
16 groveOrigin = "",
17}: RingInstanceListProps) {
18 const [instances, setInstances] = useState<RingInstanceSummary[]>([]);
19 const [loading, setLoading] = useState(true);
20 const [error, setError] = useState<string | null>(null);
21 const [unauthorized, setUnauthorized] = useState(false);
22
23 const fetchInstances = useCallback(async () => {
24 setLoading(true);
25 try {
26 const data = await ring.listInstances();
27 setInstances(data.instances);
28 setError(null);
29 setUnauthorized(false);
30 } catch (err) {
31 if (err instanceof ApiError && err.status === 401) {
32 setUnauthorized(true);
33 setError(null);
34 } else {
35 setUnauthorized(false);
36 setError("Could not load Ring instances. Try restarting the API server.");
37 }
38 setInstances([]);
39 } finally {
40 setLoading(false);
41 }
42 }, []);
43
44 useEffect(() => {
45 if (signedOut) return;
46 void fetchInstances();
47 }, [fetchInstances, signedOut]);
48
49 if (signedOut || unauthorized) {
50 const loginHref = groveOrigin ? `${groveOrigin}/login` : "/login";
51 const exploreHref = groveOrigin ? `${groveOrigin}/` : "/";
52 return (
53 <div className="max-w-3xl mx-auto px-4 py-8">
54 <div
55 className="p-4 sm:p-8 text-center"
56 style={{
57 backgroundColor: "var(--bg-card)",
58 border: "1px solid var(--border-subtle)",
59 }}
60 >
61 <div className="mx-auto mb-4 w-fit opacity-70">
62 <RingLogo size={44} />
63 </div>
64 <h1 className="text-lg mb-1">Ring</h1>
65 <p className="text-sm mb-4" style={{ color: "var(--text-faint)" }}>
66 Collect and inspect repository logs in one place.
67 </p>
68 <div className="flex items-center justify-center gap-2">
69 <Link
70 href={loginHref}
71 className="px-3 py-1.5 text-sm"
72 style={{
73 backgroundColor: "var(--accent)",
74 color: "var(--accent-text)",
75 }}
76 >
77 Sign in
78 </Link>
79 <Link
80 href={exploreHref}
81 className="px-3 py-1.5 text-sm"
82 style={{
83 border: "1px solid var(--border-subtle)",
84 color: "var(--text-secondary)",
85 }}
86 >
87 Explore Grove
88 </Link>
89 </div>
90 </div>
91 </div>
92 );
93 }
94
95 return (
96 <div className="max-w-3xl mx-auto px-4 py-6">
97 {error && (
98 <div
99 className="px-3 py-2 mb-2 text-xs"
100 style={{
101 color: "var(--status-closed-text)",
102 backgroundColor: "var(--status-closed-bg)",
103 border: "1px solid var(--status-closed-border)",
104 }}
105 >
106 {error}
107 </div>
108 )}
109
110 <div style={{ border: "1px solid var(--border-subtle)" }}>
111 {loading ? (
112 <div className="p-3 flex flex-col gap-2">
113 {Array.from({ length: 5 }).map((_, i) => (
114 <div key={i} className="skeleton" style={{ height: "2.2rem" }} />
115 ))}
116 </div>
117 ) : instances.length === 0 ? (
118 <div
119 className="px-3 py-8 text-sm text-center"
120 style={{
121 color: "var(--text-faint)",
122 backgroundColor: "var(--bg-card)",
123 }}
124 >
125 <div className="flex justify-center mb-2 opacity-70">
126 <RingLogo size={24} />
127 </div>
128 No Ring instances yet. Send logs to{" "}
129 <code>/api/repos/&lt;owner&gt;/&lt;repo&gt;/ring/logs</code>.
130 </div>
131 ) : (
132 <div>
133 {instances.map((instance, i) => (
134 <Link
135 key={`${instance.owner}/${instance.repo}`}
136 href={`/${instance.owner}/${instance.repo}`}
137 className="block hover-row px-3 py-2"
138 style={{ borderTop: i > 0 ? "1px solid var(--divide)" : undefined }}
139 >
140 <div className="flex items-start justify-between gap-3">
141 <div className="min-w-0">
142 <div className="text-sm truncate" style={{ color: "var(--text-primary)" }}>
143 {instance.owner}/{instance.repo}
144 </div>
145 <div className="text-xs mt-0.5 truncate" style={{ color: "var(--text-faint)" }}>
146 {instance.last_message ?? "No message yet"}
147 </div>
148 </div>
149 <div className="text-xs text-right shrink-0" style={{ color: "var(--text-faint)" }}>
150 <div>{instance.total} logs</div>
151 {instance.last_ts && <TimeAgo date={instance.last_ts} />}
152 </div>
153 </div>
154 </Link>
155 ))}
156 </div>
157 )}
158 </div>
159 </div>
160 );
161}
162