693 lines · 28.4 KB · TypeScript
RawDownload
import { useState } from "react";import { useAtomRefresh, useAtomValue } from "@effect/atom-react";import { useParams } from "@tanstack/react-router";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Cause from "effect/Cause";import * as Option from "effect/Option";import * as Predicate from "effect/Predicate";import type { HealthStatus, Integration, IntegrationSlug } from "@executor-js/sdk/shared";import { useIntegrationPlugins } from "@executor-js/sdk/client"; import {  ADMIN_USERS_PAGE_SIZE,  adminUserConnectionsAtom,  adminUsersWithConnectionsAtom,} from "../api/admin-atoms";import { integrationsOptimisticAtom } from "../api/atoms";import { ownerLabel } from "../api/owner-display";import { Button } from "../components/button";import { CopyButton } from "../components/copy-button";import { ErrorState } from "../components/error-state";import {  IntegrationFavicon,  integrationInferredUrl,  integrationPresetIconUrl,} from "../components/integration-favicon";import { PageContainer, PageHeader } from "../components/page";import {  Sheet,  SheetContent,  SheetDescription,  SheetHeader,  SheetTitle,} from "../components/sheet";import { Skeleton } from "../components/skeleton";import {  adminUserCopyableEmail,  adminUserTitle,  connectionHealthStatus,  connectLinkUrl,  formatAdminDate,  formatLastSeen,  integrationConnectionStates,  lastSeenTitle,  pageNumber,  shortenExternalId,  splitPage,  type AdminCatalogRow,  type AdminConnectionRow,  type AdminUserIdentityRow,} from "../lib/admin-users-display";import {  HEALTH_INDICATOR_COLOR,  HEALTH_STATUS_LABEL,  HEALTH_TEXT_CLASS,} from "../lib/health-display";import { isAsyncResultLoading } from "../lib/async-result";import { useExecutorDocumentTitle } from "../lib/document-title"; // ---------------------------------------------------------------------------// Admin · Users — the tenant-wide operator view.//// The product plane answers "what can I reach"; this page answers the owner's// question: who are my users, and what has each of them connected. It reads the// admin plane only (`/admin/users*`), which is read-only by construction, so// there is nothing to mutate here — every affordance is a read or a copy.//// ACCESS: the server is the gate. A plain member's request comes back 403 (401// with no session), and the page renders an explicit denial rather than an// empty table, so "you may not see this" never reads as "there is nobody here".// The nav item is separately hidden from non-admins (see lib/admin-access), but// that is convenience — this is the surface that actually refuses.//// The row is a button rather than a link: a user has no page of its own, and a// detail sheet keeps the list's paging position while an operator scans several// users in a row.// --------------------------------------------------------------------------- type AdminUserRow = {  readonly externalId: string;  readonly createdAt: number;  readonly lastSeenAt: number | null;  readonly status: string | null;  /** Joined server-side from the host's member directory. Null whenever the   *  host couldn't resolve the id — see `adminUserTitle` for what that renders   *  as. */  readonly email: string | null;  readonly displayName: string | null;  readonly connections: readonly AdminConnectionRow[];}; /** 401/403 from the admin plane. Both mean "not an operator here" and get the *  same denial: the distinction (no session vs wrong role) is the server's, and *  a signed-in member reading "unauthorized" would only confuse. */const isAccessDenied = (cause: Cause.Cause<unknown>): boolean =>  Option.match(Cause.findErrorOption(cause), {    onNone: () => false,    onSome: (error) =>      Predicate.isTagged(error, "AdminUsersForbidden") ||      Predicate.isTagged(error, "AdminUsersUnauthorized"),  }); // ── Access denied ─────────────────────────────────────────────────────────── function AccessDenied() {  return (    <div className="rounded-lg border border-border bg-card p-8">      <p className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">        Admin only      </p>      <h2 className="mt-2 text-base font-semibold text-foreground">        You don&apos;t have access to this workspace&apos;s users      </h2>      <p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">        Reading every user and what they&apos;ve connected requires an admin role. Ask an admin of        this workspace if you need it.      </p>    </div>  );} // ── Health ────────────────────────────────────────────────────────────────── /** The shared health vocabulary, rendered from a stored verdict. * *  Deliberately NOT `IntegrationHealthSummary`: that component probes upstream *  on mount through the product plane, which is both the wrong plane here (an *  admin is not the credential's owner) and the wrong behavior (opening an *  admin view must not fire traffic at every user's provider). This reads the *  `lastHealth` the admin payload already carries. */function HealthIndicator(props: { readonly status: HealthStatus }) {  const label = HEALTH_STATUS_LABEL[props.status];  return (    <span className="flex shrink-0 items-center gap-1.5" title={`Status: ${label}`}>      <span        className={`font-mono text-[11px] font-medium uppercase tracking-[0.08em] ${HEALTH_TEXT_CLASS[props.status]}`}      >        {label}      </span>      <span        aria-label={`Status: ${label}`}        className={`size-2 rounded-full ${HEALTH_INDICATOR_COLOR[props.status].dot}`}      />    </span>  );} // ── Integration icons ─────────────────────────────────────────────────────── /** The catalog joined into a favicon lookup. The admin payload carries only a *  slug per connection (no kind, no display URL — the platform view hands out *  nothing it doesn't have to), so brand marks come from the tenant's own *  integration catalog, which the admin can read on the product plane. */type IconLookup = (integration: IntegrationSlug) => {  readonly icon: string | null;  readonly url: string | undefined;  readonly name: string;}; const useIntegrationIcons = (): IconLookup => {  const catalog = useAtomValue(integrationsOptimisticAtom);  const integrationPlugins = useIntegrationPlugins();  const integrations = Option.getOrElse(    AsyncResult.value(catalog),    (): readonly Integration[] => [],  );   return (integration) => {    const slug = String(integration);    const found = integrations.find((row) => String(row.slug) === slug);    const name = found?.name || slug;    return {      icon: found        ? integrationPresetIconUrl(            { id: slug, kind: found.kind, name, url: found.displayUrl },            integrationPlugins,          )        : null,      url: found?.displayUrl ?? integrationInferredUrl({ id: slug, name }) ?? undefined,      name,    };  };}; /** The catalog rows this view needs — slug plus kind, since `kind` is what says *  whether an integration can be connected at all (see *  `isConnectableIntegration`). Empty while the catalog loads, which degrades to *  "connected only" rather than blocking the users table on a second request. */const useCatalogRows = (): readonly AdminCatalogRow[] => {  const catalog = useAtomValue(integrationsOptimisticAtom);  return Option.match(AsyncResult.value(catalog), {    onNone: (): readonly AdminCatalogRow[] => [],    onSome: (integrations: readonly Integration[]) =>      integrations.map((row) => ({ slug: row.slug, kind: row.kind })),  });}; /** * The connection summary: one slot per integration in the catalog, lit when * this user has connected it and greyed when they haven't. * * Showing the not-connected slots is the point — the grid answers "what is * available and what has this person actually set up", which a list of only * their connections cannot. */function ConnectionGrid(props: {  readonly catalog: readonly AdminCatalogRow[];  readonly connections: readonly AdminConnectionRow[];  readonly icons: IconLookup;}) {  const states = integrationConnectionStates(props.catalog, props.connections);  if (states.length === 0) {    return <span className="font-mono text-[11px] text-muted-foreground">No integrations</span>;  }   const connectedCount = states.filter((state) => state.connected).length;   return (    <div className="flex min-w-0 items-center gap-1.5">      <div className="flex min-w-0 flex-wrap items-center gap-1">        {states.slice(0, 8).map((state) => {          const { icon, url, name } = props.icons(state.integration);          return (            <span              key={state.integration}              // State on data-* (the repo convention) so the connected/not-              // connected distinction is readable without parsing classes.              data-slot="admin-user-integration"              data-integration={String(state.integration)}              data-connected={state.connected ? "true" : "false"}              title={`${name} — ${state.connected ? "connected" : "not connected"}`}              className={                state.connected                  ? "flex size-5 items-center justify-center"                  : "flex size-5 items-center justify-center opacity-25 grayscale"              }            >              <IntegrationFavicon                icon={icon}                integrationId={String(state.integration)}                url={url}                size={16}              />            </span>          );        })}      </div>      <span        data-slot="admin-user-connection-count"        className="shrink-0 font-mono text-[11px] text-muted-foreground"        title={`${connectedCount} of ${states.length} integrations connected`}      >        {connectedCount}/{states.length}      </span>    </div>  );} // ── Identity ──────────────────────────────────────────────────────────────── /** * The user cell: who this is, then the opaque id underneath. * * The host joins email/name onto each row server-side, so the headline is * something an operator recognizes and the `externalId` becomes the supporting * line — still mono, still carrying the full value in its title, still under the * same `data-slot` it always had, because the id is what identifies a row to * anything reading this table. When the host resolved no identity the cell * collapses back to exactly the single mono id line this table has always shown. * * NO copy button lives here: the row itself is a `<button>` (a user has no page * of its own, so the row opens a detail sheet), and a button inside a button is * invalid markup that swallows its own clicks. The copy affordances live one * click away in the sheet header, on the same values. */function UserIdentityCell(props: { readonly user: AdminUserIdentityRow }) {  const title = adminUserTitle(props.user);   if (title.primaryIsId) {    return (      <span        data-slot="admin-user-id"        className="min-w-0 truncate font-mono text-sm text-foreground"        title={props.user.externalId}      >        {shortenExternalId(title.primary)}      </span>    );  }   return (    <span className="flex min-w-0 flex-col gap-0.5">      <span        data-slot="admin-user-name"        className="truncate text-sm text-foreground"        title={title.primary}      >        {title.primary}      </span>      <span        data-slot="admin-user-id"        className="truncate font-mono text-[11px] text-muted-foreground"        title={props.user.externalId}      >        {shortenExternalId(props.user.externalId)}      </span>    </span>  );} /** * The sheet header: the same title treatment as the row, plus the copy * affordances the row can't carry. * * Two separate buttons rather than one, because they yield different things and * an operator wants a specific one: the email is what they paste into a mail * client or a support ticket, and the `externalId` is what they paste into a log * search or an API call. A single "copy" would force them to guess which. * * The email button is absent — not disabled — when the host resolved no email, * since there is nothing to put on the clipboard. The id button is always * present, whether the id is the headline (no identity resolved) or the * supporting line: every user has an id by definition, and it is the value that * identifies them to logs and to the API. */function UserDetailTitle(props: { readonly user: AdminUserRow }) {  const title = adminUserTitle(props.user);  const email = adminUserCopyableEmail(props.user);   return (    <div className="flex min-w-0 flex-col gap-1">      <div className="flex min-w-0 items-center gap-1">        <SheetTitle          data-slot="admin-user-detail-title"          className={            title.primaryIsId ? "truncate font-mono text-sm" : "truncate text-sm font-semibold"          }          title={title.primary}        >          {title.primary}        </SheetTitle>        {email ? (          <CopyButton value={email} label="Copy email" />        ) : (          title.primaryIsId && <CopyButton value={props.user.externalId} label="Copy ID" />        )}      </div>      {title.secondary !== null && (        <div className="flex min-w-0 items-center gap-1">          <span            data-slot="admin-user-detail-id"            className="truncate font-mono text-[11px] text-muted-foreground"            title={props.user.externalId}          >            {props.user.externalId}          </span>          <CopyButton value={props.user.externalId} label="Copy ID" />        </div>      )}    </div>  );} // ── Detail sheet ──────────────────────────────────────────────────────────── function UserDetail(props: {  readonly user: AdminUserRow;  readonly catalog: readonly AdminCatalogRow[];  readonly icons: IconLookup;  readonly orgSlug: string | undefined;}) {  const result = useAtomValue(adminUserConnectionsAtom(props.user.externalId));  const refresh = useAtomRefresh(adminUserConnectionsAtom(props.user.externalId));  // The connect link targets the recipient's own session, so it is built for  // this deployment's origin and copied out — never navigated to from here. It  // carries THIS admin's org so a multi-org recipient connects in the workspace  // the link came from, not whichever one their session defaults to.  const origin = globalThis.window?.location?.origin ?? "";   if (isAsyncResultLoading(result)) {    return (      <div className="flex flex-col gap-2 p-6">        {[0, 1, 2].map((row) => (          <Skeleton key={row} className="h-12" />        ))}      </div>    );  }   return AsyncResult.match(result, {    onInitial: () => (      <div className="flex flex-col gap-2 p-6">        {[0, 1, 2].map((row) => (          <Skeleton key={row} className="h-12" />        ))}      </div>    ),    onFailure: (failure) =>      isAccessDenied(failure.cause) ? (        <div className="p-6">          <AccessDenied />        </div>      ) : (        <div className="p-6">          <ErrorState message="Couldn't load this user's connections" onRetry={refresh} />        </div>      ),    onSuccess: ({ value }) => {      const states = integrationConnectionStates(props.catalog, value.connections);      const connected = states.filter((state) => state.connected);      const available = states.filter((state) => !state.connected);       return (        <div className="flex flex-col gap-8 overflow-y-auto p-6">          <section>            <h3 className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">              Connected · {connected.length}            </h3>            {connected.length === 0 ? (              <p className="mt-3 text-sm leading-6 text-muted-foreground">                This user hasn&apos;t connected anything yet. Send them a connect link below to get                started.              </p>            ) : (              <div className="mt-3 overflow-hidden rounded-lg border border-border bg-card">                {connected.flatMap((state) =>                  state.connections.map((connection) => {                    const { icon, url, name } = props.icons(connection.integration);                    return (                      <div                        key={`${connection.integration}/${connection.owner}/${connection.name}`}                        className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-b-0"                      >                        <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center">                          <IntegrationFavicon                            icon={icon}                            integrationId={String(connection.integration)}                            url={url}                            size={16}                          />                        </span>                        <div className="min-w-0 flex-1">                          <div className="flex items-center gap-2">                            <p className="truncate text-sm font-medium text-foreground">{name}</p>                            <span className="shrink-0 font-mono text-[11px] text-muted-foreground">                              {ownerLabel(connection.owner)}                            </span>                          </div>                          <p className="mt-1 truncate font-mono text-xs text-muted-foreground">                            {connection.name}                          </p>                          {connection.oauthScope ? (                            <p                              className="mt-1.5 break-words font-mono text-[11px] leading-5 text-muted-foreground"                              title={connection.oauthScope}                            >                              {connection.oauthScope}                            </p>                          ) : (                            <p className="mt-1.5 font-mono text-[11px] text-muted-foreground">                              No OAuth scope — static credential                            </p>                          )}                        </div>                        <HealthIndicator status={connectionHealthStatus(connection)} />                      </div>                    );                  }),                )}              </div>            )}          </section>           <section>            <h3 className="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">              Not connected · {available.length}            </h3>            <p className="mt-2 text-sm leading-6 text-muted-foreground">              Copy a link and send it to this user — it opens the connect flow in their own session.            </p>            {available.length === 0 ? (              <p className="mt-3 text-sm leading-6 text-muted-foreground">                This user has connected every integration in the catalog.              </p>            ) : (              <div className="mt-3 overflow-hidden rounded-lg border border-border bg-card">                {available.map((state) => {                  const { icon, url, name } = props.icons(state.integration);                  const link = connectLinkUrl(state.integration, origin, props.orgSlug);                  return (                    <div                      key={state.integration}                      className="flex items-center gap-3 border-b border-border px-4 py-3 last:border-b-0"                    >                      <span className="flex size-5 shrink-0 items-center justify-center opacity-40 grayscale">                        <IntegrationFavicon                          icon={icon}                          integrationId={String(state.integration)}                          url={url}                          size={16}                        />

Lines 1–500 of 693.