Refactor shared frontend components

This commit is contained in:
Stephen Zhou
2026-06-12 10:39:20 +08:00
parent c88f2a6889
commit 564534c1f3
21 changed files with 348 additions and 378 deletions

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import { CreateDeploymentGuideProvider } from './state/provider'
import { CreateDeploymentGuideShell } from './ui/shell/guide-shell'
import { CreateDeploymentGuideShell } from './ui/shell'
export function CreateDeploymentGuide() {
const { t } = useTranslation('deployments')

View File

@ -1,28 +0,0 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import {
continueFromReleaseAtom,
releaseCanGoNextAtom,
stepAtom,
} from '@/features/deployments/create-guide/state'
export function ReleaseActionButtons() {
const { t } = useTranslation('deployments')
const canGoNext = useAtomValue(releaseCanGoNextAtom)
const setStep = useSetAtom(stepAtom)
const continueFromRelease = useSetAtom(continueFromReleaseAtom)
return (
<>
<Button type="button" variant="secondary" onClick={() => setStep('source')}>
{t('createGuide.actions.back')}
</Button>
<Button type="button" variant="primary" disabled={!canGoNext} onClick={continueFromRelease}>
{t('createGuide.actions.next')}
</Button>
</>
)
}

View File

@ -1,7 +1,14 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { StepShell } from '../shell/layout'
import {
continueFromReleaseAtom,
releaseCanGoNextAtom,
stepAtom,
} from '@/features/deployments/create-guide/state'
import { StepShell } from '../layout'
import {
DeploymentInfoSection,
InitialReleaseSection,
@ -23,3 +30,21 @@ export function ReleaseStepContent() {
</StepShell>
)
}
export function ReleaseActionButtons() {
const { t } = useTranslation('deployments')
const canGoNext = useAtomValue(releaseCanGoNextAtom)
const setStep = useSetAtom(stepAtom)
const continueFromRelease = useSetAtom(continueFromReleaseAtom)
return (
<>
<Button type="button" variant="secondary" onClick={() => setStep('source')}>
{t('createGuide.actions.back')}
</Button>
<Button type="button" variant="primary" disabled={!canGoNext} onClick={continueFromRelease}>
{t('createGuide.actions.next')}
</Button>
</>
)
}

View File

@ -0,0 +1,78 @@
'use client'
import { useAtomValue } from 'jotai'
import { stepAtom } from '@/features/deployments/create-guide/state'
import { GuideCard, GuideFrame } from './layout'
import {
ReleaseActionButtons,
ReleaseStepContent,
} from './release-step/release-step-content'
import {
SourceActionButtons,
SourceStepContent,
} from './source-step/source-step-content'
import {
TargetBackButton,
TargetDeployButton,
TargetSkipDeploymentButton,
TargetStepContent,
} from './target-step'
export function CreateDeploymentGuideShell() {
const step = useAtomValue(stepAtom)
return (
<GuideFrame activeStep={step}>
<GuideCard
contentScrollable={step !== 'source'}
actions={<CreateDeploymentGuideActionBar />}
>
<CreateDeploymentGuideStepContent />
</GuideCard>
</GuideFrame>
)
}
function CreateDeploymentGuideStepContent() {
const step = useAtomValue(stepAtom)
if (step === 'source') {
return (
<div className="flex h-full min-h-0 flex-col gap-7 pb-4">
<SourceStepContent />
</div>
)
}
if (step === 'release') {
return (
<div className="flex h-full min-h-0 flex-col gap-7 pb-4">
<ReleaseStepContent />
</div>
)
}
return (
<div className="flex flex-col gap-7 pb-4">
<TargetStepContent />
</div>
)
}
function CreateDeploymentGuideActionBar() {
const step = useAtomValue(stepAtom)
return (
<div className="sticky bottom-0 z-10 -mx-5 mt-auto flex items-center justify-end gap-2 border-t border-divider-subtle bg-background-default-subtle/95 px-5 py-4 backdrop-blur-sm sm:-mx-8 sm:px-8 lg:-mx-10 lg:px-10">
{step === 'source' && <SourceActionButtons />}
{step === 'release' && <ReleaseActionButtons />}
{step === 'target' && (
<>
<TargetBackButton />
<TargetSkipDeploymentButton />
<TargetDeployButton />
</>
)}
</div>
)
}

View File

@ -1,29 +0,0 @@
'use client'
import { useAtomValue } from 'jotai'
import { stepAtom } from '@/features/deployments/create-guide/state'
import { ReleaseActionButtons } from '../release-step/actions'
import { SourceActionButtons } from '../source-step/actions'
import {
TargetBackButton,
TargetDeployButton,
TargetSkipDeploymentButton,
} from '../target-step/actions'
export function CreateDeploymentGuideActionBar() {
const step = useAtomValue(stepAtom)
return (
<div className="sticky bottom-0 z-10 -mx-5 mt-auto flex items-center justify-end gap-2 border-t border-divider-subtle bg-background-default-subtle/95 px-5 py-4 backdrop-blur-sm sm:-mx-8 sm:px-8 lg:-mx-10 lg:px-10">
{step === 'source' && <SourceActionButtons />}
{step === 'release' && <ReleaseActionButtons />}
{step === 'target' && (
<>
<TargetBackButton />
<TargetSkipDeploymentButton />
<TargetDeployButton />
</>
)}
</div>
)
}

View File

@ -1,22 +0,0 @@
'use client'
import { useAtomValue } from 'jotai'
import { stepAtom } from '@/features/deployments/create-guide/state'
import { CreateDeploymentGuideActionBar } from './action-bar'
import { GuideCard, GuideFrame } from './layout'
import { CreateDeploymentGuideStepContent } from './step-content'
export function CreateDeploymentGuideShell() {
const step = useAtomValue(stepAtom)
return (
<GuideFrame activeStep={step}>
<GuideCard
contentScrollable={step !== 'source'}
actions={<CreateDeploymentGuideActionBar />}
>
<CreateDeploymentGuideStepContent />
</GuideCard>
</GuideFrame>
)
}

View File

@ -1,33 +0,0 @@
'use client'
import { useAtomValue } from 'jotai'
import { stepAtom } from '@/features/deployments/create-guide/state'
import { ReleaseStepContent } from '../release-step/release-step-content'
import { SourceStepContent } from '../source-step/source-step-content'
import { TargetStepContent } from '../target-step'
export function CreateDeploymentGuideStepContent() {
const step = useAtomValue(stepAtom)
if (step === 'source') {
return (
<div className="flex h-full min-h-0 flex-col gap-7 pb-4">
<SourceStepContent />
</div>
)
}
if (step === 'release') {
return (
<div className="flex h-full min-h-0 flex-col gap-7 pb-4">
<ReleaseStepContent />
</div>
)
}
return (
<div className="flex flex-col gap-7 pb-4">
<TargetStepContent />
</div>
)
}

View File

@ -1,29 +0,0 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import {
continueFromSourceAtom,
sourceCanGoNextAtom,
} from '@/features/deployments/create-guide/state'
export function SourceActionButtons() {
const { t } = useTranslation('deployments')
const canGoNext = useAtomValue(sourceCanGoNextAtom)
const continueFromSource = useSetAtom(continueFromSourceAtom)
return (
<Button
type="button"
variant="primary"
disabled={!canGoNext}
onClick={() => continueFromSource({
defaultDslAppName: t('createGuide.dsl.defaultAppName'),
defaultReleaseName: t('createGuide.release.defaultName'),
})}
>
{t('createGuide.actions.next')}
</Button>
)
}

View File

@ -3,9 +3,14 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
import { dslFileAtom, selectDslFileAtom } from '@/features/deployments/create-guide/state'
import { StepShell } from '../../shell/layout'
import { DslReadStatus } from './read-status'
import {
dslFileAtom,
dslReadErrorAtom,
dslUnsupportedModeAtom,
isReadingDslAtom,
selectDslFileAtom,
} from '@/features/deployments/create-guide/state'
import { StepShell } from '../layout'
export function DslUploadSection() {
const { t } = useTranslation('deployments')
@ -32,3 +37,30 @@ export function DslUploadSection() {
</StepShell>
)
}
function DslReadStatus() {
const { t } = useTranslation('deployments')
const isReadingDsl = useAtomValue(isReadingDslAtom)
const dslReadError = useAtomValue(dslReadErrorAtom)
const dslUnsupportedMode = useAtomValue(dslUnsupportedModeAtom)
return (
<>
{isReadingDsl && (
<div className="system-xs-regular text-text-tertiary">
{t('createGuide.dsl.reading')}
</div>
)}
{dslReadError && (
<div className="system-xs-regular text-text-destructive">
{t('createGuide.dsl.readFailed')}
</div>
)}
{dslUnsupportedMode && (
<div role="alert" className="system-xs-regular text-text-destructive">
{t('createGuide.dsl.unsupportedMode')}
</div>
)}
</>
)
}

View File

@ -1,36 +0,0 @@
'use client'
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import {
dslReadErrorAtom,
dslUnsupportedModeAtom,
isReadingDslAtom,
} from '@/features/deployments/create-guide/state'
export function DslReadStatus() {
const { t } = useTranslation('deployments')
const isReadingDsl = useAtomValue(isReadingDslAtom)
const dslReadError = useAtomValue(dslReadErrorAtom)
const dslUnsupportedMode = useAtomValue(dslUnsupportedModeAtom)
return (
<>
{isReadingDsl && (
<div className="system-xs-regular text-text-tertiary">
{t('createGuide.dsl.reading')}
</div>
)}
{dslReadError && (
<div className="system-xs-regular text-text-destructive">
{t('createGuide.dsl.readFailed')}
</div>
)}
{dslUnsupportedMode && (
<div role="alert" className="system-xs-regular text-text-destructive">
{t('createGuide.dsl.unsupportedMode')}
</div>
)}
</>
)
}

View File

@ -3,6 +3,7 @@
import type { WorkflowSourceApp } from '@/features/deployments/create-guide/state'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
@ -11,12 +12,64 @@ import { DeploymentStateMessage } from '@/features/deployments/components/empty-
import {
effectiveSelectedAppAtom,
selectSourceAppAtom,
setSourceSearchTextAtom,
sourceAppsQueryAtom,
sourceSearchTextAtom,
} from '@/features/deployments/create-guide/state'
import { StepShell } from '../layout'
const sourceAppSkeletonKeys = ['first-source-app', 'second-source-app', 'third-source-app']
export function SourceAppList() {
export function SourceAppSelectionSection() {
const { t } = useTranslation('deployments')
return (
<StepShell
title={t('createGuide.source.title')}
description={t('createGuide.source.description')}
descriptionClassName="lg:hidden"
hideHeader
className="min-h-0 flex-1"
>
<div className="flex min-h-0 flex-1 flex-col gap-3">
<SourceSearchInput />
<SourceAppList />
</div>
</StepShell>
)
}
function SourceSearchInput() {
const { t } = useTranslation('deployments')
const sourceSearchText = useAtomValue(sourceSearchTextAtom)
const setSourceSearchText = useSetAtom(setSourceSearchTextAtom)
return (
<div className="relative">
<span className="pointer-events-none absolute top-1/2 left-2.5 i-ri-search-line size-4 -translate-y-1/2 text-text-tertiary" aria-hidden="true" />
<Input
id="create-guide-source-search"
aria-label={t('createGuide.source.sourceApp')}
value={sourceSearchText}
onChange={event => setSourceSearchText(event.target.value)}
placeholder={t('createGuide.source.searchPlaceholder')}
className="h-9 pr-8 pl-8"
/>
{sourceSearchText && (
<button
type="button"
aria-label={t('createGuide.source.clearSearch')}
onClick={() => setSourceSearchText('')}
className="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center text-text-quaternary hover:text-text-secondary"
>
<span className="i-ri-close-circle-fill size-4" aria-hidden="true" />
</button>
)}
</div>
)
}
function SourceAppList() {
const { t } = useTranslation('deployments')
const selectSourceApp = useSetAtom(selectSourceAppAtom)
const effectiveSelectedApp = useAtomValue(effectiveSelectedAppAtom)

View File

@ -1,39 +0,0 @@
'use client'
import { Input } from '@langgenius/dify-ui/input'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import {
setSourceSearchTextAtom,
sourceSearchTextAtom,
} from '@/features/deployments/create-guide/state'
export function SourceSearchInput() {
const { t } = useTranslation('deployments')
const sourceSearchText = useAtomValue(sourceSearchTextAtom)
const setSourceSearchText = useSetAtom(setSourceSearchTextAtom)
return (
<div className="relative">
<span className="pointer-events-none absolute top-1/2 left-2.5 i-ri-search-line size-4 -translate-y-1/2 text-text-tertiary" aria-hidden="true" />
<Input
id="create-guide-source-search"
aria-label={t('createGuide.source.sourceApp')}
value={sourceSearchText}
onChange={event => setSourceSearchText(event.target.value)}
placeholder={t('createGuide.source.searchPlaceholder')}
className="h-9 pr-8 pl-8"
/>
{sourceSearchText && (
<button
type="button"
aria-label={t('createGuide.source.clearSearch')}
onClick={() => setSourceSearchText('')}
className="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center text-text-quaternary hover:text-text-secondary"
>
<span className="i-ri-close-circle-fill size-4" aria-hidden="true" />
</button>
)}
</div>
)
}

View File

@ -1,25 +0,0 @@
'use client'
import { useTranslation } from 'react-i18next'
import { StepShell } from '../../shell/layout'
import { SourceAppList } from './list'
import { SourceSearchInput } from './search-input'
export function SourceAppSelectionSection() {
const { t } = useTranslation('deployments')
return (
<StepShell
title={t('createGuide.source.title')}
description={t('createGuide.source.description')}
descriptionClassName="lg:hidden"
hideHeader
className="min-h-0 flex-1"
>
<div className="flex min-h-0 flex-1 flex-col gap-3">
<SourceSearchInput />
<SourceAppList />
</div>
</StepShell>
)
}

View File

@ -8,7 +8,7 @@ import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { TitleTooltip } from '@/features/deployments/components/title-tooltip'
import { methodAtom, selectMethodAtom } from '@/features/deployments/create-guide/state'
import { StepShell } from '../shell/layout'
import { StepShell } from '../layout'
function SourceMethodCard({ value, icon, title, description, badge }: {
value: GuideMethod

View File

@ -1,10 +1,17 @@
'use client'
import { useAtomValue } from 'jotai'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { UnsupportedDslNodesAlert } from '@/features/deployments/components/unsupported-dsl-nodes-alert'
import { methodAtom, unsupportedDslNodesAtom } from '@/features/deployments/create-guide/state'
import { DslUploadSection } from './dsl/upload-section'
import { SourceAppSelectionSection } from './source-app/selection-section'
import {
continueFromSourceAtom,
methodAtom,
sourceCanGoNextAtom,
unsupportedDslNodesAtom,
} from '@/features/deployments/create-guide/state'
import { DslUploadSection } from './dsl-upload-section'
import { SourceAppSelectionSection } from './source-app-selection-section'
import { SourceMethodSection } from './source-method-section'
export function SourceStepContent() {
@ -24,3 +31,23 @@ export function SourceStepContent() {
</div>
)
}
export function SourceActionButtons() {
const { t } = useTranslation('deployments')
const canGoNext = useAtomValue(sourceCanGoNextAtom)
const continueFromSource = useSetAtom(continueFromSourceAtom)
return (
<Button
type="button"
variant="primary"
disabled={!canGoNext}
onClick={() => continueFromSource({
defaultDslAppName: t('createGuide.dsl.defaultAppName'),
defaultReleaseName: t('createGuide.release.defaultName'),
})}
>
{t('createGuide.actions.next')}
</Button>
)
}

View File

@ -1,118 +0,0 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import {
canDeployAtom,
canSkipDeploymentAtom,
createDeploymentGuideSubmissionAtom,
CreateDeploymentGuideSubmissionBlockedError,
isCreatingReleaseOnlyAtom,
isSubmittingDeploymentGuideAtom,
stepAtom,
} from '@/features/deployments/create-guide/state'
import { deploymentErrorMessage } from '@/features/deployments/error'
import { useRouter } from '@/next/navigation'
export function TargetBackButton() {
const { t } = useTranslation('deployments')
const setStep = useSetAtom(stepAtom)
const isSubmitting = useAtomValue(isSubmittingDeploymentGuideAtom)
return (
<Button type="button" variant="secondary" onClick={() => setStep('release')} disabled={isSubmitting}>
{t('createGuide.actions.back')}
</Button>
)
}
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 (
<Button type="button" variant="secondary" disabled={!canSkipDeployment || isSubmitting} onClick={handleSkipDeployment}>
{label}
</Button>
)
}
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 (
<Button type="button" variant="primary" disabled={!canDeploy || isSubmitting} onClick={handleDeploy}>
{label}
</Button>
)
}
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)
}

View File

@ -12,7 +12,7 @@ import {
selectBindingAtom,
unsupportedDslNodesAtom,
} from '@/features/deployments/create-guide/state'
import { TargetBindingSkeleton } from '../skeletons'
import { TargetBindingSkeleton } from './skeletons'
export function TargetBindingSection() {
const { t } = useTranslation('deployments')

View File

@ -13,7 +13,7 @@ import {
effectiveSelectedEnvironmentIdAtom,
selectedEnvironmentIdAtom,
} from '@/features/deployments/create-guide/state'
import { TargetEnvironmentSkeleton } from '../skeletons'
import { TargetEnvironmentSkeleton } from './skeletons'
function EnvironmentOptionRow({ environment }: {
environment: Environment

View File

@ -1,13 +1,26 @@
'use client'
import { useAtomValue } from 'jotai'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { UnsupportedDslNodesAlert } from '@/features/deployments/components/unsupported-dsl-nodes-alert'
import { unsupportedDslNodesAtom } from '@/features/deployments/create-guide/state'
import { StepShell } from '../shell/layout'
import { TargetBindingSection } from './bindings/section'
import { TargetEnvVarSection } from './env-vars/section'
import { TargetEnvironmentSection } from './environment/section'
import {
canDeployAtom,
canSkipDeploymentAtom,
createDeploymentGuideSubmissionAtom,
CreateDeploymentGuideSubmissionBlockedError,
isCreatingReleaseOnlyAtom,
isSubmittingDeploymentGuideAtom,
stepAtom,
unsupportedDslNodesAtom,
} from '@/features/deployments/create-guide/state'
import { deploymentErrorMessage } from '@/features/deployments/error'
import { useRouter } from '@/next/navigation'
import { StepShell } from '../layout'
import { TargetBindingSection } from './binding-section'
import { TargetEnvVarSection } from './env-var-section'
import { TargetEnvironmentSection } from './environment-section'
export function TargetStepContent() {
const { t } = useTranslation('deployments')
@ -28,3 +41,104 @@ export function TargetStepContent() {
</StepShell>
)
}
export function TargetBackButton() {
const { t } = useTranslation('deployments')
const setStep = useSetAtom(stepAtom)
const isSubmitting = useAtomValue(isSubmittingDeploymentGuideAtom)
return (
<Button type="button" variant="secondary" onClick={() => setStep('release')} disabled={isSubmitting}>
{t('createGuide.actions.back')}
</Button>
)
}
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 (
<Button type="button" variant="secondary" disabled={!canSkipDeployment || isSubmitting} onClick={handleSkipDeployment}>
{label}
</Button>
)
}
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 (
<Button type="button" variant="primary" disabled={!canDeploy || isSubmitting} onClick={handleDeploy}>
{label}
</Button>
)
}
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)
}