import { useState } from 'react'; import { AlertTriangle, Loader2 } from 'lucide-react'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { Stack, StackDeploymentStatus, StackStatus, StackType, } from '@/react/common/stacks/types'; import { Authorized } from '@/react/hooks/useUser'; import { InfoPanel } from '@/react/portainer/gitops/InfoPanel'; import { Alert } from '@@/Alert'; import { Icon } from '@@/Icon'; import { Button } from '@@/buttons'; import { FormSection } from '@@/form-components/FormSection'; import { useSwarmStackResources } from '../useSwarmStackServices'; import { useComposeStackContainers } from '../useComposeStackContainers'; import { StackDuplicationForm } from './StackDuplicationForm/StackDuplicationForm'; import { EditGitSettingsModal } from './EditGitSettings/EditGitSettingsModal'; import { StackActions } from './StackActions'; import { AssociateStackForm } from './AssociateStackForm'; interface StackInfoTabProps { stack?: Stack; // will be loaded only if regular or orphaned stackName: string; stackFileContent?: string; isRegular?: boolean; isExternal: boolean; isOrphaned: boolean; isOrphanedRunning: boolean; environmentId: number; yamlError?: string; } export function StackInfoTab({ stack, stackName, stackFileContent, isRegular, isExternal, isOrphaned, isOrphanedRunning, environmentId, yamlError, }: StackInfoTabProps) { const [isEditOpen, setIsEditOpen] = useState(false); const status = useStackStatus({ status: stack?.Status, environmentId, name: stackName, type: stack?.Type, }); return ( <> {stack && ( )}
{stackName} {stack && (
)}
{stack && ( <> {isOrphaned ? ( ) : ( <> {stack.GitConfig && !stack.FromAppTemplate && ( <> {isEditOpen && ( setIsEditOpen(false)} stack={stack} /> )} )} {isRegular && !!stackFileContent && ( )} )} )} ); } function DeploymentStatusSection({ status, deploymentStatus, }: { status: StackStatus; deploymentStatus?: StackDeploymentStatus[]; }) { if (status === StackStatus.Deploying) { return (

Deployment in progress...

); } if (status === StackStatus.Error) { const errorMessage = getLastDeploymentError(deploymentStatus); return (
{errorMessage || 'Deployment failed.'}
); } return null; } function getLastDeploymentError( deploymentStatus?: StackDeploymentStatus[] ): string | undefined { if (!deploymentStatus?.length) return undefined; const last = deploymentStatus[deploymentStatus.length - 1]; return last.Status === StackStatus.Error ? last.Message : undefined; } function ExternalOrphanedWarning({ isExternal, isOrphaned, }: { isExternal: boolean; isOrphaned: boolean; }) { if (!isExternal && !isOrphaned) return null; return (

{isExternal && ( This stack was created outside of Portainer. Control over this stack is limited. )} {isOrphaned && ( This stack is orphaned. You can re-associate it with the current environment using the "Associate to this environment" feature. )}

); } function useStackStatus({ status, name, type, environmentId, }: { status: Stack['Status'] | undefined; name: string; type: Stack['Type'] | undefined; environmentId: EnvironmentId; }) { const servicesQuery = useSwarmStackResources(name, { enabled: type === StackType.DockerSwarm && !status, }); const containersQuery = useComposeStackContainers( { environmentId, stackName: name }, { enabled: type === StackType.DockerCompose && !status, } ); const derivedSwarmStatus = servicesQuery.data?.length ? StackStatus.Active : StackStatus.Inactive; const derivedComposeStatus = containersQuery.data?.length ? StackStatus.Active : StackStatus.Inactive; const derivedStatus = type === StackType.DockerSwarm ? derivedSwarmStatus : derivedComposeStatus; return status || derivedStatus; }