()
| 603 | // ── Page ──────────────────────────────────────────────────────────────────── |
| 604 | |
| 605 | export function AdminUsersPage() { |
| 606 | useExecutorDocumentTitle("Users"); |
| 607 | const [offset, setOffset] = useState(0); |
| 608 | const [selected, setSelected] = useState<AdminUserRow | null>(null); |
| 609 | const search = useDebouncedSearch(); |
| 610 | |
| 611 | // A new term is a new list, so it starts on its first page: an offset kept |
| 612 | // from a broader list would land past the end of a narrower one. |
| 613 | const page = { limit: ADMIN_USERS_PAGE_SIZE, offset, search: search.term }; |
| 614 | const result = useAtomValue(adminUsersWithConnectionsAtom(page)); |
| 615 | const refresh = useAtomRefresh(adminUsersWithConnectionsAtom(page)); |
| 616 | const catalog = useCatalogRows(); |
| 617 | const icons = useIntegrationIcons(); |
| 618 | // The route is `/{-$orgSlug}/users`: the segment is OPTIONAL, so this reads |
| 619 | // `undefined` on any host that renders no segment and the link falls back to |
| 620 | // the bare form. Both cloud and self-host do canonicalize onto a slug (self- |
| 621 | // host onto `default`), so in practice the prefixed form is what ships — but |
| 622 | // the optional param is the contract, not an assumption about the host. |
| 623 | const { orgSlug } = useParams({ strict: false }) as { orgSlug?: string }; |
| 624 | |
| 625 | const header = ( |
| 626 | <PageHeader |
| 627 | title="Users" |
| 628 | description="Everyone who has used this workspace, and what each of them has connected." |
| 629 | /> |
| 630 | ); |
| 631 | |
| 632 | const loading = ( |
| 633 | <div className="flex flex-col gap-2"> |
| 634 | {[0, 1, 2, 3].map((row) => ( |
| 635 | <Skeleton key={row} className="h-14" /> |
| 636 | ))} |
| 637 | </div> |
| 638 | ); |
| 639 | |
| 640 | const searching = search.term !== ""; |
| 641 | |
| 642 | return ( |
| 643 | <PageContainer> |
| 644 | {header} |
| 645 | |
| 646 | <UserSearch |
| 647 | value={search.typed} |
| 648 | onChange={(value) => { |
| 649 | search.setTyped(value); |
| 650 | setOffset(0); |
| 651 | }} |
| 652 | onClear={() => { |
| 653 | search.clear(); |
| 654 | setOffset(0); |
| 655 | }} |
| 656 | /> |
| 657 | |
| 658 | {isAsyncResultLoading(result) |
| 659 | ? loading |
| 660 | : AsyncResult.match(result, { |
| 661 | onInitial: () => loading, |
| 662 | onFailure: (failure) => |
nothing calls this directly
no test coverage detected