import React, { useCallback, useMemo, type ReactNode } from "react"; import { Pressable, Text, View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { CircleCheck, ExternalLink } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import type { CheckoutPipelineJob, CheckoutPipelineStage } from "@getpaseo/protocol/messages"; import { GitLabIcon } from "@/components/icons/gitlab-icon"; import { definePaneContribution, type ClientForgeViewModule, type PaneChecksSlotContext, } from "@/git/client-forge-module"; import { openExternalUrl } from "@/utils/open-external-url"; import { formatDuration } from "@/utils/time"; import { CheckStatusIcon, Section, SUMMARY_DANGER_ICON, SUMMARY_SUCCESS_ICON, SUMMARY_WARNING_ICON, SummaryPill, foregroundMutedColorMapping, sectionKitStyles, successColorMapping, } from "@/git/pull-request-panel/section-kit"; import { useGitLabPipeline } from "@/git/pull-request-panel/use-pipeline"; import { deriveGitlabApprovals, deriveGitlabPipelineSummary, GitlabMergeFactsSchema, isPipelineActiveStatus, mapPipelineStatus, type GitlabApprovals, type GitlabMergeFacts, type GitlabPipelineSummary, } from "./gitlab"; function renderGitlabHeaderMeta(facts: GitlabMergeFacts): ReactNode { const approvals = deriveGitlabApprovals(facts); if (!approvals) { return null; } return ; } function renderGitlabChecksSection(facts: GitlabMergeFacts, ctx: PaneChecksSlotContext): ReactNode { if (!ctx.enabled) { return null; } const summary = deriveGitlabPipelineSummary(facts); if (!summary) { return null; } return ( ); } const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedExternalLink = withUnistyles(ExternalLink); function GitlabApprovalsBadge({ approvals }: { approvals: GitlabApprovals }) { const { t } = useTranslation(); return ( = approvals.required ? successColorMapping : foregroundMutedColorMapping } /> {t("workspace.git.pr.approvals", { given: approvals.given, required: approvals.required })} ); } function rowPressableStyle({ hovered }: { hovered?: boolean }) { return [sectionKitStyles.checkRow, Boolean(hovered) && styles.hoverable]; } function jobRowPressableStyle({ hovered }: { hovered?: boolean }) { return [styles.pipelineJobRow, Boolean(hovered) && styles.hoverable]; } function formatPipelineDuration(seconds: number | null): string { if (seconds === null || seconds <= 0) { return ""; } return formatDuration(seconds * 1000); } interface PipelineJobCounts { passed: number; failed: number; pending: number; } function countPipelineJobs(jobs: CheckoutPipelineJob[]): PipelineJobCounts { const counts: PipelineJobCounts = { passed: 0, failed: 0, pending: 0 }; for (const job of jobs) { const status = mapPipelineStatus(job.status); if (status === "success") counts.passed += 1; else if (status === "failure") counts.failed += 1; else if (status === "pending") counts.pending += 1; } return counts; } function GitLabPipelineSection({ serverId, cwd, changeRequestNumber, summary, open, onToggle, canFetchCheckDetails, }: { serverId: string; cwd: string; changeRequestNumber: number; summary: GitlabPipelineSummary; open: boolean; onToggle: () => void; canFetchCheckDetails: boolean; }) { const { t } = useTranslation(); const { pipeline, isLoading, isPlaceholderData, error } = useGitLabPipeline({ serverId, cwd, pipelineId: summary.id, changeRequestNumber, enabled: open && canFetchCheckDetails, live: isPipelineActiveStatus(summary.rawStatus), }); const counts = useMemo( () => countPipelineJobs((pipeline?.stages ?? []).flatMap((stage) => stage.jobs)), [pipeline], ); const totalCounted = counts.passed + counts.failed + counts.pending; const showBreakdown = !isPlaceholderData && totalCounted > 0; const displayCounts = showBreakdown ? counts : { passed: 0, failed: 0, pending: 0 }; const handleOpenPipeline = useCallback(() => { if (summary.url) { void openExternalUrl(summary.url); } }, [summary.url]); const sectionSummary = ( <> {showBreakdown ? null : } ); return (
{`Pipeline #${summary.id}`} {summary.rawStatus ? ( {summary.rawStatus} ) : null} {summary.url ? ( ) : null} {isLoading ? ( {t("workspace.git.pr.empty.loadingPipeline")} ) : null} {!isLoading && pipeline && pipeline.stages.length === 0 ? ( {t("workspace.git.pr.empty.noJobs")} ) : null} {!isLoading && pipeline && pipeline.stages.length > 0 ? pipeline.stages.map((stage) => ) : null} {!isLoading && !pipeline && error ? ( {t("workspace.git.pr.empty.pipelineJobsLoadFailed")} ) : null}
); } function PipelineStageGroup({ stage }: { stage: CheckoutPipelineStage }) { return ( {stage.name} {stage.jobs.map((job) => ( ))} ); } function PipelineJobRow({ job }: { job: CheckoutPipelineJob }) { const { t } = useTranslation(); const handlePress = useCallback(() => { if (job.url) { void openExternalUrl(job.url); } }, [job.url]); const duration = formatPipelineDuration(job.durationSeconds); return ( {job.name} {job.allowFailure ? ( {t("workspace.git.pr.empty.allowedToFail")} ) : null} {duration ? {duration} : null} ); } export const gitlabForgeView = { id: "gitlab", icon: GitLabIcon, brandColor: { light: "#FC6D26", dark: "#FC6D26", }, paneContributions: [ definePaneContribution(GitlabMergeFactsSchema, { renderHeaderMeta: renderGitlabHeaderMeta, renderChecksSection: renderGitlabChecksSection, }), ], } satisfies ClientForgeViewModule; const styles = StyleSheet.create((theme) => ({ approvalsBadge: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, approvalsText: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, }, hoverable: { backgroundColor: theme.colors.surface1, }, pipelineStageHeader: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingTop: theme.spacing[2], paddingBottom: theme.spacing[1], }, pipelineStageName: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.medium, color: theme.colors.foregroundMuted, textTransform: "uppercase", letterSpacing: 0.5, flexShrink: 1, }, pipelineJobRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingVertical: theme.spacing[2], paddingRight: theme.spacing[3], paddingLeft: theme.spacing[6], minHeight: 32, }, }));