import { ReactNode } from 'react'; import { CheckCircle, GitMerge, Loader2, XCircle } from 'lucide-react'; import { truncateLeftRight } from '@/portainer/filters/filters'; import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper'; import { GitCommitLink } from '@/react/portainer/gitops/GitCommitLink'; import { useGitRefs } from '@/react/portainer/gitops/queries/useGitRefs'; import { useSearch } from '@/react/portainer/gitops/queries/useSearch'; import { isGitConfigDiverged } from '@/react/portainer/gitops/utils'; import { AutomationTestingProps } from '@/types'; import { AutoUpdateResponse, RepoConfigResponse, } from '@/react/portainer/gitops/types'; import { StackDeploymentInfo } from '@/react/common/stacks/types'; import { useSource } from '@/react/portainer/gitops/sources/queries/useSource'; import { CopyButton } from '@@/buttons'; import { Card } from '@@/primitives/Card'; import { Icon } from '@@/Icon'; import { Alert } from '@@/Alert'; import { Link } from '@@/Link'; import { getGitValidityError } from './hooks/useGitRepoValidity'; export function GitReferenceCard({ stackType, gitConfig, autoUpdate, currentDeploymentInfo, sourceId, }: { stackType: 'docker' | 'helm' | 'edge' | 'edge-helm' | 'kubernetes'; gitConfig: RepoConfigResponse; autoUpdate?: AutoUpdateResponse | null; currentDeploymentInfo?: StackDeploymentInfo | null; sourceId: number; }) { const hasDivergence = isGitConfigDiverged(gitConfig, currentDeploymentInfo); const deployed = hasDivergence ? currentDeploymentInfo : undefined; const url = deployed?.RepositoryURL ?? gitConfig.URL; const configFilePath = deployed?.ConfigFilePath ?? gitConfig.ConfigFilePath; const reference = deployed?.ReferenceName ?? gitConfig.ReferenceName; const commitId = deployed?.ConfigHash ?? gitConfig.ConfigHash; const sourceIdToShow = deployed?.SourceID ?? sourceId; const refCheckQuery = useGitRefs( { sourceId: sourceIdToShow, }, { enabled: !!sourceIdToShow, suppressError: true } ); const repoError = getGitValidityError( refCheckQuery.error, !!gitConfig.Authentication ); const hasRepoError = refCheckQuery.isError; const foundRef = !!reference && !!refCheckQuery.data && refCheckQuery.data.includes(reference); const hasRefError = !reference || (refCheckQuery.isSuccess && !foundRef); // check file const enableFileCheck = stackType !== 'helm' && stackType !== 'edge-helm'; const fileCheckQuery = useSearch( { keyword: configFilePath || '', reference, sourceId: sourceIdToShow, }, enableFileCheck && !!sourceIdToShow && !!reference && !!configFilePath && !hasRepoError && !hasRefError ); const foundFile = !!configFilePath && !!fileCheckQuery.data && fileCheckQuery.data.includes(configFilePath); const hasFileError = enableFileCheck && (!configFilePath || fileCheckQuery.isError || (fileCheckQuery.isFetched && !foundFile)); const { Interval: autoUpdateInterval, Webhook: webhook } = autoUpdate || {}; const webhookUrl = webhook ? `${baseStackWebhookUrl()}/${webhook}` : ''; const isRefLoading = refCheckQuery.isFetching; const isFileLoading = enableFileCheck && fileCheckQuery.isFetching; const hasError = hasRepoError || hasRefError || hasFileError; const explainedError = getExplainedError( refCheckQuery.error, fileCheckQuery.error ); const infoMessage = explainedError || repoError; return (
Managed by Git
{hasError && ( <>
{hasRepoError && (
The git repository {url || ''} could not be reached.
)} {hasRefError && (
The git reference {reference || ''} could not be found on the remote repository.
)} {hasFileError && (
The referenced file{' '} {configFilePath || ''} could not be found on the remote repository.
)}
{!!infoMessage && ( {infoMessage} )} )}
{enableFileCheck && ( )} {!!sourceIdToShow && } {!!commitId && ( } title={commitId} data-cy="git-commit" /> )} {!!autoUpdateInterval && ( )} {!!webhook && ( {truncateLeftRight(webhookUrl, 0, 10, 25)} Copy link } title="webhook" data-cy="git-webhook" /> )}
{currentDeploymentInfo && hasDivergence && ( )}
); } function SourceLineItem({ sourceId }: { sourceId: number }) { const sourceQuery = useSource(sourceId); const sourceName = sourceQuery.data?.name; return ( {sourceName} ) : sourceQuery.isLoading ? ( '' ) : ( 'not found' ) } title={sourceName ?? ''} isLoading={sourceQuery.isLoading} isError={sourceQuery.isError || (!sourceQuery.isLoading && !sourceName)} isValid={!!sourceName} data-cy="git-source" /> ); } function DivergenceAlert({ gitConfig, currentDeploymentInfo, }: { gitConfig: RepoConfigResponse; currentDeploymentInfo: StackDeploymentInfo; }) { const urlChanged = typeof currentDeploymentInfo.RepositoryURL !== 'undefined' && currentDeploymentInfo.RepositoryURL !== gitConfig.URL; const refChanged = typeof currentDeploymentInfo.ReferenceName !== 'undefined' && currentDeploymentInfo.ReferenceName !== gitConfig.ReferenceName; return (
Settings changed since last deploy
Next: {urlChanged && {gitConfig.URL ?? ''}}{' '} {refChanged && {gitConfig.ReferenceName ?? ''}} Config:{' '} {gitConfig.ConfigFilePath ?? ''}
); } function getExplainedError(...errors: Array) { if ( errors.some( (e) => e instanceof Error && e?.message.startsWith( 'Authentication required: Invalid username or token' ) ) ) { return 'The configured Git Credentials are invalid or expired. Update the credentials to restore access.'; } return ''; } function LineItem({ label, isLoading, isValid, isError, value, title, 'data-cy': dataCy, }: { label: string; value: ReactNode; title: string; isLoading?: boolean; isValid?: boolean; isError?: boolean; } & AutomationTestingProps) { return (
{label}: {value}
); } function StateIcon({ isLoading, isValid, isError, }: { isLoading?: boolean; isValid?: boolean; isError?: boolean; }) { if (isLoading) return ; if (isError) return ; if (isValid) return ; return null; }