diff --git a/src/features/telegram/grants-page.tsx b/src/features/telegram/grants-page.tsx index 9d5ad79..1c491b3 100644 --- a/src/features/telegram/grants-page.tsx +++ b/src/features/telegram/grants-page.tsx @@ -1,10 +1,9 @@ -import { Link, useNavigate } from "@tanstack/react-router" +import { useNavigate } from "@tanstack/react-router" import { CalendarPlus, ShieldCheck } from "lucide-react" import { useDeferredValue, useMemo, useState } from "react" import { buttonMotion, - Chip, DataTable, type DataTableColumn, EmptyState, @@ -18,17 +17,16 @@ import { Count, PageBar, PageContent, Toolbar, useCan } from "@/components/shell import { CreateGrantDialog } from "@/components/telegram/create-grant-dialog" import { telegramUserName } from "@/components/telegram/telegram-user" import { Button } from "@/components/ui/button" -import type { getTelegramGrantsWithGrantors } from "@/features/telegram/grants.functions" -import type { TgUser } from "@/lib/api/types" +import type { getTelegramGrants } from "@/features/telegram/grants.functions" import { formatDate, formatDateTime } from "@/lib/format" -export type TelegramGrants = Awaited> +export type TelegramGrants = Awaited> type GrantRecord = TelegramGrants["ongoing"]["grants"][number] type Status = "active" | "scheduled" type Tab = "all" | "ongoing" | "scheduled" -type Row = GrantRecord & { grantor: TgUser | null; status: Status } +type Row = GrantRecord & { status: Status } const TAB_STATUS = { ongoing: "active", scheduled: "scheduled" } satisfies Record, Status> @@ -36,18 +34,13 @@ function userName(user: GrantRecord["user"], id: number) { return user ? telegramUserName(user) : `User ${id}` } -function searchText({ grant, user, grantor, status }: Row) { +function searchText({ grant, user, status }: Row) { return [ grant.userId, user?.firstName, user?.lastName, user?.username, - grant.grantedBy, grant.grantedBySub, - grant.source === "idp" ? "idp" : "legacy", - grantor?.firstName, - grantor?.lastName, - grantor?.username, grant.reason, formatDate(grant.validSince), formatDate(grant.validUntil), @@ -131,23 +124,15 @@ const columns: DataTableColumn[] = [ label: "Authorized by", priority: 3, minWidth: 130, - cell: ({ grant, grantor }) => ( + cell: ({ grant }) => ( By - + ), }, - { - id: "source", - label: "Source", - // Legacy grants were made before the IdP migration; both kinds count until the legacy table is dropped. - priority: 6, - minWidth: 100, - cell: ({ grant }) => {grant.source === "idp" ? "IdP" : "Legacy"}, - }, { id: "starts", label: "Starts", @@ -175,44 +160,21 @@ const columns: DataTableColumn[] = [ }, ] -/** - * Legacy grants name their grantor by Telegram ID, linked to that user; IdP grants by IdP subject (`grantedBySub`), - * which the backend does not resolve to a name. - */ -function Grantor({ grant, grantor }: Pick) { - if (grant.grantedBy === null) { - return grant.grantedBySub ? ( - - {grant.grantedBySub} - - ) : ( - - ) - } - // Its own link to the grantor; the row click still opens the grantee. - return ( - - event.stopPropagation()} - onKeyDown={(event) => event.stopPropagation()} - className={ - grantor - ? "block truncate text-(--pn-fg) underline-offset-2 transition-[color] duration-120 hover:text-(--pn-accent) hover:underline" - : "font-mono text-(--pn-fg) tabular-nums underline-offset-2 transition-[color] duration-120 hover:text-(--pn-accent) hover:underline" - } - > - {grantor ? telegramUserName(grantor) : grant.grantedBy} - +/** The grantor's IdP subject; the backend does not resolve it to a name. */ +function Grantor({ grant }: Pick) { + return grant.grantedBySub ? ( + + {grant.grantedBySub} + ) : ( + ) } const PAGE_SIZE = 20 /** Grants: ongoing and scheduled grants with a status filter; a row opens the grantee. */ -export function TelegramGrantsPage({ grants: { ongoing, scheduled, grantors } }: { grants: TelegramGrants }) { +export function TelegramGrantsPage({ grants: { ongoing, scheduled } }: { grants: TelegramGrants }) { const navigate = useNavigate() const canWrite = useCan("tg:grants:manage") const [query, setQuery] = useState("") @@ -224,14 +186,9 @@ export function TelegramGrantsPage({ grants: { ongoing, scheduled, grantors } }: const deferredQuery = useDeferredValue(query) const current = useMemo(() => { - const grantorById = new Map(grantors.map((user) => [user.id, user])) - const toRow = (status: Status) => (record: GrantRecord) => ({ - ...record, - grantor: record.grant.grantedBy === null ? null : (grantorById.get(record.grant.grantedBy) ?? null), - status, - }) + const toRow = (status: Status) => (record: GrantRecord) => ({ ...record, status }) return [...ongoing.grants.map(toRow("active")), ...scheduled.grants.map(toRow("scheduled"))] - }, [ongoing, scheduled, grantors]) + }, [ongoing, scheduled]) const counts = { all: current.length, @@ -302,7 +259,7 @@ export function TelegramGrantsPage({ grants: { ongoing, scheduled, grantors } }: label="Grants" columns={columns} rows={rows} - getRowId={(row) => row.grant.key} + getRowId={(row) => String(row.grant.id)} sort={sort} onSort={(next) => { setSort(next) diff --git a/src/features/telegram/grants.functions.ts b/src/features/telegram/grants.functions.ts index 92f69a0..a946109 100644 --- a/src/features/telegram/grants.functions.ts +++ b/src/features/telegram/grants.functions.ts @@ -3,41 +3,15 @@ import { z } from "zod" import { adminMiddleware, grantsWriteMiddleware } from "@/server/auth.middleware" -/** How long the grants page waits for grantor names before showing their ids instead. */ -const GRANTOR_LOOKUP_MS = 2000 - -/** - * The grants page: both grant lists plus the Telegram users who authorized legacy grants, so "Authorized by" can show - * a name. Grants made through the IdP name their grantor by IdP subject (`grantedBySub`) instead, which the backend - * does not resolve to a name. Grantors are looked up by id once the grants are in (one batched request, none without - * legacy grants); they are decoration, so a failed or slow lookup leaves the page with the grantor ids. - */ -export const getTelegramGrantsWithGrantors = createServerFn() +/** The grants page: ongoing and scheduled grants. The grantor is an IdP subject (`grantedBySub`). */ +export const getTelegramGrants = createServerFn() .middleware([adminMiddleware]) .handler(async ({ context }) => { const [ongoing, scheduled] = await Promise.all([ context.backend.tg.grants.getOngoing.query(), context.backend.tg.grants.getScheduled.query(), ]) - const grantorIds = new Set( - [...ongoing.grants, ...scheduled.grants].flatMap(({ grant }) => - grant.grantedBy === null ? [] : [grant.grantedBy] - ) - ) - const signal = AbortSignal.timeout(GRANTOR_LOOKUP_MS) - const lookups = await Promise.all( - Array.from(grantorIds, (userId) => - context.backend.tg.users.get - .query({ userId }, { signal }) - .then(({ user }) => user) - .catch((error) => { - console.error(error) - return null - }) - ) - ) - const grantors = lookups.filter((user) => user !== null && user !== undefined) - return { ongoing, scheduled, grantors } + return { ongoing, scheduled } }) const grantInput = z diff --git a/src/features/telegram/user-detail/profile.tsx b/src/features/telegram/user-detail/profile.tsx index b8748f0..0c4d76b 100644 --- a/src/features/telegram/user-detail/profile.tsx +++ b/src/features/telegram/user-detail/profile.tsx @@ -184,7 +184,7 @@ export function TelegramUserDetailPage({ data }: { data: TelegramUserDetail }) { type GrantWindow = Pick -type GrantsSectionProps = { ongoing: GrantWindow | null; scheduled: (GrantWindow & { key: string })[] } +type GrantsSectionProps = { ongoing: GrantWindow | null; scheduled: (GrantWindow & { id: number })[] } function GrantsSection({ ongoing, scheduled }: GrantsSectionProps) { // `tg.grants.checkUser` returns the ongoing grant without a key or reason; a user has at most one. @@ -214,7 +214,7 @@ function GrantsSection({ ongoing, scheduled }: GrantsSectionProps) { ) : null} {scheduled.map((grant) => (
  • diff --git a/src/routes/dashboard/telegram/grants.tsx b/src/routes/dashboard/telegram/grants.tsx index d7746b9..288dc05 100644 --- a/src/routes/dashboard/telegram/grants.tsx +++ b/src/routes/dashboard/telegram/grants.tsx @@ -3,15 +3,15 @@ import { createFileRoute } from "@tanstack/react-router" import { TableSkeleton } from "@/components/primitives" import { PageBar, PageContent } from "@/components/shell" import { TelegramGrantsPage } from "@/features/telegram/grants-page" -import { getTelegramGrantsWithGrantors } from "@/features/telegram/grants.functions" +import { getTelegramGrants } from "@/features/telegram/grants.functions" export const Route = createFileRoute("/dashboard/telegram/grants")({ - loader: () => getTelegramGrantsWithGrantors(), + loader: () => getTelegramGrants(), pendingComponent: () => ( <> - + ),