diff --git a/web/contract/console/deployments.ts b/web/contract/console/deployments.ts index 1d780ca6f8..b04ba8e7cd 100644 --- a/web/contract/console/deployments.ts +++ b/web/contract/console/deployments.ts @@ -18,6 +18,7 @@ export type ConsoleEnvironmentSummary = { name?: string description?: string runtime?: string + backend?: string type?: string status?: string tags?: string[] @@ -25,44 +26,19 @@ export type ConsoleEnvironmentSummary = { export type ConsoleReleaseSummary = { id?: string - displayId?: string - status?: string - description?: string - commitId?: string - createdAt?: Timestamp name?: string -} - -export type LastErrorProto = { - phase?: string - code?: string - message?: string - releaseId?: string -} - -export type ConsoleInstanceSummary = { - id?: string - replicas?: number + shortCommitId?: string + createdAt?: Timestamp + displayId?: string + commitId?: string + description?: string status?: string - desiredReleaseId?: string - desiredReleaseDisplayId?: string - observedReleaseId?: string - observedReleaseDisplayId?: string - currentDeploymentId?: string - lastDeployedAt?: Timestamp - lastReadyAt?: Timestamp - lastError?: LastErrorProto } -export type ConsoleActions = { - canDeploy?: boolean - canDeployAnotherRelease?: boolean - canCancel?: boolean - canUndeploy?: boolean - canRollback?: boolean - canViewProgress?: boolean - canViewLogs?: boolean - disabledReason?: string +export type ConsoleUser = { + id?: string + name?: string + displayName?: string } export type ConsoleWarning = { @@ -75,161 +51,121 @@ export type DeploymentStatusCount = { count?: number } +export type AppInstanceFilter = { + id?: string + name?: string + kind?: 'all' | 'environment' | 'not_deployed' | (string & {}) +} + export type AppDeploymentSummary = { - app?: ConsoleAppSummary - statusCounts?: DeploymentStatusCount[] - deployed?: boolean + id?: string + name?: string + description?: string + icon?: string + mode?: string + sourceAppId?: string + sourceAppName?: string + statuses?: DeploymentStatusCount[] lastDeployedAt?: Timestamp | null } +export type Pagination = { + total?: number + page?: number + limit?: number + totalCount?: number + perPage?: number + currentPage?: number + totalPages?: number +} + export type ListAppDeploymentsReply = { + filters?: AppInstanceFilter[] data?: AppDeploymentSummary[] - environmentOptions?: EnvironmentOption[] pagination?: Pagination } +export type AppInstanceOverview = { + id?: string + name?: string + description?: string + sourceAppId?: string + sourceAppName?: string + mode?: string + icon?: string + createdAt?: Timestamp +} + export type DeploymentSummaryRow = { - environmentId?: string - environmentName?: string - releaseId?: string - releaseDisplayId?: string + environment?: ConsoleEnvironmentSummary + release?: ConsoleReleaseSummary status?: string } -export type ChannelSummary = { - enabled?: boolean -} - export type AccessSummary = { - webapp?: ChannelSummary - cli?: ChannelSummary - api?: ChannelSummary - mcp?: ChannelSummary + accessChannelsEnabled?: boolean + webappUrl?: string + cliUrl?: string + developerApiEnabled?: boolean + apiKeyCount?: number } export type GetDeploymentOverviewReply = { - app?: ConsoleAppSummary + instance?: AppInstanceOverview deployments?: DeploymentSummaryRow[] access?: AccessSummary warnings?: ConsoleWarning[] } export type RuntimeBindingDisplay = { + kind?: string + label?: string + displayValue?: string + valueType?: string slot?: string displayName?: string maskedValue?: string } -export type RuntimeBindings = { - credentials?: RuntimeBindingDisplay[] - envVars?: RuntimeBindingDisplay[] -} - export type RuntimeEndpoints = { run?: string health?: string } -export type ObservedRuntime = { - release?: ConsoleReleaseSummary - bindings?: RuntimeBindings +export type RuntimeInstanceDetail = { + deploymentName?: string + replicas?: number + runtimeMode?: string + runtimeNote?: string endpoints?: RuntimeEndpoints -} - -export type PendingDeployment = { - deploymentId?: string - release?: ConsoleReleaseSummary - bindings?: RuntimeBindings + bindings?: RuntimeBindingDisplay[] } export type EnvironmentDeploymentRow = { + id?: string environment?: ConsoleEnvironmentSummary - instance?: ConsoleInstanceSummary - observedRuntime?: ObservedRuntime - pendingDeployment?: PendingDeployment - actions?: ConsoleActions -} - -export type Pagination = { - totalCount?: number - perPage?: number - currentPage?: number - totalPages?: number + status?: string + currentRelease?: ConsoleReleaseSummary + detail?: RuntimeInstanceDetail } export type ListEnvironmentDeploymentsReply = { - environmentDeployments?: EnvironmentDeploymentRow[] + data?: EnvironmentDeploymentRow[] pagination?: Pagination } -export type EnvironmentOption = { - id?: string - name?: string - type?: string - status?: string - description?: string - tags?: string[] +export type EnvironmentOption = ConsoleEnvironmentSummary & { disabled?: boolean disabledReason?: string } -export type ListDeploymentCandidatesReply = { - defaultReleaseId?: string - releases?: ConsoleReleaseSummary[] - environmentOptions?: EnvironmentOption[] -} - -export type CurrentInstanceState = { - instanceId?: string - status?: string - observedReleaseDisplayId?: string -} - -export type ConsoleCredentialOption = { - id?: string - displayName?: string - pluginId?: string - provider?: string -} - -export type ConsoleEnvVarOption = { - id?: string - name?: string - maskedValue?: string - valueType?: string - version?: number -} - -export type DeploymentSlot = { - kind?: string - slot?: string - label?: string - required?: boolean - selectedCredentialId?: string - selectedEnvVarId?: string - credentialOptions?: ConsoleCredentialOption[] - envVarOptions?: ConsoleEnvVarOption[] - missing?: boolean - missingReason?: string -} - -export type DeploymentBlocker = { - code?: string - message?: string -} - -export type GetDeploymentPlanReply = { +export type ReleaseRuntimePreviewReply = { release?: ConsoleReleaseSummary - environment?: ConsoleEnvironmentSummary - currentInstance?: CurrentInstanceState - slots?: DeploymentSlot[] - canDeploy?: boolean - blockers?: DeploymentBlocker[] + bindings?: RuntimeBindingDisplay[] } -export type UserDisplay = { - id?: string - displayName?: string +export type CreateReleaseReply = { + release?: ConsoleReleaseSummary } export type DeployedToSummary = { @@ -238,17 +174,10 @@ export type DeployedToSummary = { instanceStatus?: string } -export type ReleaseHistoryActions = { - canDeploy?: boolean - canViewDetail?: boolean - canDelete?: boolean -} - -export type ReleaseHistoryRow = { - release?: ConsoleReleaseSummary - createdBy?: UserDisplay +export type ReleaseHistoryRow = ConsoleReleaseSummary & { + createdBy?: ConsoleUser deployedTo?: DeployedToSummary[] - actions?: ReleaseHistoryActions + release?: ConsoleReleaseSummary } export type ListReleaseHistoryReply = { @@ -256,59 +185,36 @@ export type ListReleaseHistoryReply = { pagination?: Pagination } -export type EffectivePolicySummary = { - channel?: string - enabled?: boolean - accessMode?: string - label?: string - subjectCount?: number - version?: number -} - -export type EnvironmentPolicySummary = { +export type AccessPermission = { environment?: ConsoleEnvironmentSummary - effectivePolicy?: EffectivePolicySummary -} - -export type UserAccessSummary = { - sharedChannels?: string[] - environmentPolicies?: EnvironmentPolicySummary[] + currentRelease?: ConsoleReleaseSummary + accessMode?: string + accessModeLabel?: string + hint?: string } export type WebAppAccessRow = { environment?: ConsoleEnvironmentSummary url?: string - publicCode?: string - canCopy?: boolean - canShowQrCode?: boolean - canRegenerate?: boolean - createNeeded?: boolean } -export type WebAppAccessSummary = { - supported?: boolean +export type AccessChannelsSummary = { enabled?: boolean - rows?: WebAppAccessRow[] -} - -export type UnsupportedChannelSummary = { - supported?: boolean - statusLabel?: string -} - -export type CliAccessSummary = { - supported?: boolean - enabled?: boolean - statusLabel?: string - url?: string + webappRows?: WebAppAccessRow[] + cli?: { + url?: string + } } export type DeveloperAPIKeySummary = { id?: string + name?: string + environment?: ConsoleEnvironmentSummary environmentId?: string environmentName?: string - name?: string + maskedKey?: string maskedPrefix?: string + token?: string createdAt?: Timestamp } @@ -318,15 +224,14 @@ export type DeveloperAPISummary = { } export type GetAccessConfigReply = { - userAccess?: UserAccessSummary - webapp?: WebAppAccessSummary - mcp?: UnsupportedChannelSummary - cli?: CliAccessSummary + permissions?: AccessPermission[] + accessChannels?: AccessChannelsSummary developerApi?: DeveloperAPISummary } export type AccessSubjectDisplay = { id?: string + subjectId?: string subjectType?: string name?: string avatarUrl?: string @@ -344,11 +249,11 @@ export type AccessPolicyOption = { export type AccessPolicyDetail = { id?: string - channel?: string enabled?: boolean accessMode?: string version?: number options?: AccessPolicyOption[] + subjects?: AccessSubjectDisplay[] } export type GetEnvironmentAccessPolicyReply = { @@ -362,15 +267,10 @@ export type AccessSubject = { export type AccessPolicy = { id?: string - appId?: string + appInstanceId?: string environmentId?: string - scopeType?: string - channel?: string - enabled?: boolean accessMode?: string subjects?: AccessSubject[] - version?: number - subjectCount?: number } export type UpdateEnvironmentAccessPolicyReply = { @@ -382,55 +282,17 @@ export type SearchAccessSubjectsReply = { } export type PatchAccessChannelReply = { - policy?: EffectivePolicySummary + enabled?: boolean } -export type Release = { - id?: string - appId?: string - seq?: number - displayId?: string - status?: string - gateCommitId?: string - dslVersion?: string - description?: string - requiredPluginIds?: string[] - requiredModelSlots?: string[] - requiredEnvVarNames?: string[] - createdAt?: Timestamp - readyAt?: Timestamp - name?: string -} - -export type CreateReleaseReply = { - release?: Release -} - -export type CredentialBindingProto = { - slot?: string - credentialId?: string -} - -export type EnvVarBindingProto = { - slot?: string - envVarId?: string -} - -export type BindingsProto = { - models?: CredentialBindingProto[] - plugins?: CredentialBindingProto[] - envVars?: EnvVarBindingProto[] -} - -export type CreateReleaseFromCurrentApp = { - releaseNote?: string +export type PatchDeveloperAPIReply = { + enabled?: boolean } export type CreateDeploymentReply = { - instanceId?: string + runtimeInstanceId?: string deploymentId?: string status?: string - release?: Release } export type CancelDeploymentReply = { @@ -439,26 +301,10 @@ export type CancelDeploymentReply = { export type UndeployEnvironmentReply = { deploymentId?: string + status?: string } -export type RollbackEnvironmentReply = { - deploymentId?: string -} - -export type APIToken = { - id?: string - appId?: string - environmentId?: string - name?: string - token?: string - maskedPrefix?: string - createdAt?: Timestamp - lastUsedAt?: Timestamp -} - -export type ListEnvironmentAPITokensReply = { - data?: APIToken[] -} +export type APIToken = DeveloperAPIKeySummary export type CreateEnvironmentAPITokenReply = { apiToken?: APIToken @@ -466,36 +312,69 @@ export type CreateEnvironmentAPITokenReply = { export type DeleteEnvironmentAPITokenReply = Record +export type CreateAppInstanceReply = { + appInstanceId?: string + initialRelease?: ConsoleReleaseSummary +} + +export type GetAppInstanceSettingsReply = { + name?: string + description?: string + deleteGuard?: { + canDelete?: boolean + disabledReason?: string + } +} + +export type UpdateAppInstanceReply = GetAppInstanceSettingsReply + +export type DeleteAppInstanceReply = Record + export const listAppDeploymentsContract = base .route({ - path: '/enterprise/deployments', + path: '/enterprise/app-instances', method: 'GET', }) .input(type<{ query?: { environmentId?: string - keyword?: string + notDeployed?: boolean + query?: string pageNumber?: number resultsPerPage?: number } }>()) .output(type()) +export const createAppInstanceContract = base + .route({ + path: '/enterprise/app-instances', + method: 'POST', + }) + .input(type<{ + body: { + sourceAppId: string + name: string + description?: string + } + }>()) + .output(type()) + export const deploymentOverviewContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/overview', + path: '/enterprise/app-instances/{appInstanceId}/overview', method: 'GET', }) - .input(type<{ params: { appId: string } }>()) + .input(type<{ params: { appInstanceId: string } }>()) .output(type()) -export const environmentDeploymentsContract = base +export const runtimeInstancesContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/environment-deployments', + path: '/enterprise/app-instances/{appInstanceId}/runtime-instances', method: 'GET', }) .input(type<{ - params: { appId: string } + params: { appInstanceId: string } query?: { pageNumber?: number resultsPerPage?: number @@ -503,35 +382,40 @@ export const environmentDeploymentsContract = base }>()) .output(type()) -export const deploymentCandidatesContract = base +export const previewReleaseContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/deployment-candidates', - method: 'GET', - }) - .input(type<{ params: { appId: string } }>()) - .output(type()) - -export const deploymentPlanContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/releases/{releaseId}/deployment-plan', - method: 'GET', + path: '/enterprise/app-instances/{appInstanceId}/releases:preview', + method: 'POST', }) .input(type<{ - params: { - appId: string - environmentId: string - releaseId: string + params: { appInstanceId: string } + body: { + releaseId?: string } }>()) - .output(type()) + .output(type()) + +export const createReleaseContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}/releases', + method: 'POST', + }) + .input(type<{ + params: { appInstanceId: string } + body: { + description?: string + name: string + } + }>()) + .output(type()) export const releaseHistoryContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/release-history', + path: '/enterprise/app-instances/{appInstanceId}/releases', method: 'GET', }) .input(type<{ - params: { appId: string } + params: { appInstanceId: string } query?: { pageNumber?: number resultsPerPage?: number @@ -539,56 +423,93 @@ export const releaseHistoryContract = base }>()) .output(type()) +export const createDeploymentContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}/deployments', + method: 'POST', + }) + .input(type<{ + params: { + appInstanceId: string + } + body: { + environmentId: string + releaseId: string + } + }>()) + .output(type()) + +export const cancelDeploymentContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}/deployment:cancel', + method: 'POST', + }) + .input(type<{ + params: { + appInstanceId: string + runtimeInstanceId: string + } + }>()) + .output(type()) + +export const undeployEnvironmentContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}/runtime-instances/{runtimeInstanceId}:undeploy', + method: 'POST', + }) + .input(type<{ + params: { + appInstanceId: string + runtimeInstanceId: string + } + }>()) + .output(type()) + export const accessConfigContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/access-config', + path: '/enterprise/app-instances/{appInstanceId}/access', method: 'GET', }) - .input(type<{ params: { appId: string } }>()) + .input(type<{ params: { appInstanceId: string } }>()) .output(type()) export const environmentAccessPolicyContract = base .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deploy/access-policies/{channel}', + path: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy', method: 'GET', }) .input(type<{ params: { - appId: string + appInstanceId: string environmentId: string - channel: string } }>()) .output(type()) export const updateEnvironmentAccessPolicyContract = base .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deploy/access-policies/{channel}', + path: '/enterprise/app-instances/{appInstanceId}/environments/{environmentId}/access-policy', method: 'PUT', }) .input(type<{ params: { - appId: string + appInstanceId: string environmentId: string - channel: string } body: { - channel: string - enabled: boolean accessMode: string subjects: AccessSubject[] - expectedVersion: number } }>()) .output(type()) export const searchAccessSubjectsContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/access-subjects:search', + path: '/enterprise/app-instances/{appInstanceId}/access-subjects:search', method: 'GET', }) .input(type<{ - params: { appId: string } + params: { appInstanceId: string } query?: { keyword?: string subjectTypes?: string[] @@ -598,130 +519,45 @@ export const searchAccessSubjectsContract = base export const patchAccessChannelContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/access-channels/{channel}', + path: '/enterprise/app-instances/{appInstanceId}/access-channels', method: 'PATCH', }) .input(type<{ params: { - appId: string - channel: string + appInstanceId: string } body: { - channel: string enabled: boolean - expectedVersion: number } }>()) .output(type()) -export const createReleaseContract = base +export const patchDeveloperAPIContract = base .route({ - path: '/enterprise/apps/{appId}/deploy/releases', - method: 'POST', - }) - .input(type<{ - params: { appId: string } - body: { - description?: string - name: string - } - }>()) - .output(type()) - -export const createDeploymentContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deployments', - method: 'POST', + path: '/enterprise/app-instances/{appInstanceId}/developer-api', + method: 'PATCH', }) .input(type<{ params: { - appId: string - environmentId: string + appInstanceId: string } body: { - releaseId?: string - currentApp?: CreateReleaseFromCurrentApp - bindings?: BindingsProto - replicas?: number - idempotencyKey?: string + enabled: boolean } }>()) - .output(type()) - -export const cancelDeploymentContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deployments/{deploymentId}/cancel', - method: 'POST', - }) - .input(type<{ - params: { - appId: string - environmentId: string - deploymentId: string - } - body: { - idempotencyKey?: string - } - }>()) - .output(type()) - -export const undeployEnvironmentContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deployments:undeploy', - method: 'POST', - }) - .input(type<{ - params: { - appId: string - environmentId: string - } - body: { - idempotencyKey?: string - } - }>()) - .output(type()) - -export const rollbackEnvironmentContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/deployments:rollback', - method: 'POST', - }) - .input(type<{ - params: { - appId: string - environmentId: string - } - body: { - targetReleaseId?: string - idempotencyKey?: string - } - }>()) - .output(type()) - -export const environmentAPITokensContract = base - .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/api-keys', - method: 'GET', - }) - .input(type<{ - params: { - appId: string - environmentId: string - } - }>()) - .output(type()) + .output(type()) export const createEnvironmentAPITokenContract = base .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/api-keys', + path: '/enterprise/app-instances/{appInstanceId}/api-keys', method: 'POST', }) .input(type<{ params: { - appId: string - environmentId: string + appInstanceId: string } body: { + environmentId: string name: string } }>()) @@ -729,14 +565,43 @@ export const createEnvironmentAPITokenContract = base export const deleteEnvironmentAPITokenContract = base .route({ - path: '/enterprise/apps/{appId}/environments/{environmentId}/api-keys/{apiKeyId}', + path: '/enterprise/app-instances/{appInstanceId}/api-keys/{apiKeyId}', method: 'DELETE', }) .input(type<{ params: { - appId: string - environmentId: string + appInstanceId: string apiKeyId: string } }>()) .output(type()) + +export const appInstanceSettingsContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}/settings', + method: 'GET', + }) + .input(type<{ params: { appInstanceId: string } }>()) + .output(type()) + +export const updateAppInstanceContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}', + method: 'PATCH', + }) + .input(type<{ + params: { appInstanceId: string } + body: { + name: string + description?: string + } + }>()) + .output(type()) + +export const deleteAppInstanceContract = base + .route({ + path: '/enterprise/app-instances/{appInstanceId}', + method: 'DELETE', + }) + .input(type<{ params: { appInstanceId: string } }>()) + .output(type()) diff --git a/web/contract/router.ts b/web/contract/router.ts index 271f99e0c7..ae4411fb2c 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -4,23 +4,25 @@ import { appDeleteContract, workflowOnlineUsersContract } from './console/apps' import { bindPartnerStackContract, invoicesContract } from './console/billing' import { accessConfigContract, + appInstanceSettingsContract, cancelDeploymentContract, + createAppInstanceContract, createDeploymentContract, createEnvironmentAPITokenContract, createReleaseContract, + deleteAppInstanceContract, deleteEnvironmentAPITokenContract, - deploymentCandidatesContract, deploymentOverviewContract, - deploymentPlanContract, environmentAccessPolicyContract, - environmentAPITokensContract, - environmentDeploymentsContract, listAppDeploymentsContract, patchAccessChannelContract, + patchDeveloperAPIContract, + previewReleaseContract, releaseHistoryContract, - rollbackEnvironmentContract, + runtimeInstancesContract, searchAccessSubjectsContract, undeployEnvironmentContract, + updateAppInstanceContract, updateEnvironmentAccessPolicyContract, } from './console/deployments' import { @@ -114,24 +116,26 @@ export const consoleRouterContract = { }, deployments: { list: listAppDeploymentsContract, + createInstance: createAppInstanceContract, overview: deploymentOverviewContract, - environmentDeployments: environmentDeploymentsContract, - candidates: deploymentCandidatesContract, - deploymentPlan: deploymentPlanContract, + environmentDeployments: runtimeInstancesContract, + previewRelease: previewReleaseContract, releaseHistory: releaseHistoryContract, accessConfig: accessConfigContract, environmentAccessPolicy: environmentAccessPolicyContract, updateEnvironmentAccessPolicy: updateEnvironmentAccessPolicyContract, searchAccessSubjects: searchAccessSubjectsContract, patchAccessChannel: patchAccessChannelContract, + patchDeveloperAPI: patchDeveloperAPIContract, createRelease: createReleaseContract, createDeployment: createDeploymentContract, cancelDeployment: cancelDeploymentContract, undeployEnvironment: undeployEnvironmentContract, - rollbackEnvironment: rollbackEnvironmentContract, - environmentAPITokens: environmentAPITokensContract, createEnvironmentAPIToken: createEnvironmentAPITokenContract, deleteEnvironmentAPIToken: deleteEnvironmentAPITokenContract, + settings: appInstanceSettingsContract, + updateInstance: updateAppInstanceContract, + deleteInstance: deleteAppInstanceContract, }, workflowDraft: { environmentVariables: workflowDraftEnvironmentVariablesContract, diff --git a/web/features/deployments/components/create-instance-modal.tsx b/web/features/deployments/components/create-instance-modal.tsx index 941fdd9323..657805da57 100644 --- a/web/features/deployments/components/create-instance-modal.tsx +++ b/web/features/deployments/components/create-instance-modal.tsx @@ -6,6 +6,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -212,23 +213,37 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { const [appId, setAppId] = useState('') const [name, setName] = useState('') const [description, setDescription] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) const selectedApp = apps.find(a => a.id === appId) - const canCreate = Boolean(appId && name.trim()) + const canCreate = Boolean(appId && name.trim() && !isSubmitting) - const handleCreate = (thenDeploy: boolean) => { + const handleCreate = async (thenDeploy: boolean) => { if (!canCreate) return - const instanceId = createInstance({ - appId, - name: name.trim(), - description: description.trim() || undefined, - }) - if (thenDeploy) { - openDeployDrawer({ appId: instanceId }) - return + + setIsSubmitting(true) + try { + const result = await createInstance({ + sourceAppId: appId, + name: name.trim(), + description: description.trim() || undefined, + }) + if (thenDeploy) { + openDeployDrawer({ + appId: result.appInstanceId, + releaseId: result.initialRelease?.id, + }) + return + } + router.push(`/deployments/${result.appInstanceId}/overview`) + } + catch { + toast.error(t('createModal.createFailed')) + } + finally { + setIsSubmitting(false) } - router.push(`/deployments/${instanceId}/overview`) } return ( @@ -283,10 +298,10 @@ 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 e95806b1b8..cb961a6e58 100644 --- a/web/features/deployments/components/deploy-drawer.tsx +++ b/web/features/deployments/components/deploy-drawer.tsx @@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { deploymentAppDataQueryOptions } from '../data' +import { useSourceApps } from '../hooks/use-source-apps' import { useDeploymentsStore } from '../store' import { DeployForm } from './deploy-drawer/form' @@ -17,8 +18,9 @@ const DeployDrawer: FC = () => { const applyAppData = useDeploymentsStore(state => state.applyAppData) const closeDeployDrawer = useDeploymentsStore(state => state.closeDeployDrawer) const startDeploy = useDeploymentsStore(state => state.startDeploy) - const open = drawer.open + const { environmentOptions } = useSourceApps({ enabled: open }) + const appDataQuery = useQuery({ ...deploymentAppDataQueryOptions(drawerAppId ?? ''), enabled: open && Boolean(drawerAppId) && !storedAppData, @@ -29,9 +31,9 @@ const DeployDrawer: FC = () => { }, [appDataQuery.data, applyAppData]) const appData = storedAppData ?? (appDataQuery.data?.appId === drawerAppId ? appDataQuery.data : undefined) - const environments = appData?.candidates.environmentOptions ?? [] - const releases = appData?.candidates.releases ?? [] - const defaultReleaseId = appData?.candidates.defaultReleaseId + const environments = environmentOptions + const releases = appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => release.id) ?? [] + const defaultReleaseId = releases[0]?.id const formKey = `${drawer.appId ?? 'none'}-${drawer.environmentId ?? 'any'}-${drawer.releaseId ?? 'new'}-${open ? '1' : '0'}` return ( @@ -60,13 +62,12 @@ const DeployDrawer: FC = () => { lockedEnvId={drawer.environmentId} presetReleaseId={drawer.releaseId} onCancel={closeDeployDrawer} - onSubmit={({ environmentId, releaseId, releaseNote, bindings }) => + onSubmit={({ environmentId, releaseId, releaseNote }) => startDeploy({ appId: drawerAppId, environmentId, releaseId, releaseNote, - bindings, })} /> )} diff --git a/web/features/deployments/components/deploy-drawer/bindings.ts b/web/features/deployments/components/deploy-drawer/bindings.ts deleted file mode 100644 index e6f0761cf3..0000000000 --- a/web/features/deployments/components/deploy-drawer/bindings.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { BindingsProto, DeploymentSlot } from '@/contract/console/deployments' - -export type CredentialRequirement = { - slot: string - label: string - required: boolean - selectedCredentialId?: string - options: { id: string, label: string }[] -} - -export type EnvVarRequirement = { - key: string - label: string - required: boolean - selectedEnvVarId?: string - type: 'string' | 'secret' - options: { id: string, label: string }[] -} - -export type RequiredBindings = { - model: CredentialRequirement[] - plugin: CredentialRequirement[] - envVars: EnvVarRequirement[] -} - -function isModelSlot(kind?: string) { - return kind?.toLowerCase().includes('model') -} - -function isEnvVarSlot(kind?: string) { - const normalized = kind?.toLowerCase() ?? '' - return normalized.includes('env') -} - -function isSecretValue(type?: string) { - return type?.toLowerCase().includes('secret') ?? false -} - -export function deriveRequiredBindings(slots: DeploymentSlot[] | undefined): RequiredBindings { - const required: RequiredBindings = { - model: [], - plugin: [], - envVars: [], - } - - slots?.forEach((slot) => { - const slotName = slot.slot || slot.label - if (!slotName) - return - - if (isEnvVarSlot(slot.kind)) { - required.envVars.push({ - key: slotName, - label: slot.label || slotName, - required: slot.required ?? true, - selectedEnvVarId: slot.selectedEnvVarId, - type: isSecretValue(slot.envVarOptions?.[0]?.valueType) ? 'secret' : 'string', - options: slot.envVarOptions - ?.filter(option => option.id) - .map(option => ({ - id: option.id!, - label: `${option.name || option.id}${option.maskedValue ? ` · ${option.maskedValue}` : ''}`, - })) ?? [], - }) - return - } - - const target = isModelSlot(slot.kind) ? required.model : required.plugin - target.push({ - slot: slotName, - label: slot.label || slotName, - required: slot.required ?? true, - selectedCredentialId: slot.selectedCredentialId, - options: slot.credentialOptions - ?.filter(option => option.id) - .map(option => ({ - id: option.id!, - label: option.displayName || option.provider || option.id!, - })) ?? [], - }) - }) - - return required -} - -export function credentialValue(values: Record, item: CredentialRequirement) { - return values[item.slot] || item.selectedCredentialId || item.options[0]?.id || '' -} - -export function envVarValue(values: Record, item: EnvVarRequirement) { - return values[item.key] || item.selectedEnvVarId || item.options[0]?.id || '' -} - -export function deploymentBindings( - required: RequiredBindings, - modelCredentials: Record, - pluginCredentials: Record, - envValues: Record, -): BindingsProto { - return { - models: required.model.map(item => ({ - slot: item.slot, - credentialId: credentialValue(modelCredentials, item), - })), - plugins: required.plugin.map(item => ({ - slot: item.slot, - credentialId: credentialValue(pluginCredentials, item), - })), - envVars: required.envVars.map(item => ({ - slot: item.key, - envVarId: envVarValue(envValues, item), - })), - } -} diff --git a/web/features/deployments/components/deploy-drawer/form.tsx b/web/features/deployments/components/deploy-drawer/form.tsx index 160aecb655..383dff7c8b 100644 --- a/web/features/deployments/components/deploy-drawer/form.tsx +++ b/web/features/deployments/components/deploy-drawer/form.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' -import type { BindingsProto, ConsoleReleaseSummary, EnvironmentOption } from '@/contract/console/deployments' +import type { ConsoleReleaseSummary, EnvironmentOption, RuntimeBindingDisplay } from '@/contract/console/deployments' import { Button } from '@langgenius/dify-ui/button' import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { skipToken, useQuery } from '@tanstack/react-query' @@ -9,25 +9,27 @@ 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, releaseCommit, releaseLabel } from '../../utils' import { - credentialValue, - deploymentBindings, - deriveRequiredBindings, - envVarValue, -} from './bindings' + environmentMode, + environmentName, + isRuntimeEnvVarBinding, + isRuntimeModelBinding, + isRuntimePluginBinding, + releaseCommit, + releaseLabel, + runtimeBindingLabel, + runtimeBindingValue, +} from '../../utils' import { DeploymentSelect, EnvironmentRow, Field, - LabeledSelect, } from './select' export type DeployFormSubmit = { environmentId: string releaseId?: string releaseNote?: string - bindings?: BindingsProto } type DeployFormProps = { @@ -41,6 +43,55 @@ type DeployFormProps = { onSubmit: (params: DeployFormSubmit) => void } +type DisabledBindingControlProps = { + label: string + placeholder: string +} + +const DisabledBindingControl: FC = ({ label, placeholder }) => ( +
+ {label} + +
+) + +type DisabledBindingGroupProps = { + label: string + placeholder: string + bindings: RuntimeBindingDisplay[] + isLoading: boolean +} + +const DisabledBindingGroup: FC = ({ label, placeholder, bindings, isLoading }) => { + if (bindings.length === 0) { + return ( + + ) + } + + return ( +
+ {bindings.map(binding => ( + + ))} +
+ ) +} + export const DeployForm: FC = ({ appId, environments, @@ -56,38 +107,31 @@ export const DeployForm: FC = ({ () => presetReleaseId ? releases.find(r => r.id === presetReleaseId) : undefined, [releases, presetReleaseId], ) - const isPromote = Boolean(presetRelease) + const displayedRelease = presetRelease ?? (presetReleaseId ? { id: presetReleaseId } : undefined) + const isPromote = Boolean(presetReleaseId) const [selectedEnvId, setSelectedEnvId] = useState( () => lockedEnvId ?? environments[0]?.id ?? '', ) const selectedEnvironmentId = selectedEnvId || lockedEnvId || environments[0]?.id || '' - const planReleaseId = presetRelease?.id ?? defaultReleaseId ?? releases[0]?.id - const deploymentPlan = useQuery(consoleQuery.deployments.deploymentPlan.queryOptions({ - input: selectedEnvironmentId && planReleaseId + const [releaseNote, setReleaseNote] = useState('') + const canDeploy = Boolean(selectedEnvironmentId && (!isPromote || displayedRelease?.id || defaultReleaseId)) + const previewReleaseId = isPromote ? displayedRelease?.id ?? defaultReleaseId : undefined + const releasePreview = useQuery(consoleQuery.deployments.previewRelease.queryOptions({ + input: appId && (!isPromote || previewReleaseId) ? { - params: { - appId, - environmentId: selectedEnvironmentId, - releaseId: planReleaseId, + params: { appInstanceId: appId }, + body: { + releaseId: previewReleaseId, }, } : skipToken, + staleTime: 30 * 1000, })) - const required = useMemo(() => deriveRequiredBindings(deploymentPlan.data?.slots), [deploymentPlan.data?.slots]) - const [releaseNote, setReleaseNote] = useState('') - const [modelCredentials, setModelCredentials] = useState>({}) - const [pluginCredentials, setPluginCredentials] = useState>({}) - const [envValues, setEnvValues] = useState>({}) - - const canDeploy = Boolean( - selectedEnvironmentId - && deploymentPlan.data?.canDeploy !== false - && !deploymentPlan.isFetching - && required.model.every(item => !item.required || credentialValue(modelCredentials, item)) - && required.plugin.every(item => !item.required || credentialValue(pluginCredentials, item)) - && required.envVars.every(item => !item.required || envVarValue(envValues, item)), - ) + const previewBindings = releasePreview.data?.bindings ?? [] + const modelBindings = previewBindings.filter(isRuntimeModelBinding) + const pluginBindings = previewBindings.filter(isRuntimePluginBinding) + const envVarBindings = previewBindings.filter(isRuntimeEnvVarBinding) const lockedEnv = lockedEnvId ? environments.find(e => e.id === lockedEnvId) : undefined @@ -97,9 +141,8 @@ export const DeployForm: FC = ({ onSubmit({ environmentId: selectedEnvironmentId, - releaseId: presetRelease?.id, + releaseId: displayedRelease?.id ?? (isPromote ? defaultReleaseId : undefined), releaseNote: isPromote ? undefined : releaseNote, - bindings: deploymentBindings(required, modelCredentials, pluginCredentials, envValues), }) } @@ -115,22 +158,22 @@ export const DeployForm: FC = ({ - {isPromote && presetRelease + {isPromote && displayedRelease ? (
- {releaseLabel(presetRelease)} + {releaseLabel(displayedRelease)} · - {releaseCommit(presetRelease)} - {presetRelease.description && ( + {releaseCommit(displayedRelease)} + {displayedRelease.description && ( <> · - {presetRelease.description} + {displayedRelease.description} )}
- {presetRelease.createdAt} + {displayedRelease.createdAt}
{t('deployDrawer.existingReleaseHint')} @@ -171,67 +214,36 @@ export const DeployForm: FC = ({ )} - {(required.model.length > 0 || required.plugin.length > 0) && ( -
+
+
{t('deployDrawer.runtimeCredentials')}
- {required.model.length > 0 && ( - -
- {required.model.map(item => ( - setModelCredentials(prev => ({ ...prev, [item.slot]: v }))} - options={item.options.map(option => ({ - value: option.id, - label: option.label, - }))} - placeholder={t('deployDrawer.selectProviderKey', { provider: item.label })} - /> - ))} -
-
- )} - - {required.plugin.length > 0 && ( - -
- {required.plugin.map(item => ( - setPluginCredentials(prev => ({ ...prev, [item.slot]: v }))} - options={item.options.map(option => ({ value: option.id, label: option.label }))} - placeholder={t('deployDrawer.selectProviderCred', { provider: item.label })} - /> - ))} -
-
- )} + {t('deployDrawer.bindingsDisabled')}
- )} - - {required.envVars.length > 0 && ( - -
- {required.envVars.map(v => ( -
- {v.label} -
- setEnvValues(prev => ({ ...prev, [v.key]: next }))} - options={v.options.map(option => ({ value: option.id, label: option.label }))} - placeholder={t('deployDrawer.defaultSelect')} - /> -
-
- ))} -
+ + - )} + + + + + + +