web/app/ring/ring-instance-list.tsxblame
View source
3cbdca61"use client";
3cbdca62
3cbdca63import Link from "next/link";
3cbdca64import { useCallback, useEffect, useState } from "react";
3cbdca65import { ApiError, ring, type RingInstanceSummary } from "@/lib/api";
3cbdca66import { RingLogo } from "@/app/components/ring-logo";
3cbdca67import { TimeAgo } from "@/app/components/time-ago";
3cbdca68
3cbdca69interface RingInstanceListProps {
3cbdca610 signedOut?: boolean;
3cbdca611 groveOrigin?: string;
3cbdca612}
3cbdca613
3cbdca614export function RingInstanceList({
3cbdca615 signedOut = false,
3cbdca616 groveOrigin = "",
3cbdca617}: RingInstanceListProps) {
3cbdca618 const [instances, setInstances] = useState<RingInstanceSummary[]>([]);
3cbdca619 const [loading, setLoading] = useState(true);
3cbdca620 const [error, setError] = useState<string | null>(null);
3cbdca621 const [unauthorized, setUnauthorized] = useState(false);
3cbdca622
3cbdca623 const fetchInstances = useCallback(async () => {
3cbdca624 setLoading(true);
3cbdca625 try {
3cbdca626 const data = await ring.listInstances();
3cbdca627 setInstances(data.instances);
3cbdca628 setError(null);
3cbdca629 setUnauthorized(false);
3cbdca630 } catch (err) {
3cbdca631 if (err instanceof ApiError && err.status === 401) {
3cbdca632 setUnauthorized(true);
3cbdca633 setError(null);
3cbdca634 } else {
3cbdca635 setUnauthorized(false);
3cbdca636 setError("Could not load Ring instances. Try restarting the API server.");
3cbdca637 }
3cbdca638 setInstances([]);
3cbdca639 } finally {
3cbdca640 setLoading(false);
3cbdca641 }
3cbdca642 }, []);
3cbdca643
3cbdca644 useEffect(() => {
3cbdca645 if (signedOut) return;
3cbdca646 void fetchInstances();
3cbdca647 }, [fetchInstances, signedOut]);
3cbdca648
3cbdca649 if (signedOut || unauthorized) {
3cbdca650 const loginHref = groveOrigin ? `${groveOrigin}/login` : "/login";
3cbdca651 const exploreHref = groveOrigin ? `${groveOrigin}/` : "/";
3cbdca652 return (
3cbdca653 <div className="max-w-3xl mx-auto px-4 py-8">
3cbdca654 <div
3cbdca655 className="p-4 sm:p-8 text-center"
3cbdca656 style={{
3cbdca657 backgroundColor: "var(--bg-card)",
3cbdca658 border: "1px solid var(--border-subtle)",
3cbdca659 }}
3cbdca660 >
3cbdca661 <div className="mx-auto mb-4 w-fit opacity-70">
3cbdca662 <RingLogo size={44} />
3cbdca663 </div>
3cbdca664 <h1 className="text-lg mb-1">Ring</h1>
3cbdca665 <p className="text-sm mb-4" style={{ color: "var(--text-faint)" }}>
3cbdca666 Collect and inspect repository logs in one place.
3cbdca667 </p>
3cbdca668 <div className="flex items-center justify-center gap-2">
3cbdca669 <Link
3cbdca670 href={loginHref}
3cbdca671 className="px-3 py-1.5 text-sm"
3cbdca672 style={{
3cbdca673 backgroundColor: "var(--accent)",
3cbdca674 color: "var(--accent-text)",
3cbdca675 }}
3cbdca676 >
3cbdca677 Sign in
3cbdca678 </Link>
3cbdca679 <Link
3cbdca680 href={exploreHref}
3cbdca681 className="px-3 py-1.5 text-sm"
3cbdca682 style={{
3cbdca683 border: "1px solid var(--border-subtle)",
3cbdca684 color: "var(--text-secondary)",
3cbdca685 }}
3cbdca686 >
3cbdca687 Explore Grove
3cbdca688 </Link>
3cbdca689 </div>
3cbdca690 </div>
3cbdca691 </div>
3cbdca692 );
3cbdca693 }
3cbdca694
3cbdca695 return (
3cbdca696 <div className="max-w-3xl mx-auto px-4 py-6">
3cbdca697 {error && (
3cbdca698 <div
3cbdca699 className="px-3 py-2 mb-2 text-xs"
3cbdca6100 style={{
3cbdca6101 color: "var(--status-closed-text)",
3cbdca6102 backgroundColor: "var(--status-closed-bg)",
3cbdca6103 border: "1px solid var(--status-closed-border)",
3cbdca6104 }}
3cbdca6105 >
3cbdca6106 {error}
3cbdca6107 </div>
3cbdca6108 )}
3cbdca6109
3cbdca6110 <div style={{ border: "1px solid var(--border-subtle)" }}>
3cbdca6111 {loading ? (
3cbdca6112 <div className="p-3 flex flex-col gap-2">
3cbdca6113 {Array.from({ length: 5 }).map((_, i) => (
3cbdca6114 <div key={i} className="skeleton" style={{ height: "2.2rem" }} />
3cbdca6115 ))}
3cbdca6116 </div>
3cbdca6117 ) : instances.length === 0 ? (
3cbdca6118 <div
3cbdca6119 className="px-3 py-8 text-sm text-center"
3cbdca6120 style={{
3cbdca6121 color: "var(--text-faint)",
3cbdca6122 backgroundColor: "var(--bg-card)",
3cbdca6123 }}
3cbdca6124 >
3cbdca6125 <div className="flex justify-center mb-2 opacity-70">
3cbdca6126 <RingLogo size={24} />
3cbdca6127 </div>
3cbdca6128 No Ring instances yet. Send logs to{" "}
3cbdca6129 <code>/api/repos/&lt;owner&gt;/&lt;repo&gt;/ring/logs</code>.
3cbdca6130 </div>
3cbdca6131 ) : (
3cbdca6132 <div>
3cbdca6133 {instances.map((instance, i) => (
3cbdca6134 <Link
3cbdca6135 key={`${instance.owner}/${instance.repo}`}
3cbdca6136 href={`/${instance.owner}/${instance.repo}`}
3cbdca6137 className="block hover-row px-3 py-2"
3cbdca6138 style={{ borderTop: i > 0 ? "1px solid var(--divide)" : undefined }}
3cbdca6139 >
3cbdca6140 <div className="flex items-start justify-between gap-3">
3cbdca6141 <div className="min-w-0">
3cbdca6142 <div className="text-sm truncate" style={{ color: "var(--text-primary)" }}>
3cbdca6143 {instance.owner}/{instance.repo}
3cbdca6144 </div>
3cbdca6145 <div className="text-xs mt-0.5 truncate" style={{ color: "var(--text-faint)" }}>
3cbdca6146 {instance.last_message ?? "No message yet"}
3cbdca6147 </div>
3cbdca6148 </div>
3cbdca6149 <div className="text-xs text-right shrink-0" style={{ color: "var(--text-faint)" }}>
3cbdca6150 <div>{instance.total} logs</div>
3cbdca6151 {instance.last_ts && <TimeAgo date={instance.last_ts} />}
3cbdca6152 </div>
3cbdca6153 </div>
3cbdca6154 </Link>
3cbdca6155 ))}
3cbdca6156 </div>
3cbdca6157 )}
3cbdca6158 </div>
3cbdca6159 </div>
3cbdca6160 );
3cbdca6161}