From 141d936e91dd20bc43f6da33a3c84f5eac4a9de6 Mon Sep 17 00:00:00 2001 From: zhangx1n Date: Mon, 4 May 2026 15:34:22 +0800 Subject: [PATCH] feat(app-deploy): wire release deployment UI --- .../generated/enterprise/orpc.gen.ts | 22 +- .../generated/enterprise/types.gen.ts | 35 +- .../contracts/generated/enterprise/zod.gen.ts | 28 +- .../components/create-instance-modal.tsx | 15 +- .../deployments/components/deploy-drawer.tsx | 23 +- .../components/deploy-drawer/form.tsx | 315 +++++++++++++----- .../components/deploy-drawer/select.tsx | 2 +- .../deployments/components/rollback-modal.tsx | 1 + .../deployments/detail/access-tab.tsx | 15 +- .../detail/access-tab/api-keys.tsx | 9 +- .../access-tab/developer-api-section.tsx | 11 + .../deployments/detail/overview-tab.tsx | 41 ++- .../deployments/detail/versions-tab.tsx | 81 ++++- .../versions-tab/deploy-release-menu.tsx | 11 - .../hooks/use-deployment-mutations.ts | 97 +++--- web/features/deployments/types.ts | 2 + web/features/deployments/utils.ts | 7 + web/i18n/en-US/deployments.json | 26 +- web/i18n/zh-Hans/deployments.json | 26 +- 19 files changed, 575 insertions(+), 192 deletions(-) diff --git a/packages/contracts/generated/enterprise/orpc.gen.ts b/packages/contracts/generated/enterprise/orpc.gen.ts index 85b9b8f273..3b0c150f7d 100644 --- a/packages/contracts/generated/enterprise/orpc.gen.ts +++ b/packages/contracts/generated/enterprise/orpc.gen.ts @@ -36,6 +36,7 @@ import { zEnterpriseAppDeployConsoleListAppInstancesQuery, zEnterpriseAppDeployConsoleListAppInstancesResponse, zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath, + zEnterpriseAppDeployConsoleListDeploymentBindingOptionsQuery, zEnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse, zEnterpriseAppDeployConsoleListDeploymentEnvironmentOptionsResponse, zEnterpriseAppDeployConsoleListReleasesPath, @@ -46,6 +47,8 @@ import { zEnterpriseAppDeployConsolePreviewReleaseBody, zEnterpriseAppDeployConsolePreviewReleasePath, zEnterpriseAppDeployConsolePreviewReleaseResponse, + zEnterpriseAppDeployConsoleRevealDeveloperApiKeyPath, + zEnterpriseAppDeployConsoleRevealDeveloperApiKeyResponse, zEnterpriseAppDeployConsoleSearchAccessSubjectsPath, zEnterpriseAppDeployConsoleSearchAccessSubjectsQuery, zEnterpriseAppDeployConsoleSearchAccessSubjectsResponse, @@ -197,6 +200,17 @@ export const deleteDeveloperApiKey = oc .input(z.object({ params: zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyPath })) .output(zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse) +export const revealDeveloperApiKey = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'EnterpriseAppDeployConsole_RevealDeveloperApiKey', + path: '/enterprise/app-instances/{appInstanceId}/api-keys/{apiKeyId}:reveal', + tags: ['EnterpriseAppDeployConsole'], + }) + .input(z.object({ params: zEnterpriseAppDeployConsoleRevealDeveloperApiKeyPath })) + .output(zEnterpriseAppDeployConsoleRevealDeveloperApiKeyResponse) + export const listDeploymentBindingOptions = oc .route({ inputStructure: 'detailed', @@ -205,7 +219,12 @@ export const listDeploymentBindingOptions = oc path: '/enterprise/app-instances/{appInstanceId}/deployment-binding-options', tags: ['EnterpriseAppDeployConsole'], }) - .input(z.object({ params: zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath })) + .input( + z.object({ + params: zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath, + query: zEnterpriseAppDeployConsoleListDeploymentBindingOptionsQuery.optional(), + }), + ) .output(zEnterpriseAppDeployConsoleListDeploymentBindingOptionsResponse) export const createDeployment = oc @@ -400,6 +419,7 @@ export const appDeploy = { searchAccessSubjects, createDeveloperApiKey, deleteDeveloperApiKey, + revealDeveloperApiKey, listDeploymentBindingOptions, createDeployment, updateDeveloperApi, diff --git a/packages/contracts/generated/enterprise/types.gen.ts b/packages/contracts/generated/enterprise/types.gen.ts index 56228f2738..078c737930 100644 --- a/packages/contracts/generated/enterprise/types.gen.ts +++ b/packages/contracts/generated/enterprise/types.gen.ts @@ -29,6 +29,7 @@ export type AccessStatus = { cliUrl?: string developerApiEnabled?: boolean apiKeyCount?: number + apiUrl?: string } export type AccessSubject = { @@ -92,6 +93,10 @@ export type AppInstanceBasicInfo = { sourceAppName?: string mode?: string createdAt?: string + sourceAppAvailable?: boolean + canCreateRelease?: boolean + icon?: string + iconBackground?: string } export type AppInstanceCard = { @@ -102,6 +107,9 @@ export type AppInstanceCard = { sourceAppName?: string statuses?: Array lastDeployedAt?: string + sourceAppAvailable?: boolean + canCreateRelease?: boolean + iconBackground?: string } export type AppRunnerBatchRuntimeArtifactReply = { @@ -256,7 +264,6 @@ export type ConsoleUser = { export type CreateAppInstanceReply = { appInstanceId?: string - initialRelease?: ConsoleRelease } export type CreateAppInstanceReq = { @@ -486,6 +493,7 @@ export type DeploymentStatusRow = { export type DeveloperApiAccess = { enabled?: boolean apiKeys?: Array + apiUrl?: string } export type DeveloperApiKeyRow = { @@ -1065,6 +1073,10 @@ export type RetryEnvironmentReq = { id?: string } +export type RevealDeveloperApiKeyReply = { + token?: string +} + export type RuntimeEndpoints = { run?: string health?: string @@ -1609,12 +1621,31 @@ export type EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses = { export type EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse = EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses[keyof EnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponses] +export type EnterpriseAppDeployConsoleRevealDeveloperApiKeyData = { + body?: never + path: { + appInstanceId: string + apiKeyId: string + } + query?: never + url: '/enterprise/app-instances/{appInstanceId}/api-keys/{apiKeyId}:reveal' +} + +export type EnterpriseAppDeployConsoleRevealDeveloperApiKeyResponses = { + 200: RevealDeveloperApiKeyReply +} + +export type EnterpriseAppDeployConsoleRevealDeveloperApiKeyResponse + = EnterpriseAppDeployConsoleRevealDeveloperApiKeyResponses[keyof EnterpriseAppDeployConsoleRevealDeveloperApiKeyResponses] + export type EnterpriseAppDeployConsoleListDeploymentBindingOptionsData = { body?: never path: { appInstanceId: string } - query?: never + query?: { + releaseId?: string + } url: '/enterprise/app-instances/{appInstanceId}/deployment-binding-options' } diff --git a/packages/contracts/generated/enterprise/zod.gen.ts b/packages/contracts/generated/enterprise/zod.gen.ts index 1e7e3d44ae..1fcb927ef0 100644 --- a/packages/contracts/generated/enterprise/zod.gen.ts +++ b/packages/contracts/generated/enterprise/zod.gen.ts @@ -19,6 +19,7 @@ export const zAccessStatus = z.object({ .min(-2147483648, { error: 'Invalid value: Expected int32 to be >= -2147483648' }) .max(2147483647, { error: 'Invalid value: Expected int32 to be <= 2147483647' }) .optional(), + apiUrl: z.string().optional(), }) export const zAccessSubject = z.object({ @@ -88,6 +89,10 @@ export const zAppInstanceBasicInfo = z.object({ sourceAppName: z.string().optional(), mode: z.string().optional(), createdAt: z.iso.datetime().optional(), + sourceAppAvailable: z.boolean().optional(), + canCreateRelease: z.boolean().optional(), + icon: z.string().optional(), + iconBackground: z.string().optional(), }) export const zAppRunnerBootstrapAssignment = z.object({ @@ -228,7 +233,6 @@ export const zConsoleUser = z.object({ export const zCreateAppInstanceReply = z.object({ appInstanceId: z.string().optional(), - initialRelease: zConsoleRelease.optional(), }) export const zCreateAppInstanceReq = z.object({ @@ -445,6 +449,7 @@ export const zCreateDeveloperApiKeyReply = z.object({ export const zDeveloperApiAccess = z.object({ enabled: z.boolean().optional(), apiKeys: z.array(zDeveloperApiKeyRow).optional(), + apiUrl: z.string().optional(), }) /** @@ -1010,6 +1015,10 @@ export const zRetryEnvironmentReq = z.object({ id: z.string().optional(), }) +export const zRevealDeveloperApiKeyReply = z.object({ + token: z.string().optional(), +}) + export const zRuntimeEndpoints = z.object({ run: z.string().optional(), health: z.string().optional(), @@ -1139,6 +1148,9 @@ export const zAppInstanceCard = z.object({ sourceAppName: z.string().optional(), statuses: z.array(zStatusCount).optional(), lastDeployedAt: z.iso.datetime().optional(), + sourceAppAvailable: z.boolean().optional(), + canCreateRelease: z.boolean().optional(), + iconBackground: z.string().optional(), }) export const zSubjectAccountData = z.object({ @@ -1711,10 +1723,24 @@ export const zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyPath = z.object({ */ export const zEnterpriseAppDeployConsoleDeleteDeveloperApiKeyResponse = zDeleteDeveloperApiKeyReply +export const zEnterpriseAppDeployConsoleRevealDeveloperApiKeyPath = z.object({ + appInstanceId: z.string(), + apiKeyId: z.string(), +}) + +/** + * OK + */ +export const zEnterpriseAppDeployConsoleRevealDeveloperApiKeyResponse = zRevealDeveloperApiKeyReply + export const zEnterpriseAppDeployConsoleListDeploymentBindingOptionsPath = z.object({ appInstanceId: z.string(), }) +export const zEnterpriseAppDeployConsoleListDeploymentBindingOptionsQuery = z.object({ + releaseId: z.string().optional(), +}) + /** * OK */ diff --git a/web/features/deployments/components/create-instance-modal.tsx b/web/features/deployments/components/create-instance-modal.tsx index 16f4b7f842..e4d94d3722 100644 --- a/web/features/deployments/components/create-instance-modal.tsx +++ b/web/features/deployments/components/create-instance-modal.tsx @@ -204,7 +204,6 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { const { t } = useTranslation('deployments') const router = useRouter() const createInstance = useCreateDeploymentInstance() - const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const { data: appList, isLoading } = useAppList({ page: 1, limit: MAX_STUDIO_SOURCE_APPS, name: '' }) const apps = useMemo(() => { return (appList?.data ?? []).map(toStudioSourceAppInfo) @@ -218,7 +217,7 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { const selectedApp = apps.find(a => a.id === appId) const canCreate = Boolean(appId && name.trim() && !isSubmitting) - const handleCreate = async (thenDeploy: boolean) => { + const handleCreate = async () => { if (!canCreate) return @@ -230,13 +229,6 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { description: description.trim() || undefined, }) onClose() - if (thenDeploy) { - openDeployDrawer({ - appInstanceId: result.appInstanceId, - releaseId: result.initialRelease?.id, - }) - return - } router.push(`/deployments/${result.appInstanceId}/overview`) } catch { @@ -299,12 +291,9 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { - - ) diff --git a/web/features/deployments/components/deploy-drawer.tsx b/web/features/deployments/components/deploy-drawer.tsx index 9c1f3eb7f5..6e8d5512e2 100644 --- a/web/features/deployments/components/deploy-drawer.tsx +++ b/web/features/deployments/components/deploy-drawer.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog' +import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -62,15 +63,21 @@ const DeployDrawer: FC = () => { defaultReleaseId={defaultReleaseId} lockedEnvId={drawer.environmentId} presetReleaseId={drawer.releaseId} + isSubmitting={startDeploy.isPending} onCancel={closeDeployDrawer} - onSubmit={({ environmentId, releaseId, releaseNote }) => { - closeDeployDrawer() - startDeploy.mutate({ - appInstanceId: drawerAppInstanceId, - environmentId, - releaseId, - releaseNote, - }) + onSubmit={async ({ environmentId, releaseId, bindings }) => { + try { + await startDeploy.mutateAsync({ + appInstanceId: drawerAppInstanceId, + environmentId, + releaseId, + bindings, + }) + closeDeployDrawer() + } + catch { + toast.error(t('deployDrawer.deployFailed')) + } }} /> )} diff --git a/web/features/deployments/components/deploy-drawer/form.tsx b/web/features/deployments/components/deploy-drawer/form.tsx index 2e6a1438f8..d935ca22e0 100644 --- a/web/features/deployments/components/deploy-drawer/form.tsx +++ b/web/features/deployments/components/deploy-drawer/form.tsx @@ -1,24 +1,19 @@ 'use client' +import type { DeploymentBindingOptionSlot, DeploymentRuntimeBinding } from '@dify/contracts/enterprise/types.gen' import type { FC } from 'react' -import type { ConsoleReleaseSummary, EnvironmentOption, RuntimeBindingDisplay } from '@/features/deployments/types' +import type { ConsoleReleaseSummary, EnvironmentOption } from '@/features/deployments/types' import { Button } from '@langgenius/dify-ui/button' import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { skipToken, useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import { consoleQuery } from '@/service/client' import { environmentMode, environmentName, - isRuntimeEnvVarBinding, - isRuntimeModelBinding, - isRuntimePluginBinding, releaseCommit, releaseLabel, - runtimeBindingLabel, - runtimeBindingValue, } from '../../utils' import { DeploymentSelect, @@ -28,8 +23,8 @@ import { export type DeployFormSubmit = { environmentId: string - releaseId?: string - releaseNote?: string + releaseId: string + bindings: DeploymentRuntimeBinding[] } type DeployFormProps = { @@ -39,41 +34,160 @@ type DeployFormProps = { defaultReleaseId?: string lockedEnvId?: string presetReleaseId?: string + isSubmitting?: boolean onCancel: () => void - onSubmit: (params: DeployFormSubmit) => void + onSubmit: (params: DeployFormSubmit) => void | Promise } -type RuntimeBindingGroupProps = { +type BindingSelections = Record + +type BindingSelectOption = { + value: string label: string - bindings: RuntimeBindingDisplay[] - isLoading: boolean } -const RuntimeBindingGroup: FC = ({ label, bindings, isLoading }) => { +type BindingOptionsPanelProps = { + slots: DeploymentBindingOptionSlot[] + selections: BindingSelections + isLoading: boolean + hasError: boolean + onChange: (slot: string, value: string) => void +} + +const isEnvBindingSlot = (slot: DeploymentBindingOptionSlot) => + (slot.kind?.toLowerCase() ?? '').includes('env') + +const bindingSlotKey = (slot: DeploymentBindingOptionSlot) => slot.slot ?? '' + +const 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!, + })) +} + +const hasMissingRequiredBinding = (slot: DeploymentBindingOptionSlot, selectedValue?: string) => + Boolean(slot.required && !selectedValue) + +const 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)) +} + +const BindingOptionsPanel: FC = ({ + slots, + selections, + isLoading, + hasError, + onChange, +}) => { const { t } = useTranslation('deployments') - return ( -
-
{label}
-
- {isLoading - ? {t('deployDrawer.loadingBindings')} - : bindings.length === 0 - ? {t('deployDrawer.noBindingRequired')} - : bindings.map(binding => ( -
- - {runtimeBindingLabel(binding)} - - - {runtimeBindingValue(binding)} - -
- ))} + if (isLoading) { + return ( +
+ {t('deployDrawer.loadingBindings')}
+ ) + } + + 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')} +
+ )} +
+ ) + })}
) } @@ -85,6 +199,7 @@ export const DeployForm: FC = ({ defaultReleaseId, lockedEnvId, presetReleaseId, + isSubmitting, onCancel, onSubmit, }) => { @@ -101,34 +216,71 @@ export const DeployForm: FC = ({ ) const selectedEnvironmentId = selectedEnvId || lockedEnvId || environments[0]?.id || '' const selectedEnvironment = environments.find(env => env.id === selectedEnvironmentId) - const [releaseNote, setReleaseNote] = useState('') - const canDeploy = Boolean(selectedEnvironmentId && selectedEnvironment && !selectedEnvironment.disabled && (!isPromote || displayedRelease?.id || defaultReleaseId)) - const previewReleaseId = isPromote ? displayedRelease?.id ?? defaultReleaseId : undefined - const releasePreview = useQuery(consoleQuery.enterprise.appDeploy.previewRelease.queryOptions({ - input: appInstanceId && (!isPromote || previewReleaseId) + const [selectedReleaseId, setSelectedReleaseId] = useState( + () => displayedRelease?.id ?? defaultReleaseId ?? '', + ) + const selectedRelease = releases.find(release => release.id === selectedReleaseId) + const targetReleaseId = displayedRelease?.id ?? selectedRelease?.id ?? selectedReleaseId + const bindingOptions = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentBindingOptions.queryOptions({ + input: appInstanceId && targetReleaseId ? { params: { appInstanceId }, - body: { - releaseId: previewReleaseId, + query: { + releaseId: targetReleaseId, }, } : skipToken, })) - const previewBindings = releasePreview.data?.bindings ?? [] - const modelBindings = previewBindings.filter(isRuntimeModelBinding) - const pluginBindings = previewBindings.filter(isRuntimePluginBinding) - const envVarBindings = previewBindings.filter(isRuntimeEnvVarBinding) + const bindingSlots = useMemo( + () => bindingOptions.data?.slots?.filter(slot => slot.slot) ?? [], + [bindingOptions.data?.slots], + ) + const [manualBindings, setManualBindings] = useState({}) + const selectedBindings = useMemo(() => { + const next: BindingSelections = {} + for (const slot of bindingSlots) { + 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 + }, [bindingSlots, manualBindings]) + const deploymentBindings = useMemo( + () => selectedDeploymentBindings(bindingSlots, selectedBindings), + [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 canDeploy = Boolean( + selectedEnvironmentId + && selectedEnvironment + && !selectedEnvironment.disabled + && targetReleaseId + && bindingOptionsReady + && requiredBindingsReady + && !isSubmitting, + ) const lockedEnv = lockedEnvId ? environments.find(e => e.id === lockedEnvId) : undefined + const submitLabel = isSubmitting + ? t('deployDrawer.deploying') + : isPromote + ? t('deployDrawer.promote') + : t('deployDrawer.deploy') const handleDeploy = () => { - if (!canDeploy) + if (!canDeploy || !targetReleaseId) return onSubmit({ environmentId: selectedEnvironmentId, - releaseId: displayedRelease?.id ?? (isPromote ? defaultReleaseId : undefined), - releaseNote: isPromote ? undefined : releaseNote, + releaseId: targetReleaseId, + bindings: deploymentBindings, }) } @@ -143,7 +295,7 @@ export const DeployForm: FC = ({
- + {isPromote && displayedRelease ? (
@@ -166,19 +318,23 @@ export const DeployForm: FC = ({
) - : ( -
- setReleaseNote(e.target.value)} - placeholder={t('deployDrawer.notePlaceholder')} - maxLength={80} + : releases.length === 0 + ? ( +
+ {t('deployDrawer.noReleaseAvailable')} +
+ ) + : ( + release.id).map(release => ({ + value: release.id!, + label: `${releaseLabel(release)} · ${releaseCommit(release)}`, + }))} + placeholder={t('deployDrawer.selectRelease')} /> - - {t('deployDrawer.newReleaseHint')} - -
- )} + )}
= ({ )} -
-
-
-
{t('deployDrawer.runtimeCredentials')}
- {t('deployDrawer.bindingsDisabled')} -
- - {t('deployDrawer.readOnly')} - -
- setManualBindings(prev => ({ ...prev, [slot]: value }))} /> - - -
+ )}
diff --git a/web/features/deployments/components/deploy-drawer/select.tsx b/web/features/deployments/components/deploy-drawer/select.tsx index d9dea61ebc..b470a80bcb 100644 --- a/web/features/deployments/components/deploy-drawer/select.tsx +++ b/web/features/deployments/components/deploy-drawer/select.tsx @@ -54,7 +54,7 @@ export const DeploymentSelect: FC = ({ value, onChange, options, pl > diff --git a/web/features/deployments/components/rollback-modal.tsx b/web/features/deployments/components/rollback-modal.tsx index 8951d6e602..853240dd62 100644 --- a/web/features/deployments/components/rollback-modal.tsx +++ b/web/features/deployments/components/rollback-modal.tsx @@ -84,6 +84,7 @@ const RollbackModal: FC = () => { appInstanceId: modal.appInstanceId, environmentId: modal.environmentId, releaseId: modal.targetReleaseId, + bindings: [], }) } diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx index efe7b85aab..305e9c4b80 100644 --- a/web/features/deployments/detail/access-tab.tsx +++ b/web/features/deployments/detail/access-tab.tsx @@ -8,7 +8,7 @@ import type { } from '@/features/deployments/types' import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' -import { consoleQuery } from '@/service/client' +import { consoleClient, consoleQuery } from '@/service/client' import { useGenerateDeploymentApiKey, useRevokeDeploymentApiKey, @@ -105,6 +105,17 @@ const AccessTab: FC = ({ instanceId: appId }) => { }, }) } + const handleCopyApiKey = async (apiKeyId: string) => { + const response = await consoleClient.enterprise.appDeploy.revealDeveloperApiKey({ + params: { + appInstanceId: appId, + apiKeyId, + }, + }) + if (!response.token) + throw new Error('Reveal developer API key did not return a token.') + return response.token + } const handleSetEnvironmentAccessPolicy = async ( appId: string, environmentId: string, @@ -150,6 +161,7 @@ const AccessTab: FC = ({ instanceId: appId }) => { /> = ({ instanceId: appId }) => { body: { enabled }, })} onGenerate={handleGenerateApiKey} + onCopyApiKey={handleCopyApiKey} onRevoke={handleRevokeApiKey} onClearCreatedToken={() => setCreatedApiToken(undefined)} /> diff --git a/web/features/deployments/detail/access-tab/api-keys.tsx b/web/features/deployments/detail/access-tab/api-keys.tsx index f2f01d5816..ce6c2d3f08 100644 --- a/web/features/deployments/detail/access-tab/api-keys.tsx +++ b/web/features/deployments/detail/access-tab/api-keys.tsx @@ -16,18 +16,23 @@ import { environmentName } from '../../utils' type ApiKeyRowProps = { apiKey: DeveloperAPIKeySummary + onCopy: (apiKeyId: string) => Promise onRevoke: () => void } -export const ApiKeyRow: FC = ({ apiKey, onRevoke }) => { +export const ApiKeyRow: FC = ({ apiKey, onCopy, onRevoke }) => { const { t } = useTranslation('deployments') const [copied, setCopied] = useState(false) const displayValue = apiKey.maskedKey || apiKey.maskedPrefix || apiKey.id || '—' const environmentLabel = apiKey.environment?.name || apiKey.environmentName || apiKey.environmentId || apiKey.environment?.id const handleCopy = async () => { + if (!apiKey.id) + return + try { - await navigator.clipboard.writeText(displayValue) + const token = await onCopy(apiKey.id) + await navigator.clipboard.writeText(token) setCopied(true) toast.success(t('access.copyToast')) window.setTimeout(() => setCopied(false), 1500) diff --git a/web/features/deployments/detail/access-tab/developer-api-section.tsx b/web/features/deployments/detail/access-tab/developer-api-section.tsx index d8918d779a..eaa246583c 100644 --- a/web/features/deployments/detail/access-tab/developer-api-section.tsx +++ b/web/features/deployments/detail/access-tab/developer-api-section.tsx @@ -9,22 +9,26 @@ import { CopyPill, Section } from './common' type DeveloperApiSectionProps = { apiEnabled: boolean + apiUrl?: string environments: ConsoleEnvironmentSummary[] apiKeys: DeveloperAPIKeySummary[] createdToken?: string onToggle: (enabled: boolean) => void onGenerate: (environmentId: string) => void + onCopyApiKey: (apiKeyId: string) => Promise onRevoke: (environmentId: string, apiKeyId: string) => void onClearCreatedToken: () => void } export const DeveloperApiSection: FC = ({ apiEnabled, + apiUrl, environments, apiKeys, createdToken, onToggle, onGenerate, + onCopyApiKey, onRevoke, onClearCreatedToken, }) => { @@ -44,6 +48,12 @@ export const DeveloperApiSection: FC = ({ {apiEnabled ? (
+ {apiUrl && ( + + )}
@@ -102,6 +112,7 @@ export const DeveloperApiSection: FC = ({ onRevoke(environmentId, apiKey.id!)} /> ) diff --git a/web/features/deployments/detail/overview-tab.tsx b/web/features/deployments/detail/overview-tab.tsx index d09d310396..e8b1359f02 100644 --- a/web/features/deployments/detail/overview-tab.tsx +++ b/web/features/deployments/detail/overview-tab.tsx @@ -9,7 +9,10 @@ import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode- import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/client' import { StatusBadge } from '../components/status-badge' -import { deploymentOverviewQueryOptions } from '../queries' +import { + deploymentOverviewQueryOptions, + deploymentReleaseHistoryQueryOptions, +} from '../queries' import { useDeploymentsStore } from '../store' import { releaseLabel, @@ -56,16 +59,18 @@ type AccessOverviewRowProps = { label: string enabled: boolean hint?: string + meta?: string } -const AccessOverviewRow: FC = ({ label, enabled, hint }) => { +const AccessOverviewRow: FC = ({ label, enabled, hint, meta }) => { const { t } = useTranslation('deployments') return (
{label} - {hint && {hint}} + {hint && {hint}} + {meta && {meta}}
= ({ instanceId }) => { const router = useRouter() const input = { params: { appInstanceId: instanceId } } const { data: overview } = useQuery(deploymentOverviewQueryOptions(instanceId)) + const { data: releaseHistory } = useQuery(deploymentReleaseHistoryQueryOptions(instanceId)) const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({ input, })) @@ -108,6 +114,8 @@ const OverviewTab: FC = ({ instanceId }) => { () => overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? [], [overview?.deployments], ) + const releaseRows = releaseHistory?.data?.filter(row => row.id) ?? [] + const canCreateRelease = overviewApp?.canCreateRelease ?? true if (!app) return null @@ -119,6 +127,7 @@ const OverviewTab: FC = ({ instanceId }) => { const appModeLabel = getAppModeLabel(overviewApp?.mode ?? app.mode, tCommon) const webappAccessUrl = webappUrl(overview?.access?.webappUrl) const cliUrl = overview?.access?.cliUrl + const apiUrl = overview?.access?.apiUrl ?? accessConfig?.developerApi?.apiUrl const apiKeysCount = overview?.access?.apiKeyCount ?? accessConfig?.developerApi?.apiKeys?.length ?? 0 return ( @@ -145,9 +154,24 @@ const OverviewTab: FC = ({ instanceId }) => { ? (
-
{t('overview.notDeployedYet')}
-
) @@ -195,8 +219,11 @@ const OverviewTab: FC = ({ instanceId }) => { label={t('overview.api')} enabled={overview?.access?.developerApiEnabled ?? false} hint={overview?.access?.developerApiEnabled - ? t('overview.apiKeysCount', { count: apiKeysCount }) + ? apiUrl || t('overview.notConfigured') : t('overview.notConfigured')} + meta={overview?.access?.developerApiEnabled + ? t('overview.apiKeysCount', { count: apiKeysCount }) + : undefined} />
diff --git a/web/features/deployments/detail/versions-tab.tsx b/web/features/deployments/detail/versions-tab.tsx index 525b48d998..e570a62a98 100644 --- a/web/features/deployments/detail/versions-tab.tsx +++ b/web/features/deployments/detail/versions-tab.tsx @@ -1,12 +1,17 @@ 'use client' import type { FC } from 'react' +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useQuery } from '@tanstack/react-query' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import Input from '@/app/components/base/input' +import { useCreateDeploymentRelease } from '../hooks/use-deployment-mutations' import { deploymentEnvironmentDeploymentsQueryOptions, + deploymentOverviewQueryOptions, deploymentReleaseHistoryQueryOptions, } from '../queries' import { @@ -27,8 +32,13 @@ type VersionsTabProps = { const VersionsTab: FC = ({ instanceId: appId }) => { const { t } = useTranslation('deployments') + const { data: overview } = useQuery(deploymentOverviewQueryOptions(appId)) const { data: releaseHistory } = useQuery(deploymentReleaseHistoryQueryOptions(appId)) const { data: environmentDeployments } = useQuery(deploymentEnvironmentDeploymentsQueryOptions(appId)) + const createRelease = useCreateDeploymentRelease() + const [isCreating, setIsCreating] = useState(false) + const [releaseName, setReleaseName] = useState('') + const [releaseDescription, setReleaseDescription] = useState('') const releaseRows = useMemo( () => releaseHistory?.data?.filter(row => row.id) ?? [], [releaseHistory?.data], @@ -37,10 +47,32 @@ const VersionsTab: FC = ({ instanceId: appId }) => { () => deployedRows(environmentDeployments?.data), [environmentDeployments?.data], ) + const canCreateRelease = overview?.instance?.canCreateRelease ?? true + const trimmedReleaseName = releaseName.trim() + const canSubmitRelease = Boolean(canCreateRelease && trimmedReleaseName && !createRelease.isPending) + + const handleCreateRelease = async () => { + if (!canSubmitRelease) + return + + try { + await createRelease.mutateAsync({ + appInstanceId: appId, + name: trimmedReleaseName, + description: releaseDescription.trim() || undefined, + }) + setReleaseName('') + setReleaseDescription('') + setIsCreating(false) + } + catch { + toast.error(t('versions.createFailed')) + } + } return (
-
+
{t('versions.releaseHistory')} {' '} @@ -50,12 +82,55 @@ const VersionsTab: FC = ({ instanceId: appId }) => { )
+
+ {!canCreateRelease && ( +
+ {t('versions.sourceAppUnavailable')} +
+ )} + + {isCreating && ( +
+
{t('versions.createRelease')}
+
+ setReleaseName(e.target.value)} + placeholder={t('versions.releaseNamePlaceholder')} + maxLength={128} + /> + setReleaseDescription(e.target.value)} + placeholder={t('versions.releaseDescriptionPlaceholder')} + maxLength={512} + /> +
+ + +
+
+
+ )} + {releaseRows.length === 0 ? (
- {t('versions.empty')} + {canCreateRelease ? t('versions.emptyWithCreate') : t('versions.emptySourceUnavailable')}
) : ( diff --git a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx index f238b7ad03..b53d4ce898 100644 --- a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx +++ b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx @@ -17,7 +17,6 @@ import { useDeploymentsStore } from '../../store' import { activeRelease, deployedRows, - deploymentId, deploymentStatus, environmentId, environmentName, @@ -32,7 +31,6 @@ type DeployReleaseMenuProps = { export const DeployReleaseMenu: FC = ({ appInstanceId, releaseId }) => { const { t } = useTranslation('deployments') const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) - const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal) const [open, setOpen] = useState(false) const { data: environmentDeployments } = useQuery({ ...deploymentEnvironmentDeploymentsQueryOptions(appInstanceId), @@ -79,15 +77,6 @@ export const DeployReleaseMenu: FC = ({ appInstanceId, r setOpen(false) if (disabled) return - if (row) { - openRollbackModal({ - appInstanceId, - environmentId: envId, - deploymentId: deploymentId(row), - targetReleaseId: releaseId, - }) - return - } openDeployDrawer({ appInstanceId, environmentId: envId, releaseId }) }} > diff --git a/web/features/deployments/hooks/use-deployment-mutations.ts b/web/features/deployments/hooks/use-deployment-mutations.ts index a0cd2bf512..49e2a0d79d 100644 --- a/web/features/deployments/hooks/use-deployment-mutations.ts +++ b/web/features/deployments/hooks/use-deployment-mutations.ts @@ -1,7 +1,7 @@ 'use client' +import type { DeploymentRuntimeBinding } from '@dify/contracts/enterprise/types.gen' import type { QueryClient, QueryKey } from '@tanstack/react-query' -import type { ConsoleReleaseSummary } from '@/features/deployments/types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { consoleClient, consoleQuery } from '@/service/client' import { @@ -11,6 +11,7 @@ import { deploymentInstanceDetailQueryKeys, deploymentInstanceIdentityQueryKeys, deploymentInstanceStateQueryKeys, + deploymentOverviewQueryKey, deploymentReleaseHistoryQueryKey, deploymentsListQueryKey, deploymentsListQueryOptions, @@ -18,14 +19,19 @@ import { export type CreateDeploymentInstanceResult = { appInstanceId: string - initialRelease?: ConsoleReleaseSummary } type CreateDeploymentParams = { appInstanceId: string environmentId: string - releaseId?: string - releaseNote?: string + releaseId: string + bindings: DeploymentRuntimeBinding[] +} + +type CreateReleaseParams = { + appInstanceId: string + name: string + description?: string } type CreateInstanceParams = { @@ -113,7 +119,6 @@ export const useCreateDeploymentInstance = () => { return { appInstanceId: response.appInstanceId, - initialRelease: response.initialRelease, } }, onSuccess: () => { @@ -122,6 +127,35 @@ export const useCreateDeploymentInstance = () => { }) } +export const useCreateDeploymentRelease = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: consoleQuery.enterprise.appDeploy.createRelease.mutationKey(), + mutationFn: async ({ appInstanceId, name, description }: CreateReleaseParams) => { + const response = await consoleClient.enterprise.appDeploy.createRelease({ + params: { + appInstanceId, + }, + body: { + name, + description, + }, + }) + if (!response.release?.id) + throw new Error('Create release did not return a release.') + + return response.release + }, + onSuccess: (_data, variables) => { + return invalidateQueries(queryClient, [ + deploymentReleaseHistoryQueryKey(variables.appInstanceId), + deploymentOverviewQueryKey(variables.appInstanceId), + ]) + }, + }) +} + export const useUpdateDeploymentInstance = () => { const queryClient = useQueryClient() @@ -151,58 +185,21 @@ export const useStartDeployment = () => { appInstanceId, environmentId, releaseId, - releaseNote, + bindings, }: CreateDeploymentParams) => { - let targetReleaseId = releaseId - let releaseWasCreated = false - await consoleClient.enterprise.appDeploy.previewRelease({ + if (!releaseId) + throw new Error('releaseId is required to start a deployment.') + + return consoleClient.enterprise.appDeploy.createDeployment({ params: { appInstanceId, }, body: { - releaseId: targetReleaseId, + environmentId, + releaseId, + bindings, }, }) - - try { - if (!targetReleaseId) { - const trimmedReleaseNote = releaseNote?.trim() - const response = await consoleClient.enterprise.appDeploy.createRelease({ - params: { - appInstanceId, - }, - body: { - name: trimmedReleaseNote || 'Release', - description: trimmedReleaseNote || undefined, - }, - }) - releaseWasCreated = true - if (!response.release) - throw new Error('Create release did not return a release.') - targetReleaseId = response.release.id - } - - if (!targetReleaseId) - throw new Error('Failed to create a deployable release.') - - return await consoleClient.enterprise.appDeploy.createDeployment({ - params: { - appInstanceId, - }, - body: { - environmentId, - releaseId: targetReleaseId, - }, - }) - } - catch (error) { - if (releaseWasCreated) { - await queryClient.invalidateQueries({ - queryKey: deploymentReleaseHistoryQueryKey(appInstanceId), - }) - } - throw error - } }, onSuccess: (_data, variables) => { return invalidateDeploymentState(queryClient, variables.appInstanceId) diff --git a/web/features/deployments/types.ts b/web/features/deployments/types.ts index 6a74328ec6..fa8aa63fc6 100644 --- a/web/features/deployments/types.ts +++ b/web/features/deployments/types.ts @@ -23,6 +23,8 @@ export type AppInfo = { description?: string sourceAppId?: string sourceAppName?: string + sourceAppAvailable?: boolean + canCreateRelease?: boolean } export type ConsoleEnvironmentSummary = EnterpriseContract.ConsoleEnvironment & { diff --git a/web/features/deployments/utils.ts b/web/features/deployments/utils.ts index 273766a9b9..ad03738cf4 100644 --- a/web/features/deployments/utils.ts +++ b/web/features/deployments/utils.ts @@ -123,7 +123,10 @@ export function toAppInfoFromSummary(summary: AppDeploymentSummary): AppInfo | u mode: (summary.mode || 'workflow') as AppMode, iconType: 'emoji', icon: summary.icon, + iconBackground: summary.iconBackground, sourceAppName: summary.sourceAppName, + sourceAppAvailable: summary.sourceAppAvailable, + canCreateRelease: summary.canCreateRelease, } } @@ -136,9 +139,13 @@ export function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | name: instance.name ?? instance.id, mode: (instance.mode || 'workflow') as AppMode, iconType: 'emoji', + icon: instance.icon, + iconBackground: instance.iconBackground, description: instance.description ?? undefined, sourceAppId: instance.sourceAppId, sourceAppName: instance.sourceAppName, + sourceAppAvailable: instance.sourceAppAvailable, + canCreateRelease: instance.canCreateRelease, } } diff --git a/web/i18n/en-US/deployments.json b/web/i18n/en-US/deployments.json index 628f056b1d..59740f90c7 100644 --- a/web/i18n/en-US/deployments.json +++ b/web/i18n/en-US/deployments.json @@ -5,6 +5,7 @@ "access.api.disabled": "API access is turned off for this instance.", "access.api.dismissToken": "Dismiss token", "access.api.empty": "Deploy to an environment first to start issuing API keys.", + "access.api.endpoint": "Endpoint", "access.api.envPrefix": "env: {{env}}", "access.api.keyList": "API key list", "access.api.newKey": "New key", @@ -95,7 +96,6 @@ "createModal.appSearchPlaceholder": "Search apps…", "createModal.cancel": "Cancel", "createModal.create": "Create", - "createModal.createAndDeploy": "Create and deploy", "createModal.createFailed": "Failed to create app instance.", "createModal.description": "Pick a source app from Studio and create a deployable instance.", "createModal.descriptionLabel": "Description", @@ -108,18 +108,25 @@ "createModal.sourceApp": "Source app (required)", "createModal.title": "Create app instance", "deployDrawer.bindingsDisabled": "Resolved from the release preview. Editing is not available yet.", + "deployDrawer.bindingOptionsFailed": "Failed to load credential options.", + "deployDrawer.bindingSelectionHint": "Choose the credentials used by this deployment.", "deployDrawer.cancel": "Cancel", "deployDrawer.defaultSelect": "Select...", "deployDrawer.deploy": "Deploy", - "deployDrawer.description": "Create a new release from the current app YAML and deploy it to a target environment.", + "deployDrawer.deployFailed": "Failed to start deployment.", + "deployDrawer.deploying": "Deploying...", + "deployDrawer.description": "Select a release and deploy it to a target environment.", "deployDrawer.envVars": "Environment variables", "deployDrawer.existingReleaseHint": "This existing release will be deployed as-is. No new release will be created.", "deployDrawer.loadingBindings": "Resolving...", "deployDrawer.lockedHint": "Locked to current environment", + "deployDrawer.missingRequiredBinding": "Select a credential for this required binding.", "deployDrawer.modelCreds": "Model credentials", "deployDrawer.needsValidation": " (needs validation)", "deployDrawer.newReleaseHint": "A new release will be created from the current app YAML.", "deployDrawer.noBindingRequired": "Not required", + "deployDrawer.noCredentialCandidates": "No available credentials.", + "deployDrawer.noReleaseAvailable": "Create a release before deploying this app instance.", "deployDrawer.notFound": "Instance not found.", "deployDrawer.noteLabel": "Release note (optional)", "deployDrawer.notePlaceholder": "e.g. Ship onboarding copy tweak", @@ -129,9 +136,11 @@ "deployDrawer.promoteTitle": "Promote release", "deployDrawer.readOnly": "Read-only", "deployDrawer.releaseLabel": "Release", + "deployDrawer.requiredBinding": "Required", "deployDrawer.runtimeCredentials": "Runtime credentials", "deployDrawer.secretPlaceholder": "secret", "deployDrawer.selectEnv": "Select an environment", + "deployDrawer.selectCredential": "Select a credential", "deployDrawer.selectProviderCred": "Select {{provider}} credential", "deployDrawer.selectProviderKey": "Select {{provider}} key", "deployDrawer.selectRelease": "Select a release", @@ -227,6 +236,9 @@ "overview.noAccessConfig": "No access configuration.", "overview.notConfigured": "Not configured", "overview.notDeployedYet": "Not deployed yet.", + "overview.noReleaseYet": "Create a release before deploying this app instance.", + "overview.noReleaseSourceUnavailable": "The source app was deleted. Existing releases can still be deployed, but there is no release yet.", + "overview.createRelease": "Create release", "overview.sourceApp": "Source app", "overview.sourceAppDeletedDescription": "Historical releases are still deployable, but new releases cannot be generated from the deleted source app. Switch to another source app to continue.", "overview.sourceAppDeletedTitle": "Source app was deleted", @@ -237,6 +249,16 @@ "overview.switchSourceAppHint": "After switching, only newly created releases use the new source app. Historical releases and existing deployments are not changed.", "overview.viewDeployments": "View deployments", "overview.webapp": "WebApp", + "versions.cancelCreate": "Cancel", + "versions.create": "Create", + "versions.createFailed": "Failed to create release.", + "versions.createRelease": "Create release", + "versions.creating": "Creating...", + "versions.emptySourceUnavailable": "No releases yet. The source app was deleted, so new releases cannot be created.", + "versions.emptyWithCreate": "No releases yet. Create the first release before deploying.", + "versions.releaseDescriptionPlaceholder": "Describe this release", + "versions.releaseNamePlaceholder": "Release name", + "versions.sourceAppUnavailable": "The source app was deleted. Existing releases are still deployable, but new releases cannot be created.", "rollback.cancel": "Cancel", "rollback.confirm": "Deploy release", "rollback.currentRelease": "Current release", diff --git a/web/i18n/zh-Hans/deployments.json b/web/i18n/zh-Hans/deployments.json index c9a1916779..f601862beb 100644 --- a/web/i18n/zh-Hans/deployments.json +++ b/web/i18n/zh-Hans/deployments.json @@ -5,6 +5,7 @@ "access.api.disabled": "该实例的 API 接入已关闭。", "access.api.dismissToken": "关闭密钥", "access.api.empty": "请先部署到环境后再签发 API 密钥。", + "access.api.endpoint": "请求地址", "access.api.envPrefix": "env:{{env}}", "access.api.keyList": "API Key 列表", "access.api.newKey": "生成新 Key", @@ -95,7 +96,6 @@ "createModal.appSearchPlaceholder": "搜索应用…", "createModal.cancel": "取消", "createModal.create": "创建", - "createModal.createAndDeploy": "创建并部署", "createModal.createFailed": "创建应用实例失败。", "createModal.description": "从 Studio 选择一个源应用并创建可部署的实例。", "createModal.descriptionLabel": "描述", @@ -108,18 +108,25 @@ "createModal.sourceApp": "源应用(必选)", "createModal.title": "创建应用实例", "deployDrawer.bindingsDisabled": "来自发布预览的解析结果,暂不支持在这里编辑。", + "deployDrawer.bindingOptionsFailed": "加载凭据选项失败。", + "deployDrawer.bindingSelectionHint": "选择本次部署要使用的运行时凭据。", "deployDrawer.cancel": "取消", "deployDrawer.defaultSelect": "选择...", "deployDrawer.deploy": "部署", - "deployDrawer.description": "基于当前应用 YAML 创建一个新的发布版本,并部署到目标环境。", + "deployDrawer.deployFailed": "启动部署失败。", + "deployDrawer.deploying": "部署中...", + "deployDrawer.description": "选择一个发布版本,并部署到目标环境。", "deployDrawer.envVars": "环境变量", "deployDrawer.existingReleaseHint": "将直接部署该已有发布版本,不会创建新的版本。", "deployDrawer.loadingBindings": "解析中...", "deployDrawer.lockedHint": "已锁定至当前环境", + "deployDrawer.missingRequiredBinding": "请选择该必填绑定使用的凭据。", "deployDrawer.modelCreds": "模型凭据", "deployDrawer.needsValidation": "(待验证)", "deployDrawer.newReleaseHint": "将基于当前应用 YAML 创建一个新的发布版本。", "deployDrawer.noBindingRequired": "无需配置", + "deployDrawer.noCredentialCandidates": "没有可用凭据。", + "deployDrawer.noReleaseAvailable": "请先创建发布版本,再部署该应用实例。", "deployDrawer.notFound": "未找到实例。", "deployDrawer.noteLabel": "发布备注(可选)", "deployDrawer.notePlaceholder": "例如:优化引导文案", @@ -129,9 +136,11 @@ "deployDrawer.promoteTitle": "推送发布版本", "deployDrawer.readOnly": "只读", "deployDrawer.releaseLabel": "发布版本", + "deployDrawer.requiredBinding": "必填", "deployDrawer.runtimeCredentials": "运行时凭据", "deployDrawer.secretPlaceholder": "机密值", "deployDrawer.selectEnv": "选择一个环境", + "deployDrawer.selectCredential": "选择凭据", "deployDrawer.selectProviderCred": "选择 {{provider}} 凭据", "deployDrawer.selectProviderKey": "选择 {{provider}} 密钥", "deployDrawer.selectRelease": "选择一个发布版本", @@ -227,6 +236,9 @@ "overview.noAccessConfig": "未配置接入方式。", "overview.notConfigured": "未配置", "overview.notDeployedYet": "尚未部署。", + "overview.noReleaseYet": "请先创建发布版本,再部署该应用实例。", + "overview.noReleaseSourceUnavailable": "源应用已删除。已有发布版本仍可部署,但当前还没有可部署版本。", + "overview.createRelease": "创建版本", "overview.sourceApp": "源应用", "overview.sourceAppDeletedDescription": "历史 Release 仍可继续部署,但无法再基于已删除的源应用生成新 Release。请切换到其他源应用后继续使用。", "overview.sourceAppDeletedTitle": "源应用已被删除", @@ -237,6 +249,16 @@ "overview.switchSourceAppHint": "切换后,仅新建 Release 会使用新的源应用;历史 Release 和现有部署不受影响。", "overview.viewDeployments": "查看部署", "overview.webapp": "WebApp", + "versions.cancelCreate": "取消", + "versions.create": "创建", + "versions.createFailed": "创建发布版本失败。", + "versions.createRelease": "创建版本", + "versions.creating": "创建中...", + "versions.emptySourceUnavailable": "暂无发布版本。源应用已删除,无法创建新版本。", + "versions.emptyWithCreate": "暂无发布版本,请先创建第一个可部署版本。", + "versions.releaseDescriptionPlaceholder": "描述这个版本", + "versions.releaseNamePlaceholder": "版本名称", + "versions.sourceAppUnavailable": "源应用已删除。已有发布版本仍可部署,但无法创建新版本。", "rollback.cancel": "取消", "rollback.confirm": "确认部署", "rollback.currentRelease": "当前发布",