'use client' import type { DeploymentBindingOptionSlot, DeploymentEnvironmentOption, DeploymentRuntimeBinding, ReleaseRow, RuntimeInstanceRow } from '@dify/contracts/enterprise/types.gen' import { Button } from '@langgenius/dify-ui/button' import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { skipToken, useMutation, useQuery } from '@tanstack/react-query' import { useSetAtom } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton' import { consoleQuery } from '@/service/client' import { DEPLOYMENT_PAGE_SIZE } from '../../data' import { environmentId, environmentMode, environmentName } from '../../environment' import { releaseCommit, releaseLabel } from '../../release' import { releaseDeploymentAction } from '../../release-action' import { isUndeployedDeploymentRow } from '../../runtime-status' import { closeDeployDrawerAtom } from '../../store' import { DeploymentSelect, EnvironmentRow, Field, } from './select' type DeployFormProps = { appInstanceId: string lockedEnvId?: string presetReleaseId?: string } type DeployReadyFormProps = DeployFormProps & { environments: EnvironmentOption[] releases: ReleaseRow[] defaultReleaseId?: string runtimeRows: RuntimeInstanceRow[] } type EnvironmentOption = DeploymentEnvironmentOption & { disabled?: boolean } const DEPLOY_FORM_FIELD_SKELETON_KEYS = ['environment', 'release'] type BindingSelections = Record type BindingSelectOption = { value: string label: string } type BindingOptionsPanelProps = { slots: DeploymentBindingOptionSlot[] selections: BindingSelections isLoading: boolean hasError: boolean onChange: (slot: string, value: string) => void } function isEnvBindingSlot(slot: DeploymentBindingOptionSlot) { return (slot.kind?.toLowerCase() ?? '').includes('env') } function bindingSlotKey(slot: DeploymentBindingOptionSlot) { return slot.slot ?? '' } function bindingCandidateOptions(slot: DeploymentBindingOptionSlot): BindingSelectOption[] { if (isEnvBindingSlot(slot)) { return (slot.envVarCandidates ?? []) .filter(candidate => candidate.envVarId) .map(candidate => ({ value: candidate.envVarId!, label: [ candidate.name, candidate.displayValue, ].filter(Boolean).join(' · ') || candidate.envVarId!, })) } return (slot.candidates ?? []) .filter(candidate => candidate.credentialId) .map(candidate => ({ value: candidate.credentialId!, label: [ candidate.displayName, candidate.pluginName || candidate.pluginId, candidate.pluginVersion, ].filter(Boolean).join(' · ') || candidate.credentialId!, })) } function hasMissingRequiredBinding(slot: DeploymentBindingOptionSlot, selectedValue?: string) { return Boolean(slot.required && !selectedValue) } function selectedDeploymentBindings(slots: DeploymentBindingOptionSlot[], selections: BindingSelections): DeploymentRuntimeBinding[] { return slots .map((slot): DeploymentRuntimeBinding | undefined => { const slotKey = bindingSlotKey(slot) const selectedValue = selections[slotKey] if (!slotKey || !selectedValue) return undefined return isEnvBindingSlot(slot) ? { slot: slotKey, envVarId: selectedValue } : { slot: slotKey, credentialId: selectedValue } }) .filter((binding): binding is DeploymentRuntimeBinding => Boolean(binding)) } function selectedBindingSelections(slots: DeploymentBindingOptionSlot[], manualBindings: BindingSelections): BindingSelections { const next: BindingSelections = {} for (const slot of slots) { const slotKey = bindingSlotKey(slot) const candidates = bindingCandidateOptions(slot) const existing = manualBindings[slotKey] if (existing && candidates.some(candidate => candidate.value === existing)) next[slotKey] = existing else if (candidates.length === 1 && candidates[0]) next[slotKey] = candidates[0].value } return next } function BindingOptionsPanel({ slots, selections, isLoading, hasError, onChange, }: BindingOptionsPanelProps) { const { t } = useTranslation('deployments') if (isLoading) { return (
) } if (hasError) { return (
{t('deployDrawer.bindingOptionsFailed')}
) } return (
{t('deployDrawer.runtimeCredentials')}
{t('deployDrawer.bindingSelectionHint')}
{slots.length === 0 ? (
{t('deployDrawer.noBindingRequired')}
) : slots.map((slot) => { const slotKey = bindingSlotKey(slot) const candidates = bindingCandidateOptions(slot) const selectedValue = selections[slotKey] ?? '' const missing = hasMissingRequiredBinding(slot, selectedValue) return (
{slot.label || slotKey} {slot.required && ( {t('deployDrawer.requiredBinding')} )}
{slotKey}
{candidates.length === 0 ? (
{t('deployDrawer.noCredentialCandidates')}
) : ( onChange(slotKey, value)} options={candidates} placeholder={t('deployDrawer.selectCredential')} /> )}
{missing && (
{t('deployDrawer.missingRequiredBinding')}
)}
) })}
) } function DeployFormSkeleton() { return (
{DEPLOY_FORM_FIELD_SKELETON_KEYS.map(key => ( ))}
) } function DeployReadyForm({ appInstanceId, environments, releases, defaultReleaseId, lockedEnvId, presetReleaseId, runtimeRows, }: DeployReadyFormProps) { const { t } = useTranslation('deployments') const closeDeployDrawer = useSetAtom(closeDeployDrawerAtom) const startDeploy = useMutation(consoleQuery.enterprise.appDeploy.createDeployment.mutationOptions()) const presetRelease = presetReleaseId ? releases.find(r => r.id === presetReleaseId) : undefined const displayedRelease: ReleaseRow | undefined = presetRelease ?? (presetReleaseId ? { id: presetReleaseId } : undefined) const isExistingRelease = Boolean(presetReleaseId) const [selectedEnvId, setSelectedEnvId] = useState( () => lockedEnvId ?? environments.find(env => !env.disabled)?.id ?? environments[0]?.id ?? '', ) const selectedEnvironmentId = selectedEnvId || lockedEnvId || environments[0]?.id || '' const selectedEnvironment = environments.find(env => env.id === selectedEnvironmentId) const [selectedReleaseId, setSelectedReleaseId] = useState( () => displayedRelease?.id ?? defaultReleaseId ?? '', ) const selectedRelease = releases.find(release => release.id === selectedReleaseId) const targetReleaseId = displayedRelease?.id ?? selectedRelease?.id ?? selectedReleaseId const targetRelease = displayedRelease ?? selectedRelease ?? (targetReleaseId ? { id: targetReleaseId } : undefined) const deploymentRows = runtimeRows.filter(row => Boolean(row.environment?.id) && !isUndeployedDeploymentRow(row)) const selectedDeploymentRow = deploymentRows.find(row => environmentId(row.environment) === selectedEnvironmentId) const action = releaseDeploymentAction({ targetRelease, currentRelease: selectedDeploymentRow?.currentRelease, releaseRows: releases, isExistingRelease, }) const bindingOptions = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentBindingOptions.queryOptions({ input: appInstanceId && targetReleaseId ? { params: { appInstanceId }, query: { releaseId: targetReleaseId, }, } : skipToken, })) const bindingSlots = bindingOptions.data?.slots?.filter(slot => slot.slot) ?? [] const [manualBindings, setManualBindings] = useState({}) const selectedBindings = selectedBindingSelections(bindingSlots, manualBindings) const deploymentBindings = selectedDeploymentBindings(bindingSlots, selectedBindings) const bindingOptionsLoading = Boolean(targetReleaseId && (bindingOptions.isLoading || bindingOptions.isFetching)) const bindingOptionsReady = Boolean(targetReleaseId && bindingOptions.data && !bindingOptionsLoading && !bindingOptions.isError) const requiredBindingsReady = bindingSlots.every(slot => !hasMissingRequiredBinding(slot, selectedBindings[bindingSlotKey(slot)])) const isSubmitting = startDeploy.isPending const canDeploy = Boolean( selectedEnvironmentId && selectedEnvironment && !selectedEnvironment.disabled && targetReleaseId && bindingOptionsReady && requiredBindingsReady && !isSubmitting, ) const lockedEnv = lockedEnvId ? environments.find(e => e.id === lockedEnvId) : undefined const actionTitle = action === 'rollback' ? t('deployDrawer.rollbackTitle') : action === 'promote' ? t('deployDrawer.promoteTitle') : action === 'deployExistingRelease' ? t('deployDrawer.deployExistingReleaseTitle') : t('deployDrawer.title') const actionDescription = action === 'rollback' ? t('deployDrawer.rollbackDescription') : action === 'promote' ? t('deployDrawer.promoteDescription') : action === 'deployExistingRelease' ? t('deployDrawer.deployExistingReleaseDescription') : t('deployDrawer.description') const submitLabel = isSubmitting ? t('deployDrawer.deploying') : action === 'rollback' ? t('deployDrawer.rollback') : action === 'promote' ? t('deployDrawer.promote') : action === 'deployExistingRelease' ? t('deployDrawer.deployExistingRelease') : t('deployDrawer.deploy') const handleDeploy = () => { if (!canDeploy || !targetReleaseId) return startDeploy.mutate( { params: { appInstanceId, }, body: { environmentId: selectedEnvironmentId, releaseId: targetReleaseId, bindings: deploymentBindings, }, }, { onSuccess: () => { closeDeployDrawer() }, onError: () => { toast.error(t('deployDrawer.deployFailed')) }, }, ) } return (
{actionTitle} {actionDescription}
{isExistingRelease && displayedRelease ? (
{releaseLabel(displayedRelease)} · {releaseCommit(displayedRelease)}
{displayedRelease.createdAt}
{t('deployDrawer.existingReleaseHint')}
) : releases.length === 0 ? (
{t('deployDrawer.noReleaseAvailable')}
) : ( release.id).map(release => ({ value: release.id!, label: `${releaseLabel(release)} · ${releaseCommit(release)}`, }))} placeholder={t('deployDrawer.selectRelease')} /> )}
{lockedEnv ? : ( env.id).map(env => ({ value: env.id!, label: `${environmentName(env)} · ${t(environmentMode(env) === 'isolated' ? 'mode.isolated' : 'mode.shared')} · ${(env.type ?? 'env').toUpperCase()}`, disabled: env.disabled, disabledReason: env.disabledReason, }))} placeholder={t('deployDrawer.selectEnv')} /> )} {targetReleaseId && ( setManualBindings(prev => ({ ...prev, [slot]: value }))} /> )}
) } export function DeployForm({ appInstanceId, lockedEnvId, presetReleaseId, }: DeployFormProps) { const { t } = useTranslation('deployments') const releaseHistoryQuery = useQuery(consoleQuery.enterprise.appDeploy.listReleases.queryOptions({ input: { params: { appInstanceId }, query: { pageNumber: 1, resultsPerPage: DEPLOYMENT_PAGE_SIZE, }, }, })) const environmentOptionsQuery = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions()) const runtimeInstancesQuery = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({ input: { params: { appInstanceId }, }, })) if (releaseHistoryQuery.isLoading || environmentOptionsQuery.isLoading || runtimeInstancesQuery.isLoading) { return } if (releaseHistoryQuery.isError || environmentOptionsQuery.isError || runtimeInstancesQuery.isError) { return (
{t('common.loadFailed')}
) } const environments = environmentOptionsQuery.data?.environments ?.filter(environment => environment.id) .map(environment => ({ ...environment, disabled: environment.deployable === false, })) ?? [] const releases = releaseHistoryQuery.data?.data?.filter(release => release.id) ?? [] const defaultReleaseId = releases[0]?.id const runtimeRows = runtimeInstancesQuery.data?.data ?? [] const formKey = `${appInstanceId}-${lockedEnvId ?? 'any'}-${presetReleaseId ?? 'new'}-${defaultReleaseId ?? 'none'}` return ( ) }