'use client'
import type { Environment } from '@dify/contracts/enterprise/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import {
EnvVarBindingsPanel,
} from '@/features/deployments/components/env-var-bindings'
import {
RuntimeCredentialBindingsPanel,
} from '@/features/deployments/components/runtime-credential-bindings'
import { TitleTooltip } from '@/features/deployments/components/title-tooltip'
import { UnsupportedDslNodesAlert } from '@/features/deployments/components/unsupported-dsl-nodes-alert'
import {
canDeployAtom,
canSkipDeploymentAtom,
createDeploymentGuideSubmissionAtom,
CreateDeploymentGuideSubmissionBlockedError,
deployableEnvironmentsAtom,
deployableEnvironmentsQueryAtom,
deploymentOptionsQueryAtom,
deploymentTargetBindingSelectionsAtom,
deploymentTargetBindingSlotsAtom,
deploymentTargetEnvVarSlotsAtom,
effectiveSelectedEnvironmentIdAtom,
envVarValuesAtom,
isCreatingReleaseOnlyAtom,
isSubmittingDeploymentGuideAtom,
selectBindingAtom,
selectedEnvironmentIdAtom,
setEnvVarAtom,
stepAtom,
unsupportedDslNodesAtom,
} from '@/features/deployments/create-guide/state'
import { deploymentErrorMessage } from '@/features/deployments/shared/domain/error'
import { useRouter } from '@/next/navigation'
import { StepShell } from './layout'
const targetEnvironmentSkeletonKeys = ['first-target-environment', 'second-target-environment']
const targetBindingSkeletonKeys = ['first-target-binding', 'second-target-binding']
export function TargetStepContent() {
const { t } = useTranslation('deployments')
const unsupportedDslNodes = useAtomValue(unsupportedDslNodesAtom)
return (
)
}
function TargetEnvironmentSection() {
const { t } = useTranslation('deployments')
const environmentsQuery = useAtomValue(deployableEnvironmentsQueryAtom)
const environments = useAtomValue(deployableEnvironmentsAtom)
const effectiveSelectedEnvironmentId = useAtomValue(effectiveSelectedEnvironmentIdAtom)
const isEnvironmentError = environmentsQuery.isError
const isEnvironmentLoading = environmentsQuery.isLoading || (environmentsQuery.isFetching && !environmentsQuery.data)
const selectEnvironment = useSetAtom(selectedEnvironmentIdAtom)
const hasEnvironmentOptions = environments.length > 0
return (
{t('createGuide.target.environment')}
{hasEnvironmentOptions
? (
value={effectiveSelectedEnvironmentId}
onValueChange={selectEnvironment}
className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-2"
>
{environments.map(environment => (
))}
)
: isEnvironmentLoading
?
: (
{isEnvironmentError
? t('createGuide.target.loadEnvironmentsFailed')
: t('createGuide.target.noEnvironmentOptions')}
)}
)
}
function EnvironmentOptionRow({ environment }: {
environment: Environment
}) {
const { t } = useTranslation('deployments')
const summary = environment.description.trim() || `${t(`mode.${environment.mode}`)} ยท ${t(`backend.${environment.backend}`)}`
return (
value={environment.id}
variant="unstyled"
className={cn(
'group flex cursor-pointer items-center gap-3 rounded-xl border p-3 outline-hidden',
'border-components-option-card-option-border bg-components-option-card-option-bg hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'data-checked:border-state-accent-solid data-checked:bg-state-accent-hover data-checked:shadow-xs',
)}
>
{environment.displayName}
{summary}
)
}
function TargetBindingSection() {
const { t } = useTranslation('deployments')
const deploymentOptionsQuery = useAtomValue(deploymentOptionsQueryAtom)
const bindingSlots = useAtomValue(deploymentTargetBindingSlotsAtom)
const bindingSelections = useAtomValue(deploymentTargetBindingSelectionsAtom)
const isBindingError = deploymentOptionsQuery.isError
const isBindingLoading = deploymentOptionsQuery.isLoading || (deploymentOptionsQuery.isFetching && !deploymentOptionsQuery.data)
const selectBinding = useSetAtom(selectBindingAtom)
const unsupportedDslNodes = useAtomValue(unsupportedDslNodesAtom)
const shouldRender = !(isBindingError && unsupportedDslNodes.length > 0)
if (!shouldRender)
return null
if (isBindingLoading || isBindingError) {
return (
{t('createGuide.target.bindings')}
{t('createGuide.target.bindingHint')}
{isBindingLoading
?
: (
{t('createGuide.target.loadBindingsFailed')}
)}
)
}
return (
)
}
function TargetEnvVarSection() {
const { t } = useTranslation('deployments')
const setEnvVar = useSetAtom(setEnvVarAtom)
const envVarValues = useAtomValue(envVarValuesAtom)
const deploymentOptionsQuery = useAtomValue(deploymentOptionsQueryAtom)
const envVarSlots = useAtomValue(deploymentTargetEnvVarSlotsAtom)
const isBindingError = deploymentOptionsQuery.isError
const isBindingLoading = deploymentOptionsQuery.isLoading || (deploymentOptionsQuery.isFetching && !deploymentOptionsQuery.data)
if (isBindingLoading || isBindingError)
return null
return (
t('createGuide.target.envVarSource.ariaLabel', { key })}
envVarCountLabel={t('createGuide.target.envVarCount', { count: envVarSlots.length })}
onChange={setEnvVar}
listScrollable={false}
className="border-components-option-card-option-border bg-components-option-card-option-bg"
/>
)
}
function TargetEnvironmentSkeleton() {
return (
{targetEnvironmentSkeletonKeys.map(key => (
))}
)
}
function TargetBindingSkeleton() {
return (
{targetBindingSkeletonKeys.map(key => (
))}
)
}
export function TargetBackButton() {
const { t } = useTranslation('deployments')
const setStep = useSetAtom(stepAtom)
const isSubmitting = useAtomValue(isSubmittingDeploymentGuideAtom)
return (
)
}
export function TargetSkipDeploymentButton() {
const { t } = useTranslation('deployments')
const router = useRouter()
const canSkipDeployment = useAtomValue(canSkipDeploymentAtom)
const submitCreateDeploymentGuide = useSetAtom(createDeploymentGuideSubmissionAtom)
const isSubmitting = useAtomValue(isSubmittingDeploymentGuideAtom)
const isSkippingDeployment = useAtomValue(isCreatingReleaseOnlyAtom)
const label = isSkippingDeployment
? t('createGuide.actions.creating')
: t('createGuide.actions.skipDeploy')
async function handleSkipDeployment() {
if (!canSkipDeployment)
return
try {
const appInstanceId = await submitCreateDeploymentGuide({ deployToEnvironment: false })
if (appInstanceId)
router.push(`/deployments/${appInstanceId}/overview`)
}
catch (error) {
await showSubmissionError({
error,
fallbackMessage: t('createGuide.errors.createReleaseFailed'),
unsupportedDslModeMessage: t('createGuide.dsl.unsupportedMode'),
})
}
}
return (
)
}
export function TargetDeployButton() {
const { t } = useTranslation('deployments')
const router = useRouter()
const canDeploy = useAtomValue(canDeployAtom)
const submitCreateDeploymentGuide = useSetAtom(createDeploymentGuideSubmissionAtom)
const isSubmitting = useAtomValue(isSubmittingDeploymentGuideAtom)
const isSkippingDeployment = useAtomValue(isCreatingReleaseOnlyAtom)
const label = isSubmitting && !isSkippingDeployment
? t('createGuide.actions.deploying')
: t('createGuide.actions.createAndDeploy')
async function handleDeploy() {
if (!canDeploy)
return
try {
const appInstanceId = await submitCreateDeploymentGuide({ deployToEnvironment: true })
if (appInstanceId)
router.push(`/deployments/${appInstanceId}/overview`)
}
catch (error) {
await showSubmissionError({
error,
fallbackMessage: t('createGuide.errors.deployFailed'),
unsupportedDslModeMessage: t('createGuide.dsl.unsupportedMode'),
})
}
}
return (
)
}
async function showSubmissionError({
error,
fallbackMessage,
unsupportedDslModeMessage,
}: {
error: unknown
fallbackMessage: string
unsupportedDslModeMessage: string
}) {
if (error instanceof CreateDeploymentGuideSubmissionBlockedError) {
toast.error(error.reason === 'unsupportedDslMode' ? unsupportedDslModeMessage : fallbackMessage)
return
}
toast.error(await deploymentErrorMessage(error) || fallbackMessage)
}