471 lines · 15.3 KB · TypeScript
RawDownload
import { GitPullRequestIcon, RefreshCwIcon } from "lucide-react";import { Option } from "effect";import { type ReactNode, useId } from "react";import type {  SourceControlProviderKind,  SourceControlDiscoveryResult,  SourceControlProviderAuth,  SourceControlProviderDiscoveryItem,  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 { GitHubIcon, type Icon } from "../Icons";import { RedactedSensitiveText } from "./RedactedSensitiveText";import { SettingsPageContainer, SettingsSection } from "./settingsLayout"; const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {  versionControlSystems: [],  sourceControlProviders: [],}; const GitLabIcon: Icon = (props) => (  <svg {...props} viewBox="0 0 32 32" fill="none">    <path      d="m31.46 12.78-.04-.12-4.35-11.35A1.14 1.14 0 0 0 25.94.6c-.24 0-.47.1-.66.24-.19.15-.33.36-.39.6l-2.94 9h-11.9l-2.94-9A1.14 1.14 0 0 0 6.07.58a1.15 1.15 0 0 0-1.14.72L.58 12.68l-.05.11a8.1 8.1 0 0 0 2.68 9.34l.02.01.04.03 6.63 4.97 3.28 2.48 2 1.52a1.35 1.35 0 0 0 1.62 0l2-1.52 3.28-2.48 6.67-5h.02a8.09 8.09 0 0 0 2.7-9.36Z"      fill="#E24329"    />    <path      d="m31.46 12.78-.04-.12a14.75 14.75 0 0 0-5.86 2.64l-9.55 7.24 6.09 4.6 6.67-5h.02a8.09 8.09 0 0 0 2.67-9.36Z"      fill="#FC6D26"    />    <path      d="m9.9 27.14 3.28 2.48 2 1.52a1.35 1.35 0 0 0 1.62 0l2-1.52 3.28-2.48-6.1-4.6-6.07 4.6Z"      fill="#FCA326"    />    <path      d="M6.44 15.3a14.71 14.71 0 0 0-5.86-2.63l-.05.12a8.1 8.1 0 0 0 2.68 9.34l.02.01.04.03 6.63 4.97 6.1-4.6-9.56-7.24Z"      fill="#FC6D26"    />  </svg>); const AzureDevOpsIcon: Icon = (props) => {  const id = useId().replaceAll(":", "");  const gradientA = `${id}-azure-a`;  const gradientB = `${id}-azure-b`;  const gradientC = `${id}-azure-c`;   return (    <svg {...props} viewBox="0 0 96 96">      <defs>        <linearGradient          id={gradientA}          x1="-1032.17"          x2="-1059.21"          y1="145.31"          y2="65.43"          gradientTransform="matrix(1 0 0 -1 1075 158)"          gradientUnits="userSpaceOnUse"        >          <stop offset="0" stopColor="#114a8b" />          <stop offset="1" stopColor="#0669bc" />        </linearGradient>        <linearGradient          id={gradientB}          x1="-1023.73"          x2="-1029.98"          y1="108.08"          y2="105.97"          gradientTransform="matrix(1 0 0 -1 1075 158)"          gradientUnits="userSpaceOnUse"        >          <stop offset="0" stopOpacity=".3" />          <stop offset=".07" stopOpacity=".2" />          <stop offset=".32" stopOpacity=".1" />          <stop offset=".62" stopOpacity=".05" />          <stop offset="1" stopOpacity="0" />        </linearGradient>        <linearGradient          id={gradientC}          x1="-1027.16"          x2="-997.48"          y1="147.64"          y2="68.56"          gradientTransform="matrix(1 0 0 -1 1075 158)"          gradientUnits="userSpaceOnUse"        >          <stop offset="0" stopColor="#3ccbf4" />          <stop offset="1" stopColor="#2892df" />        </linearGradient>      </defs>      <path        fill={`url(#${gradientA})`}        d="M33.34 6.54h26.04l-27.03 80.1a4.15 4.15 0 0 1-3.94 2.81H8.15a4.14 4.14 0 0 1-3.93-5.47L29.4 9.38a4.15 4.15 0 0 1 3.94-2.83z"      />      <path        fill="#0078d4"        d="M71.17 60.26H29.88a1.91 1.91 0 0 0-1.3 3.31l26.53 24.76a4.17 4.17 0 0 0 2.85 1.13h23.38z"      />      <path        fill={`url(#${gradientB})`}        d="M33.34 6.54a4.12 4.12 0 0 0-3.95 2.88L4.25 83.92a4.14 4.14 0 0 0 3.91 5.54h20.79a4.44 4.44 0 0 0 3.4-2.9l5.02-14.78 17.91 16.7a4.24 4.24 0 0 0 2.67.97h23.29L71.02 60.26H41.24L59.47 6.55z"      />      <path        fill={`url(#${gradientC})`}        d="M66.6 9.36a4.14 4.14 0 0 0-3.93-2.82H33.65a4.15 4.15 0 0 1 3.93 2.82l25.18 74.62a4.15 4.15 0 0 1-3.93 5.48h29.02a4.15 4.15 0 0 0 3.93-5.48z"      />    </svg>  );}; const BitbucketIcon: Icon = (props) => {  const id = useId().replaceAll(":", "");  const gradientId = `${id}-bitbucket-a`;   return (    <svg {...props} viewBox="8.4 14.39 2481.29 2231.21">      <path fill="none" d="M989.97,1493.09h518.05l125.04-730.04H852.22L989.97,1493.09z" />      <path        fill="#2684FF"        d="M88.92,14.4C45.02,13.83,8.97,48.96,8.41,92.86c-0.06,4.61,0.28,9.22,1.02,13.77l337.48,2048.72 c8.68,51.75,53.26,89.8,105.74,90.24h1619.03c39.38,0.5,73.19-27.9,79.49-66.78l337.49-2071.78c7.03-43.34-22.41-84.17-65.75-91.2 c-4.55-0.74-9.15-1.08-13.76-1.02L88.92,14.4z M1509.99,1495.09H993.24l-139.92-731h781.89L1509.99,1495.09z"      />      <linearGradient        id={gradientId}        gradientUnits="userSpaceOnUse"        x1="945.1094"        y1="1524.8389"        x2="944.4923"        y2="1524.1893"        gradientTransform="matrix(1996.6343 0 0 -1480.3047 -1884485.625 2258195)"      >        <stop offset="0.18" stopColor="#0052CC" />        <stop offset="1" stopColor="#2684FF" />      </linearGradient>      <path        fill={`url(#${gradientId})`}        d="M2379.27,763.06h-745.5l-125.12,730.42H992.31l-609.67,723.67c19.32,16.71,43.96,26,69.5,26.21h1618.13 c39.35,0.51,73.14-27.88,79.44-66.72L2379.27,763.06z"      />    </svg>  );}; const SOURCE_CONTROL_PROVIDER_ICONS: Partial<Record<SourceControlProviderKind, Icon>> = {  github: GitHubIcon,  gitlab: GitLabIcon,  "azure-devops": AzureDevOpsIcon,  bitbucket: BitbucketIcon,}; 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 authPresentation(auth: SourceControlProviderAuth): {  readonly label: string;  readonly badge: "warning" | null;} {  if (auth.status === "authenticated") {    return { label: "Signed in", badge: null };  }  if (auth.status === "unauthenticated") {    return { label: "Sign in", badge: "warning" };  }  return { label: "Sign in", 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 (!item.implemented) 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] : null;   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 (!item.implemented) {    return <span>Support for {item.label} is coming soon.</span>;  }   if (item.status !== "available") {    return <span>Not found - {item.installHint}</span>;  }   if (auth) {    if (auth.status === "authenticated") {      return (        <>          <span>Authenticated</span>          {authAccount ? (            <>              <span aria-hidden>as</span>              <RedactedAccount account={authAccount} />            </>          ) : null}        </>      );    }    if (auth.status === "unauthenticated") {      return <span>Sign in with the {item.executable} CLI to enable pull request actions.</span>;    }    return (      <span>        Install and sign in with the {item.executable} CLI to enable pull request actions.      </span>    );  }   return <span>Available</span>;} function DiscoveryItemRow({  item,}: {  readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem;}) {  const version = optionLabel(item.version);  const enabled = item.implemented && item.status === "available";  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",        !item.implemented && "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}              {!item.implemented ? (                <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">            {item.implemented ? (              <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}>      {Array.from({ length: 2 }, (_, index) => (        <div key={index} 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">                <Skeleton className="size-2 rounded-full" />                <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="Detected tools">      <Empty className="min-h-88">        <EmptyMedia variant="icon">          <GitPullRequestIcon />        </EmptyMedia>        <EmptyHeader>          <EmptyTitle>            {hasError ? "Could not scan source control" : "No source control tools found"}          </EmptyTitle>          <EmptyDescription>            {hasError ? error : "Install a supported Git or pull request CLI, then scan again."}          </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="Scan source control tools"          >            <RefreshCwIcon className={cn("size-3", discovery.isPending && "animate-spin")} />          </Button>        }      />      <TooltipPopup side="top">Scan source control tools</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>  );}