dify/web/features/deployments/deploy-drawer/ui/form.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

286 lines
9.0 KiB
TypeScript

'use client'
import type {
Environment,
EnvironmentDeployment,
Release,
} from '@dify/contracts/enterprise/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue, useSetAtom } from 'jotai'
import { ScopeProvider } from 'jotai-scope'
import { useTranslation } from 'react-i18next'
import { EnvVarBindingsPanel } from '../../shared/components/env-var-bindings'
import { isAvailableDeploymentTarget } from '../../shared/domain/runtime-status'
import {
canAttemptDeployAtom,
canSubmitDeployAtom,
closeDeployDrawerAtom,
deployBindingSlotsAtom,
deployEnvVarSlotsAtom,
deployEnvVarValuesAtom,
deployFormAppInstanceIdAtom,
deployHasBindingOptionsErrorAtom,
deployHasSelectedEnvironmentAtom,
deployIsBindingOptionsLoadingAtom,
deployReadyFormConfigAtom,
deployReadyFormLocalAtoms,
deployReleaseSubmissionAtom,
deploySelectedBindingsAtom,
deployShowValidationErrorsAtom,
deployTargetReleaseIdAtom,
isDeployReleaseSubmittingAtom,
releaseDeploymentViewAtom,
releaseDeploymentViewIsErrorAtom,
releaseDeploymentViewIsLoadingAtom,
selectDeployBindingAtom,
setDeployEnvVarAtom,
showDeployValidationErrorsAtom,
} from '../state'
import { currentReleaseIdForEnvironment, selectableDeployReleases } from '../state/release-options'
import {
BindingOptionsPanel,
DEPLOY_DRAWER_BINDING_LIST_CLASS_NAME,
DeployFormHeader,
EnvironmentField,
ReleaseField,
} from './form-sections'
import { DeployFormSkeleton } from './form-skeleton'
type DeployFormProps = {
appInstanceId: string
lockedEnvId?: string
presetReleaseId?: string
}
type DeployReadyFormProps = DeployFormProps & {
environments: Environment[]
releases: Release[]
runtimeRows: EnvironmentDeployment[]
defaultReleaseId?: string
releaseEmptyLabel?: string
}
function DeployRuntimeCredentialBindingsSection() {
const { t } = useTranslation('deployments')
const bindingSlots = useAtomValue(deployBindingSlotsAtom)
const selectedBindings = useAtomValue(deploySelectedBindingsAtom)
const isBindingOptionsLoading = useAtomValue(deployIsBindingOptionsLoadingAtom)
const hasBindingOptionsError = useAtomValue(deployHasBindingOptionsErrorAtom)
const showValidationErrors = useAtomValue(deployShowValidationErrorsAtom)
const selectBinding = useSetAtom(selectDeployBindingAtom)
return (
<BindingOptionsPanel
slots={bindingSlots}
selections={selectedBindings}
isLoading={isBindingOptionsLoading}
hasError={hasBindingOptionsError}
bindingCountLabel={t(($) => $['deployDrawer.bindingCount'], { count: bindingSlots.length })}
showMissingRequired={showValidationErrors}
onChange={selectBinding}
/>
)
}
function DeployEnvVarBindingsSection() {
const { t } = useTranslation('deployments')
const envVarSlots = useAtomValue(deployEnvVarSlotsAtom)
const envVarValues = useAtomValue(deployEnvVarValuesAtom)
const isBindingOptionsLoading = useAtomValue(deployIsBindingOptionsLoadingAtom)
const hasBindingOptionsError = useAtomValue(deployHasBindingOptionsErrorAtom)
const showValidationErrors = useAtomValue(deployShowValidationErrorsAtom)
const setDeployEnvVar = useSetAtom(setDeployEnvVarAtom)
if (isBindingOptionsLoading || hasBindingOptionsError) return null
return (
<EnvVarBindingsPanel
slots={envVarSlots}
values={envVarValues}
title={t(($) => $['deployDrawer.envVars'])}
hint={t(($) => $['deployDrawer.envVarHint'])}
envVarPlaceholder={t(($) => $['deployDrawer.envVarPlaceholder'])}
literalSourceLabel={t(($) => $['deployDrawer.envVarSource.literal'])}
defaultSourceLabel={t(($) => $['deployDrawer.envVarSource.default'])}
lastDeploymentSourceLabel={t(($) => $['deployDrawer.envVarSource.lastDeployment'])}
valueTypeLabels={{
string: t(($) => $['deployDrawer.envVarType.string']),
number: t(($) => $['deployDrawer.envVarType.number']),
secret: t(($) => $['deployDrawer.envVarType.secret']),
}}
sourceAriaLabel={(key) => t(($) => $['deployDrawer.envVarSource.ariaLabel'], { key })}
defaultSourcePriority="lastDeployment"
envVarCountLabel={t(($) => $['deployDrawer.envVarCount'], { count: envVarSlots.length })}
missingRequiredLabel={t(($) => $['deployDrawer.missingRequiredEnvVar'])}
listClassName={DEPLOY_DRAWER_BINDING_LIST_CLASS_NAME}
showMissingRequired={showValidationErrors}
onChange={setDeployEnvVar}
/>
)
}
function DeployBindingsSection() {
const targetReleaseId = useAtomValue(deployTargetReleaseIdAtom)
const hasSelectedEnvironment = useAtomValue(deployHasSelectedEnvironmentAtom)
if (!targetReleaseId || !hasSelectedEnvironment) return null
return (
<>
<DeployRuntimeCredentialBindingsSection />
<DeployEnvVarBindingsSection />
</>
)
}
function DeployFormBody() {
return (
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
<div className="flex flex-col gap-5">
<ReleaseField />
<EnvironmentField />
<DeployBindingsSection />
</div>
</div>
)
}
function DeployFooter() {
const { t } = useTranslation('deployments')
const closeDeployDrawer = useSetAtom(closeDeployDrawerAtom)
const showValidationErrors = useSetAtom(showDeployValidationErrorsAtom)
const submitDeployRelease = useSetAtom(deployReleaseSubmissionAtom)
const canAttemptDeploy = useAtomValue(canAttemptDeployAtom)
const canDeploy = useAtomValue(canSubmitDeployAtom)
const isSubmitting = useAtomValue(isDeployReleaseSubmittingAtom)
const submitLabel = isSubmitting
? t(($) => $['deployDrawer.deploying'])
: t(($) => $['deployDrawer.deploy'])
function handleDeploy() {
showValidationErrors()
if (!canDeploy) return
submitDeployRelease({
deployFailedMessage: t(($) => $['deployDrawer.deployFailed']),
})
}
return (
<div className="flex shrink-0 justify-end gap-2 border-t border-divider-subtle bg-background-default-subtle px-6 py-4">
<Button type="button" variant="secondary" onClick={closeDeployDrawer}>
{t(($) => $['deployDrawer.cancel'])}
</Button>
<Button variant="primary" disabled={!canAttemptDeploy} onClick={handleDeploy}>
{submitLabel}
</Button>
</div>
)
}
function deployReadyFormStoreKey({
appInstanceId,
environments,
releases,
runtimeRows,
defaultReleaseId,
lockedEnvId,
presetReleaseId,
}: DeployReadyFormProps) {
return [
appInstanceId,
lockedEnvId ?? 'any',
presetReleaseId ?? 'new',
defaultReleaseId ?? 'none',
environments.map((env) => env.id).join(','),
releases.map((release) => release.id).join(','),
runtimeRows.map((row) => `${row.environment.id}:${row.currentRelease?.id ?? 'none'}`).join(','),
].join('|')
}
function DeployReadyForm(config: DeployReadyFormProps) {
return (
<ScopeProvider
atoms={[[deployReadyFormConfigAtom, config], ...deployReadyFormLocalAtoms]}
name="DeployReadyForm"
>
<div className="flex min-h-0 flex-1 flex-col">
<DeployFormHeader />
<DeployFormBody />
<DeployFooter />
</div>
</ScopeProvider>
)
}
function DeployFormContent({ appInstanceId, lockedEnvId, presetReleaseId }: DeployFormProps) {
const { t } = useTranslation('deployments')
const deploymentView = useAtomValue(releaseDeploymentViewAtom)
const isLoading = useAtomValue(releaseDeploymentViewIsLoadingAtom)
const isError = useAtomValue(releaseDeploymentViewIsErrorAtom)
if (isLoading) {
return <DeployFormSkeleton />
}
if (isError) {
return (
<div className="p-4 system-sm-regular text-text-destructive">
{t(($) => $['common.loadFailed'])}
</div>
)
}
if (!deploymentView) {
return (
<div className="p-4 system-sm-regular text-text-destructive">
{t(($) => $['common.loadFailed'])}
</div>
)
}
const runtimeRows = deploymentView.environmentDeployments
const environments = runtimeRows
.filter((row) => lockedEnvId || isAvailableDeploymentTarget(row))
.map((row) => row.environment)
const releaseRows = deploymentView.releases
const currentReleaseId = currentReleaseIdForEnvironment(runtimeRows, lockedEnvId)
const releases = selectableDeployReleases({
releases: releaseRows,
lockedEnvId,
currentReleaseId,
presetReleaseId,
})
const defaultReleaseId = releases[0]?.id
const releaseEmptyLabel =
lockedEnvId && !presetReleaseId && currentReleaseId
? t(($) => $['deployDrawer.noOtherReleaseAvailable'])
: undefined
const readyFormConfig = {
appInstanceId,
environments,
releases,
runtimeRows,
defaultReleaseId,
releaseEmptyLabel,
lockedEnvId,
presetReleaseId,
}
const formKey = deployReadyFormStoreKey(readyFormConfig)
return <DeployReadyForm key={formKey} {...readyFormConfig} />
}
export function DeployForm(props: DeployFormProps) {
return (
<ScopeProvider
key={props.appInstanceId}
atoms={[[deployFormAppInstanceIdAtom, props.appInstanceId]]}
name="DeployForm"
>
<DeployFormContent {...props} />
</ScopeProvider>
)
}