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> );}
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>
);
}