Julius Marmingefeat(scm): Gitlab (#2462)0ce7e566 months agoHistory
372 lines · 11.4 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,}; 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]    : 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 (!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">                <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="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>  );}