Vectrum
Sign In
128 lines · 5.5 KB · TypeScript
import Link from "next/link";
import type { Metadata } from "next";
import { revertPushAction } from "@/app/actions/activity";
import { ConfirmButton } from "@/components/confirm-button";
import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon, SparkleIcon } from "@/components/icons";
import { liftRowClass } from "@/components/repo-rows";
import { Blankslate, Box, ButtonLink, Label, PageTitle, Segmented, segmentClass } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { describeEvent, eventPath, isRevertable } from "@/lib/event-text";
import { listActiveAgents, listAgentActivity } from "@/lib/events";
import { formatDate, timeAgo } from "@/lib/format";
import { revParse } from "@/lib/git";
import { findRepoById } from "@/lib/repos";

export const metadata: Metadata = { title: "Agent activity" };

const PAGE_SIZE = 40;

export default async function AgentActivityPage({ searchParams }: PageProps<"/settings/activity">) {
  const viewer = await requireViewer("/settings/activity");
  const query = await searchParams;
  const agent = typeof query.agent === "string" ? query.agent : undefined;
  const page = Math.max(1, Number(query.page) || 1);
  const [agents, { events, total }] = await Promise.all([
    listActiveAgents(viewer.id),
    listAgentActivity(viewer.id, { agent, page, perPage: PAGE_SIZE }),
  ]);
  const repos = new Map<string, { base: string; name: string; dir: string } | null>();
  for (const e of events) {
    if (!repos.has(e.repoId)) {
      const r = await findRepoById(e.repoId);
      repos.set(e.repoId, r ? { base: `/${r.owner}/${r.name}`, name: `${r.owner}/${r.name}`, dir: r.dir } : null);
    }
  }
  // Pushes to branches that are gone can't be undone anymore.
  const undoable = new Set<string>();
  for (const e of events) {
    const repo = repos.get(e.repoId);
    if (repo && isRevertable(e) && (await revParse(repo.dir, String(e.payload.ref)))) undoable.add(e.id);
  }
  const href = (a?: string, p = 1) => `/settings/activity?${new URLSearchParams({ ...(a ? { agent: a } : {}), ...(p > 1 ? { page: String(p) } : {}) })}`;

  return (
    <div>
      <PageTitle
        title="Agent activity"
        description="Everything your agents did with your access, newest first: commits, pull requests, comments, issues. A push they made can be undone with a revert commit."
      />
      {agents.length > 1 && (
        <Segmented className="mb-4 flex-wrap">
          <Link href={href()} className={segmentClass(!agent)}>
            All agents
          </Link>
          {agents.map((a) => (
            <Link key={a.key} href={href(a.key)} className={segmentClass(agent === a.key)}>
              <SparkleIcon /> {a.name}
            </Link>
          ))}
        </Segmented>
      )}
      <Box className="p-1.5">
        {events.length === 0 ? (
          <Blankslate icon={<ActivityIcon />} title="Nothing yet">
            When an agent you connected changes something, it shows up here.
          </Blankslate>
        ) : (
          <ul>
            {events.map((e) => {
              const repo = repos.get(e.repoId);
              const reverted = e.payload.reverted as { commit: string; pull: number | null } | undefined;
              return (
                <li key={e.id} className={`${liftRowClass} flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2.5`}>
                  <span className="icon-tile size-8 shrink-0 rounded-row">
                    <SparkleIcon width={14} height={14} />
                  </span>
                  <div className="min-w-0 flex-1">
                    <p className="type-ui text-ink">
                      <strong className="font-medium">{e.agent?.name}</strong>{" "}
                      {repo ? (
                        <Link href={`${repo.base}${eventPath(e)}`} className="hover:underline">
                          {describeEvent(e)}
                        </Link>
                      ) : (
                        describeEvent(e)
                      )}
                    </p>
                    <p className="type-meta text-ink3">
                      {repo ? repo.name : "a deleted repository"} · <span title={formatDate(e.createdAt)}>{timeAgo(e.createdAt)}</span>
                    </p>
                  </div>
                  {reverted && (
                    <Link href={repo ? `${repo.base}${reverted.pull ? `/pulls/${reverted.pull}` : `/commit/${reverted.commit}`}` : "#"}>
                      <Label>Reverted</Label>
                    </Link>
                  )}
                  {undoable.has(e.id) && (
                    <ConfirmButton
                      action={revertPushAction.bind(null, e.id)}
                      message={`Undo this push? A new commit reverts its changes on the branch (or, if the branch is protected, a pull request does).`}
                      variant="default"
                      confirmLabel="Revert"
                    >
                      Undo
                    </ConfirmButton>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </Box>
      {(page > 1 || page * PAGE_SIZE < total) && (
        <div className="mt-6 flex justify-center gap-2">
          {page > 1 && (
            <ButtonLink href={href(agent, page - 1)} icon={<ChevronLeftIcon />}>
              Newer
            </ButtonLink>
          )}
          {page * PAGE_SIZE < total && (
            <ButtonLink href={href(agent, page + 1)}>
              Older <ChevronRightIcon />
            </ButtonLink>
          )}
        </div>
      )}
    </div>
  );
}