392 lines · 12.1 KB · TypeScript
RawDownload
import { GitPullRequestIcon, RefreshCwIcon } from "lucide-react";import { Option } from "effect";import { type ReactNode } from "react";import type {  SourceControlProviderKind,  SourceControlDiscoveryResult,  SourceControlProviderAuth,  SourceControlProviderDiscoveryItem,  VcsDriverKind,  VcsDiscoveryItem,} from "@t3tools/contracts"; import { cn } from "../../lib/utils";import {  refreshSourceControlDiscovery,  useSourceControlDiscovery,} from "../../lib/sourceControlDiscoveryState";import { Badge } from "../ui/badge";import { Button } from "../ui/button";import {  Empty,  EmptyContent,  EmptyDescription,  EmptyHeader,  EmptyMedia,  EmptyTitle,} from "../ui/empty";import { Skeleton } from "../ui/skeleton";import { Switch } from "../ui/switch";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import {  AzureDevOpsIcon,  BitbucketIcon,  GitHubIcon,  GitIcon,  GitLabIcon,  JujutsuIcon,  type Icon,} from "../Icons";import { RedactedSensitiveText } from "./RedactedSensitiveText";import { SettingsPageContainer, SettingsSection } from "./settingsLayout"; const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {  versionControlSystems: [],  sourceControlProviders: [],}; const SOURCE_CONTROL_PROVIDER_ICONS: Partial<Record<SourceControlProviderKind, Icon>> = {  github: GitHubIcon,  gitlab: GitLabIcon,  "azure-devops": AzureDevOpsIcon,  bitbucket: BitbucketIcon,}; const VCS_ICONS: Partial<Record<VcsDriverKind, Icon>> = {  git: GitIcon,  jj: JujutsuIcon,}; const SOURCE_CONTROL_SKELETON_ROWS = ["primary", "secondary"] as const; function optionLabel(value: Option.Option<string>): string | null {  return Option.getOrNull(value);} function isProviderDiscoveryItem(  item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem,): item is SourceControlProviderDiscoveryItem {  return "auth" in item;} function isVcsNotReady(item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem): boolean {  return !isProviderDiscoveryItem(item) && !item.implemented;} function authPresentation(auth: SourceControlProviderAuth): {  readonly label: string;  readonly badge: "warning" | null;} {  if (auth.status === "authenticated") {    return { label: "Authenticated", badge: null };  }  if (auth.status === "unauthenticated") {    return { label: "Not authenticated", badge: "warning" };  }  return { label: "Status unknown", badge: null };} function RedactedAccount(props: { readonly account: string | null }) {  return (    <RedactedSensitiveText      value={props.account}      ariaLabel="Toggle source control account visibility"      revealTooltip="Click to reveal account"      hideTooltip="Click to hide account"    />  );} function itemStatusDot(item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem): string {  if (isVcsNotReady(item)) return "bg-muted-foreground/35";  if (item.status !== "available") return "bg-warning";  if (isProviderDiscoveryItem(item) && item.auth.status !== "authenticated") return "bg-warning";  return "bg-success";} function SourceControlItemMark({  item,}: {  readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem;}) {  const dotClassName = itemStatusDot(item);  const Icon = isProviderDiscoveryItem(item)    ? SOURCE_CONTROL_PROVIDER_ICONS[item.kind]    : VCS_ICONS[item.kind];   if (!Icon) {    return <span className={cn("size-2 shrink-0 rounded-full", dotClassName)} aria-hidden />;  }   return (    <span className="relative inline-flex size-5 shrink-0 items-center justify-center">      <Icon className="size-4.5 text-foreground/80" aria-hidden />      <span        className={cn(          "pointer-events-none absolute -left-0.5 -top-0.5 size-2 rounded-full ring-2 ring-background",          dotClassName,        )}        aria-hidden      />    </span>  );} function itemSummary({  item,  auth,  authAccount,}: {  readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem;  readonly auth: SourceControlProviderAuth | null;  readonly authAccount: string | null;}) {  if (isVcsNotReady(item)) {    return <span>Support for {item.label} is coming soon.</span>;  }   if (item.status !== "available") {    return <span>Not available on this server — {item.installHint}</span>;  }   if (auth) {    if (auth.status === "authenticated") {      return (        <>          <span>Authenticated</span>          {authAccount ? (            <>              <span aria-hidden>as</span>              <RedactedAccount account={authAccount} />            </>          ) : null}        </>      );    }     if (!item.executable) {      return <span>{item.installHint}</span>;    }     if (auth.status === "unauthenticated") {      return (        <span>          {item.label} is not authenticated on this server. Sign in or configure credentials using          the <code className="rounded bg-muted px-1 py-px text-[11px]">{item.executable}</code>{" "}          tool on the server host to enable pull request features.        </span>      );    }    return (      <span>        Could not verify {item.label}. {item.installHint}      </span>    );  }   return <span>Available</span>;} function DiscoveryItemRow({  item,}: {  readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem;}) {  const version = optionLabel(item.version);  const enabled =    item.status === "available" && (isProviderDiscoveryItem(item) || item.implemented);  const auth = isProviderDiscoveryItem(item) ? item.auth : null;  const authStatus = auth ? authPresentation(auth) : null;  const authAccount = auth ? optionLabel(auth.account) : null;   return (    <div      className={cn(        "border-t border-border/60 first:border-t-0",        isVcsNotReady(item) && "opacity-80",      )}    >      <div className="px-4 py-3.5 sm:px-5">        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">          <div className="min-w-0 flex-1 space-y-1">            <div className="flex min-w-0 flex-wrap items-center gap-2">              <SourceControlItemMark item={item} />              <h3 className="truncate text-[13px] font-semibold tracking-[-0.01em] text-foreground">                {item.label}              </h3>              {version ? <code className="text-xs text-muted-foreground">{version}</code> : null}              {isVcsNotReady(item) ? (                <Badge variant="warning" size="sm">                  Coming Soon                </Badge>              ) : null}              {authStatus?.badge ? (                <Badge variant={authStatus.badge} size="sm">                  {authStatus.label}                </Badge>              ) : null}            </div>            <p className="flex min-w-0 flex-wrap items-center gap-x-1 text-xs text-muted-foreground">              {itemSummary({ item, auth, authAccount })}            </p>          </div>          <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">            {!isVcsNotReady(item) ? (              <Switch checked={enabled} disabled aria-label={`${item.label} availability`} />            ) : null}          </div>        </div>      </div>    </div>  );} function SourceControlSectionSkeleton({  title,  headerAction,}: {  readonly title: string;  readonly headerAction?: ReactNode;}) {  return (    <SettingsSection title={title} headerAction={headerAction}>      {SOURCE_CONTROL_SKELETON_ROWS.map((row) => (        <div key={row} className="border-t border-border/60 px-4 py-3.5 first:border-t-0 sm:px-5">          <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">            <div className="min-w-0 flex-1 space-y-2">              <div className="flex items-center gap-2">                <span className="relative inline-flex size-5 shrink-0 items-center justify-center">                  <Skeleton className="size-4.5 rounded-md" />                  <Skeleton                    className="pointer-events-none absolute -left-0.5 -top-0.5 size-2 rounded-full ring-2 ring-background"                    aria-hidden                  />                </span>                <Skeleton className="h-4 w-28 rounded-full" />                <Skeleton className="h-5 w-14 rounded-full" />              </div>              <Skeleton className="h-3 w-full max-w-xs rounded-full" />            </div>            <div className="flex shrink-0 items-center gap-2">              <Skeleton className="size-7 rounded-md" />              <Skeleton className="h-5 w-9 rounded-full" />            </div>          </div>        </div>      ))}    </SettingsSection>  );} function EmptySourceControlDiscovery({  error,  isPending,  onScan,}: {  readonly error: string | null;  readonly isPending: boolean;  readonly onScan: () => void;}) {  const hasError = error !== null;   return (    <SettingsSection title="Server environment">      <Empty className="min-h-88">        <EmptyMedia variant="icon">          <GitPullRequestIcon />        </EmptyMedia>        <EmptyHeader>          <EmptyTitle>            {hasError ? "Could not scan the server environment" : "Nothing detected yet"}          </EmptyTitle>          <EmptyDescription>            {hasError              ? error              : "Install Git on the server, add optional hosting integrations or credentials your workspace needs, then rescan."}          </EmptyDescription>        </EmptyHeader>        <EmptyContent>          <Button            size="sm"            variant="outline"            className="h-8 gap-1.5 px-3 text-xs"            onClick={onScan}            disabled={isPending}          >            <RefreshCwIcon className={cn("size-3.5", isPending && "animate-spin")} />            Scan          </Button>        </EmptyContent>      </Empty>    </SettingsSection>  );} export function SourceControlSettingsPanel() {  const discovery = useSourceControlDiscovery();   const result = discovery.data ?? EMPTY_DISCOVERY_RESULT;  const hasDiscoveryItems =    result.versionControlSystems.length > 0 || result.sourceControlProviders.length > 0;  const isInitialScanPending = discovery.isPending && discovery.data === null;  const handleScan = () => {    void refreshSourceControlDiscovery();  };  const scanButton = (    <Tooltip>      <TooltipTrigger        render={          <Button            size="icon-xs"            variant="ghost"            className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"            onClick={handleScan}            disabled={discovery.isPending}            aria-label="Rescan server environment"          >            <RefreshCwIcon className={cn("size-3", discovery.isPending && "animate-spin")} />          </Button>        }      />      <TooltipPopup side="top">Rescan Git and hosting integrations</TooltipPopup>    </Tooltip>  );   return (    <SettingsPageContainer>      {isInitialScanPending ? (        <>          <SourceControlSectionSkeleton title="Version Control" headerAction={scanButton} />          <SourceControlSectionSkeleton title="Source Control Providers" />        </>      ) : hasDiscoveryItems ? (        <>          {result.versionControlSystems.length > 0 ? (            <SettingsSection title="Version Control" headerAction={scanButton}>              {result.versionControlSystems.map((item) => (                <DiscoveryItemRow key={`vcs:${item.kind}`} item={item} />              ))}            </SettingsSection>          ) : null}           {result.sourceControlProviders.length > 0 ? (            <SettingsSection              title="Source Control Providers"              headerAction={result.versionControlSystems.length === 0 ? scanButton : null}            >              {result.sourceControlProviders.map((item) => (                <DiscoveryItemRow key={`provider:${item.kind}`} item={item} />              ))}            </SettingsSection>          ) : null}        </>      ) : (        <EmptySourceControlDiscovery          error={discovery.error}          isPending={discovery.isPending}          onScan={handleScan}        />      )}    </SettingsPageContainer>  );}