update to the new apis

This commit is contained in:
Stephen Zhou 2026-04-29 15:50:47 +08:00
parent 71b04fd48f
commit da6fd82b6f
No known key found for this signature in database
30 changed files with 928 additions and 943 deletions

View File

@ -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<string, never>
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<string, never>
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<ListAppDeploymentsReply>())
export const createAppInstanceContract = base
.route({
path: '/enterprise/app-instances',
method: 'POST',
})
.input(type<{
body: {
sourceAppId: string
name: string
description?: string
}
}>())
.output(type<CreateAppInstanceReply>())
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<GetDeploymentOverviewReply>())
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<ListEnvironmentDeploymentsReply>())
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<ListDeploymentCandidatesReply>())
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<GetDeploymentPlanReply>())
.output(type<ReleaseRuntimePreviewReply>())
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<CreateReleaseReply>())
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<ListReleaseHistoryReply>())
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<CreateDeploymentReply>())
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<CancelDeploymentReply>())
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<UndeployEnvironmentReply>())
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<GetAccessConfigReply>())
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<GetEnvironmentAccessPolicyReply>())
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<UpdateEnvironmentAccessPolicyReply>())
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<PatchAccessChannelReply>())
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<CreateReleaseReply>())
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<CreateDeploymentReply>())
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<CancelDeploymentReply>())
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<UndeployEnvironmentReply>())
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<RollbackEnvironmentReply>())
export const environmentAPITokensContract = base
.route({
path: '/enterprise/apps/{appId}/environments/{environmentId}/api-keys',
method: 'GET',
})
.input(type<{
params: {
appId: string
environmentId: string
}
}>())
.output(type<ListEnvironmentAPITokensReply>())
.output(type<PatchDeveloperAPIReply>())
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<DeleteEnvironmentAPITokenReply>())
export const appInstanceSettingsContract = base
.route({
path: '/enterprise/app-instances/{appInstanceId}/settings',
method: 'GET',
})
.input(type<{ params: { appInstanceId: string } }>())
.output(type<GetAppInstanceSettingsReply>())
export const updateAppInstanceContract = base
.route({
path: '/enterprise/app-instances/{appInstanceId}',
method: 'PATCH',
})
.input(type<{
params: { appInstanceId: string }
body: {
name: string
description?: string
}
}>())
.output(type<UpdateAppInstanceReply>())
export const deleteAppInstanceContract = base
.route({
path: '/enterprise/app-instances/{appInstanceId}',
method: 'DELETE',
})
.input(type<{ params: { appInstanceId: string } }>())
.output(type<DeleteAppInstanceReply>())

View File

@ -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,

View File

@ -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<string>('')
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 }) => {
<Button variant="secondary" onClick={onClose}>
{t('createModal.cancel')}
</Button>
<Button variant="secondary" disabled={!canCreate} onClick={() => handleCreate(false)}>
<Button variant="secondary" disabled={!canCreate} onClick={() => void handleCreate(false)}>
{t('createModal.create')}
</Button>
<Button variant="primary" disabled={!canCreate} onClick={() => handleCreate(true)}>
<Button variant="primary" disabled={!canCreate} onClick={() => void handleCreate(true)}>
{t('createModal.createAndDeploy')}
</Button>
</div>

View File

@ -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,
})}
/>
)}

View File

@ -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<string, string>, item: CredentialRequirement) {
return values[item.slot] || item.selectedCredentialId || item.options[0]?.id || ''
}
export function envVarValue(values: Record<string, string>, item: EnvVarRequirement) {
return values[item.key] || item.selectedEnvVarId || item.options[0]?.id || ''
}
export function deploymentBindings(
required: RequiredBindings,
modelCredentials: Record<string, string>,
pluginCredentials: Record<string, string>,
envValues: Record<string, string>,
): 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),
})),
}
}

View File

@ -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<DisabledBindingControlProps> = ({ label, placeholder }) => (
<div className="flex items-center gap-2">
<span className="w-20 shrink-0 system-xs-medium text-text-secondary">{label}</span>
<button
type="button"
disabled
className="flex h-8 min-w-0 flex-1 cursor-not-allowed items-center justify-between rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-2 system-sm-medium text-text-quaternary opacity-60"
>
<span className="truncate">{placeholder}</span>
<span className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-quaternary" />
</button>
</div>
)
type DisabledBindingGroupProps = {
label: string
placeholder: string
bindings: RuntimeBindingDisplay[]
isLoading: boolean
}
const DisabledBindingGroup: FC<DisabledBindingGroupProps> = ({ label, placeholder, bindings, isLoading }) => {
if (bindings.length === 0) {
return (
<DisabledBindingControl
label={label}
placeholder={isLoading ? placeholder : '—'}
/>
)
}
return (
<div className="flex flex-col gap-2">
{bindings.map(binding => (
<DisabledBindingControl
key={`${binding.kind}-${runtimeBindingLabel(binding)}-${runtimeBindingValue(binding)}-${binding.valueType ?? ''}`}
label={runtimeBindingLabel(binding)}
placeholder={runtimeBindingValue(binding)}
/>
))}
</div>
)
}
export const DeployForm: FC<DeployFormProps> = ({
appId,
environments,
@ -56,38 +107,31 @@ export const DeployForm: FC<DeployFormProps> = ({
() => 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<string>(
() => 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<string>('')
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<string>('')
const [modelCredentials, setModelCredentials] = useState<Record<string, string>>({})
const [pluginCredentials, setPluginCredentials] = useState<Record<string, string>>({})
const [envValues, setEnvValues] = useState<Record<string, string>>({})
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<DeployFormProps> = ({
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<DeployFormProps> = ({
</div>
<Field label={isPromote ? t('deployDrawer.releaseLabel') : t('deployDrawer.noteLabel')}>
{isPromote && presetRelease
{isPromote && displayedRelease
? (
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between rounded-lg border border-components-panel-border bg-components-panel-bg-blur px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<span className="shrink-0 font-mono system-sm-semibold text-text-primary">{releaseLabel(presetRelease)}</span>
<span className="shrink-0 font-mono system-sm-semibold text-text-primary">{releaseLabel(displayedRelease)}</span>
<span className="shrink-0 system-xs-regular text-text-tertiary">·</span>
<span className="shrink-0 font-mono system-xs-regular text-text-tertiary">{releaseCommit(presetRelease)}</span>
{presetRelease.description && (
<span className="shrink-0 font-mono system-xs-regular text-text-tertiary">{releaseCommit(displayedRelease)}</span>
{displayedRelease.description && (
<>
<span className="shrink-0 system-xs-regular text-text-tertiary">·</span>
<span className="truncate system-xs-regular text-text-secondary">{presetRelease.description}</span>
<span className="truncate system-xs-regular text-text-secondary">{displayedRelease.description}</span>
</>
)}
</div>
<span className="shrink-0 system-xs-regular text-text-quaternary">{presetRelease.createdAt}</span>
<span className="shrink-0 system-xs-regular text-text-quaternary">{displayedRelease.createdAt}</span>
</div>
<span className="system-xs-regular text-text-tertiary">
{t('deployDrawer.existingReleaseHint')}
@ -171,67 +214,36 @@ export const DeployForm: FC<DeployFormProps> = ({
)}
</Field>
{(required.model.length > 0 || required.plugin.length > 0) && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-3">
<div className="system-xs-medium-uppercase text-text-tertiary">{t('deployDrawer.runtimeCredentials')}</div>
{required.model.length > 0 && (
<Field label={t('deployDrawer.modelCreds')}>
<div className="flex flex-col gap-2">
{required.model.map(item => (
<LabeledSelect
key={item.slot}
label={item.label}
value={credentialValue(modelCredentials, item)}
onChange={v => setModelCredentials(prev => ({ ...prev, [item.slot]: v }))}
options={item.options.map(option => ({
value: option.id,
label: option.label,
}))}
placeholder={t('deployDrawer.selectProviderKey', { provider: item.label })}
/>
))}
</div>
</Field>
)}
{required.plugin.length > 0 && (
<Field label={t('deployDrawer.pluginCreds')}>
<div className="flex flex-col gap-2">
{required.plugin.map(item => (
<LabeledSelect
key={item.slot}
label={item.label}
value={credentialValue(pluginCredentials, item)}
onChange={v => setPluginCredentials(prev => ({ ...prev, [item.slot]: v }))}
options={item.options.map(option => ({ value: option.id, label: option.label }))}
placeholder={t('deployDrawer.selectProviderCred', { provider: item.label })}
/>
))}
</div>
</Field>
)}
<span className="system-xs-regular text-text-quaternary">{t('deployDrawer.bindingsDisabled')}</span>
</div>
)}
{required.envVars.length > 0 && (
<Field label={t('deployDrawer.envVars')}>
<div className="flex flex-col gap-2">
{required.envVars.map(v => (
<div key={v.key} className="flex items-center gap-2">
<span className="w-20 shrink-0 system-xs-medium text-text-secondary">{v.label}</span>
<div className="min-w-0 flex-1">
<DeploymentSelect
value={envVarValue(envValues, v)}
onChange={next => setEnvValues(prev => ({ ...prev, [v.key]: next }))}
options={v.options.map(option => ({ value: option.id, label: option.label }))}
placeholder={t('deployDrawer.defaultSelect')}
/>
</div>
</div>
))}
</div>
<Field label={t('deployDrawer.modelCreds')}>
<DisabledBindingGroup
label={t('deployDrawer.modelCreds')}
placeholder={t('deployDrawer.defaultSelect')}
bindings={modelBindings}
isLoading={releasePreview.isFetching}
/>
</Field>
)}
<Field label={t('deployDrawer.pluginCreds')}>
<DisabledBindingGroup
label={t('deployDrawer.pluginCreds')}
placeholder={t('deployDrawer.defaultSelect')}
bindings={pluginBindings}
isLoading={releasePreview.isFetching}
/>
</Field>
<Field label={t('deployDrawer.envVars')}>
<DisabledBindingGroup
label={t('deployDrawer.envVars')}
placeholder={t('deployDrawer.defaultSelect')}
bindings={envVarBindings}
isLoading={releasePreview.isFetching}
/>
</Field>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={onCancel}>

View File

@ -37,17 +37,16 @@ const RollbackModal: FC = () => {
const appData = useDeploymentsStore(state => modal.appId ? state.appData[modal.appId] : undefined)
const closeRollbackModal = useDeploymentsStore(state => state.closeRollbackModal)
const rollbackDeployment = useDeploymentsStore(state => state.rollbackDeployment)
const { appMap } = useSourceApps()
const { appMap, environmentOptions } = useSourceApps()
const currentRow = deployedRows(appData?.environmentDeployments.environmentDeployments)
const currentRow = deployedRows(appData?.environmentDeployments.data)
.find(row => environmentId(row.environment) === modal.environmentId)
const targetRelease = [
...(appData?.candidates.releases ?? []),
...(appData?.releaseHistory.data?.map(row => row.release).filter(release => !!release) ?? []),
...(appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => !!release?.id) ?? []),
].find(release => release?.id === modal.targetReleaseId)
const currentRelease = activeRelease(currentRow)
const environment = currentRow?.environment
?? appData?.candidates.environmentOptions?.find(env => env.id === modal.environmentId)
?? environmentOptions.find(env => env.id === modal.environmentId)
const app = modal.appId ? appMap.get(modal.appId) : undefined
const confirm = () => {

View File

@ -1,9 +1,9 @@
import type {
AccessSubject,
BindingsProto,
ConsoleReleaseSummary,
CreateAppInstanceReply,
GetAccessConfigReply,
GetDeploymentOverviewReply,
ListDeploymentCandidatesReply,
ListEnvironmentDeploymentsReply,
ListReleaseHistoryReply,
} from '@/contract/console/deployments'
@ -18,7 +18,6 @@ export type DeploymentAppData = {
appId: string
overview: GetDeploymentOverviewReply
environmentDeployments: ListEnvironmentDeploymentsReply
candidates: ListDeploymentCandidatesReply
releaseHistory: ListReleaseHistoryReply
accessConfig: GetAccessConfigReply
}
@ -28,19 +27,26 @@ export type CreateDeploymentParams = {
environmentId: string
releaseId?: string
releaseNote?: string
bindings?: BindingsProto
}
const idempotencyKey = (prefix: string) => `${prefix}-${globalThis.crypto?.randomUUID?.() ?? Date.now()}`
export type CreateInstanceParams = {
sourceAppId: string
name: string
description?: string
}
export type UpdateInstanceParams = {
name: string
description?: string
}
export const deploymentAppDataQueryKey = (appId: string) => ['console', 'deployments', 'app-data', appId] as const
export const fetchDeploymentAppData = async (appId: string): Promise<DeploymentAppData> => {
const input = { params: { appId } }
const input = { params: { appInstanceId: appId } }
const [
overview,
environmentDeployments,
candidates,
releaseHistory,
accessConfig,
] = await Promise.all([
@ -52,7 +58,6 @@ export const fetchDeploymentAppData = async (appId: string): Promise<DeploymentA
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
},
}),
consoleClient.deployments.candidates(input),
consoleClient.deployments.releaseHistory({
...input,
query: {
@ -67,7 +72,6 @@ export const fetchDeploymentAppData = async (appId: string): Promise<DeploymentA
appId,
overview,
environmentDeployments,
candidates,
releaseHistory,
accessConfig,
}
@ -87,63 +91,69 @@ export const refreshDeploymentAppData = async (appId: string): Promise<Deploymen
})
}
export const createRelease = async (appId: string, releaseNote?: string): Promise<ConsoleReleaseSummary> => {
const trimmedReleaseNote = releaseNote?.trim()
const response = await consoleClient.deployments.createRelease({
params: {
appInstanceId: appId,
},
body: {
name: trimmedReleaseNote || 'Release',
description: trimmedReleaseNote || undefined,
},
})
if (!response.release)
throw new Error('Create release did not return a release.')
return response.release
}
export const createDeployment = async ({
appId,
environmentId,
releaseId,
releaseNote,
bindings,
}: CreateDeploymentParams) => {
const trimmedReleaseNote = releaseNote?.trim()
let targetReleaseId = releaseId
await consoleClient.deployments.previewRelease({
params: {
appInstanceId: appId,
},
body: {
releaseId: targetReleaseId,
},
})
if (!targetReleaseId) {
const release = await createRelease(appId, releaseNote)
targetReleaseId = release.id
}
if (!targetReleaseId)
throw new Error('Failed to create a deployable release.')
return consoleClient.deployments.createDeployment({
params: {
appId,
environmentId,
appInstanceId: appId,
},
body: {
...(releaseId
? { releaseId }
: { currentApp: { releaseNote: trimmedReleaseNote || undefined } }),
bindings,
idempotencyKey: idempotencyKey('deploy'),
environmentId,
releaseId: targetReleaseId,
},
})
}
export const cancelDeployment = async (appId: string, environmentId: string, deploymentId: string) => {
export const cancelDeployment = async (appId: string, runtimeInstanceId: string) => {
return consoleClient.deployments.cancelDeployment({
params: {
appId,
environmentId,
deploymentId,
},
body: {
idempotencyKey: idempotencyKey('cancel'),
appInstanceId: appId,
runtimeInstanceId,
},
})
}
export const undeployEnvironment = async (appId: string, environmentId: string) => {
export const undeployEnvironment = async (appId: string, runtimeInstanceId: string) => {
return consoleClient.deployments.undeployEnvironment({
params: {
appId,
environmentId,
},
body: {
idempotencyKey: idempotencyKey('undeploy'),
},
})
}
export const rollbackEnvironment = async (appId: string, environmentId: string, targetReleaseId: string) => {
return consoleClient.deployments.rollbackEnvironment({
params: {
appId,
environmentId,
},
body: {
targetReleaseId,
idempotencyKey: idempotencyKey('rollback'),
appInstanceId: appId,
runtimeInstanceId,
},
})
}
@ -151,35 +161,42 @@ export const rollbackEnvironment = async (appId: string, environmentId: string,
export const createApiKey = async (appId: string, environmentId: string, name: string) => {
return consoleClient.deployments.createEnvironmentAPIToken({
params: {
appId,
environmentId,
appInstanceId: appId,
},
body: {
environmentId,
name,
},
})
}
export const deleteApiKey = async (appId: string, environmentId: string, apiKeyId: string) => {
export const deleteApiKey = async (appId: string, apiKeyId: string) => {
return consoleClient.deployments.deleteEnvironmentAPIToken({
params: {
appId,
environmentId,
appInstanceId: appId,
apiKeyId,
},
})
}
export const patchAccessChannel = async (appId: string, channel: string, enabled: boolean, expectedVersion = 0) => {
export const patchAccessChannel = async (appId: string, enabled: boolean) => {
return consoleClient.deployments.patchAccessChannel({
params: {
appId,
channel,
appInstanceId: appId,
},
body: {
enabled,
},
})
}
export const patchDeveloperAPI = async (appId: string, enabled: boolean) => {
return consoleClient.deployments.patchDeveloperAPI({
params: {
appInstanceId: appId,
},
body: {
channel,
enabled,
expectedVersion,
},
})
}
@ -187,24 +204,54 @@ export const patchAccessChannel = async (appId: string, channel: string, enabled
export const updateEnvironmentAccessPolicy = async (
appId: string,
environmentId: string,
channel: string,
enabled: boolean,
accessMode: string,
subjects: AccessSubject[] = [],
expectedVersion = 0,
) => {
return consoleClient.deployments.updateEnvironmentAccessPolicy({
params: {
appId,
appInstanceId: appId,
environmentId,
channel,
},
body: {
channel,
enabled,
accessMode,
subjects,
expectedVersion,
},
})
}
export const createAppInstance = async ({
sourceAppId,
name,
description,
}: CreateInstanceParams): Promise<CreateAppInstanceReply> => {
return consoleClient.deployments.createInstance({
body: {
sourceAppId,
name,
description,
},
})
}
export const updateAppInstance = async (
appId: string,
{ name, description }: UpdateInstanceParams,
) => {
return consoleClient.deployments.updateInstance({
params: {
appInstanceId: appId,
},
body: {
name,
description,
},
})
}
export const deleteAppInstance = async (appId: string) => {
return consoleClient.deployments.deleteInstance({
params: {
appInstanceId: appId,
},
})
}

View File

@ -2,17 +2,12 @@
import type { FC } from 'react'
import type {
APIToken,
ConsoleEnvironmentSummary,
DeveloperAPIKeySummary,
} from '@/contract/console/deployments'
import { useQueries } from '@tanstack/react-query'
import { useMemo } from 'react'
import { consoleQuery } from '@/service/client'
import { useDeploymentsStore } from '../store'
import {
deployedRows,
environmentName,
} from '../utils'
import { AccessChannelsSection } from './access-tab/channels-section'
import { DeveloperApiSection } from './access-tab/developer-api-section'
@ -42,68 +37,38 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
const accessConfig = appData?.accessConfig
const deploymentRows = useMemo(
() => deployedRows(appData?.environmentDeployments.environmentDeployments),
[appData?.environmentDeployments.environmentDeployments],
() => deployedRows(appData?.environmentDeployments.data),
[appData?.environmentDeployments.data],
)
const policies = useMemo(
() => accessConfig?.userAccess?.environmentPolicies ?? [],
[accessConfig?.userAccess?.environmentPolicies],
() => accessConfig?.permissions ?? [],
[accessConfig?.permissions],
)
const deployedEnvs = useMemo(
() => uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.webapp?.rows?.map(row => row.environment) ?? []),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
]),
[accessConfig?.webapp?.rows, deploymentRows, policies],
[accessConfig?.accessChannels?.webappRows, deploymentRows, policies],
)
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
const apiTokenEnvironments = useMemo(
() => deployedEnvs.filter((env): env is ConsoleEnvironmentSummary & { id: string } => Boolean(env.id)),
[deployedEnvs],
)
const apiTokenQueries = useQueries({
queries: apiTokenEnvironments.map(env => consoleQuery.deployments.environmentAPITokens.queryOptions({
input: {
params: {
appId,
environmentId: env.id,
},
},
enabled: apiEnabled,
staleTime: 30 * 1000,
})),
})
const apiTokenRows = apiTokenQueries.flatMap((query, index): DeveloperAPIKeySummary[] => {
const env = apiTokenEnvironments[index]
return query.data?.data?.map((token: APIToken) => ({
...token,
environmentName: token.environmentId ? environmentName(env) : undefined,
})) ?? []
})
const apiKeys = apiTokenQueries.some(query => query.isSuccess)
? apiTokenRows
: accessConfig?.developerApi?.apiKeys ?? []
const refetchApiTokens = async () => {
await Promise.all(apiTokenQueries.map(query => query.refetch()))
}
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
const handleGenerateApiKey = (environmentId: string) => {
void (async () => {
await generateApiKey(appId, environmentId)
await refetchApiTokens()
})()
}
const handleRevokeApiKey = (environmentId: string, apiKeyId: string) => {
void (async () => {
await revokeApiKey(appId, environmentId, apiKeyId)
await refetchApiTokens()
})()
}
const webappRows = accessConfig?.webapp?.rows?.filter(row => row.url) ?? []
const runEnabled = accessConfig?.webapp?.enabled ?? false
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
const visibleCreatedApiToken = createdApiToken?.appId === appId ? createdApiToken : undefined
const webappChannelVersion = policies.find(policy => policy.effectivePolicy?.channel === 'webapp')?.effectivePolicy?.version ?? 0
const cliDomain = getUrlOrigin(accessConfig?.cli?.url)
const webappChannelVersion = 0
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
return (

View File

@ -22,7 +22,8 @@ type ApiKeyRowProps = {
export const ApiKeyRow: FC<ApiKeyRowProps> = ({ apiKey, onRevoke }) => {
const { t } = useTranslation('deployments')
const [copied, setCopied] = useState(false)
const displayValue = apiKey.maskedPrefix || apiKey.id || '—'
const displayValue = apiKey.maskedKey || apiKey.maskedPrefix || apiKey.id || '—'
const environmentLabel = apiKey.environment?.name || apiKey.environmentName || apiKey.environmentId || apiKey.environment?.id
const handleCopy = async () => {
try {
@ -41,7 +42,7 @@ export const ApiKeyRow: FC<ApiKeyRowProps> = ({ apiKey, onRevoke }) => {
<div className="flex min-w-[140px] flex-col">
<span className="system-sm-medium text-text-primary">{apiKey.name || apiKey.id}</span>
<span className="system-xs-regular text-text-tertiary">
{t('access.api.envPrefix', { env: apiKey.environmentName || apiKey.environmentId })}
{t('access.api.envPrefix', { env: environmentLabel })}
</span>
</div>
<div className="flex min-w-0 flex-1 items-center gap-1 rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal pr-1 pl-2">

View File

@ -95,13 +95,14 @@ export const DeveloperApiSection: FC<DeveloperApiSectionProps> = ({
: (
<div className="flex flex-col divide-y divide-divider-subtle">
{apiKeys.map((apiKey) => {
if (!apiKey.id || !apiKey.environmentId)
const environmentId = apiKey.environmentId ?? apiKey.environment?.id
if (!apiKey.id || !environmentId)
return null
return (
<ApiKeyRow
key={apiKey.id}
apiKey={apiKey}
onRevoke={() => onRevoke(apiKey.environmentId!, apiKey.id!)}
onRevoke={() => onRevoke(environmentId, apiKey.id!)}
/>
)
})}

View File

@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import type { AccessSubject, ConsoleEnvironmentSummary, EnvironmentPolicySummary } from '@/contract/console/deployments'
import type { AccessPermission, AccessSubject, ConsoleEnvironmentSummary } from '@/contract/console/deployments'
import { useTranslation } from 'react-i18next'
import { Section } from './common'
import { EnvironmentPermissionRow } from './permissions'
@ -9,7 +9,7 @@ import { EnvironmentPermissionRow } from './permissions'
type AccessPermissionsSectionProps = {
appId: string
environments: ConsoleEnvironmentSummary[]
policies: EnvironmentPolicySummary[]
policies: AccessPermission[]
onSetPolicy: (
appId: string,
environmentId: string,
@ -43,7 +43,7 @@ export const AccessPermissionsSection: FC<AccessPermissionsSectionProps> = ({
: (
<div className="flex flex-col gap-3">
{environments.map((env) => {
const policy = policies.find(item => item.environment?.id === env.id)?.effectivePolicy
const policy = policies.find(item => item.environment?.id === env.id)
return (
<EnvironmentPermissionRow
key={env.id}

View File

@ -3,11 +3,11 @@
import type { FC } from 'react'
import type { AccessPermissionKind } from '../../types'
import type {
AccessPermission,
AccessPolicyDetail,
AccessSubject,
AccessSubjectDisplay,
ConsoleEnvironmentSummary,
EffectivePolicySummary,
} from '@/contract/console/deployments'
import { cn } from '@langgenius/dify-ui/cn'
import {
@ -99,12 +99,14 @@ type SelectableAccessSubject = AccessSubjectDisplay & {
}
function normalizeSubject(subject: AccessSubjectDisplay): SelectableAccessSubject | undefined {
if (!subject.id || !subject.subjectType)
const id = subject.id ?? subject.subjectId
if (!id || !subject.subjectType)
return undefined
return {
...subject,
id: subject.id,
id,
subjectId: subject.subjectId ?? id,
subjectType: subject.subjectType,
}
}
@ -121,6 +123,11 @@ function policySubjects(subjects: SelectableAccessSubject[]): AccessSubject[] {
}
function selectedSubjectsFromPolicy(policy?: AccessPolicyDetail) {
if (policy?.subjects?.length) {
return policy.subjects
.map(normalizeSubject)
.filter((subject): subject is SelectableAccessSubject => Boolean(subject))
}
const selectedOption = policy?.options?.find(option => option.selected)
?? policy?.options?.find(option => option.mode === policy?.accessMode)
return [
@ -186,7 +193,7 @@ const SubjectPicker: FC<SubjectPickerProps> = ({
const subjectsQuery = useQuery(consoleQuery.deployments.searchAccessSubjects.queryOptions({
input: open
? {
params: { appId },
params: { appInstanceId: appId },
query: {
keyword: debouncedKeyword.trim() || undefined,
subjectTypes: ['account', 'group'],
@ -293,7 +300,7 @@ const SubjectPicker: FC<SubjectPickerProps> = ({
type EnvironmentPermissionRowProps = {
appId: string
environment: ConsoleEnvironmentSummary
summaryPolicy?: EffectivePolicySummary
summaryPolicy?: AccessPermission
onSetPolicy: (
appId: string,
environmentId: string,
@ -313,14 +320,12 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
}) => {
const { t } = useTranslation('deployments')
const environmentId = environment.id
const channel = summaryPolicy?.channel ?? 'webapp'
const policyQuery = useQuery(consoleQuery.deployments.environmentAccessPolicy.queryOptions({
input: environmentId
? {
params: {
appId,
appInstanceId: appId,
environmentId,
channel,
},
}
: skipToken,
@ -330,7 +335,7 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
const policyKind = accessModeToPermissionKey(detailPolicy?.accessMode ?? summaryPolicy?.accessMode)
const policyFingerprint = [
detailPolicy?.id ?? 'new',
detailPolicy?.version ?? summaryPolicy?.version ?? 0,
detailPolicy?.version ?? 0,
detailPolicy?.accessMode ?? summaryPolicy?.accessMode ?? '',
].join(':')
const policySelectedSubjects = useMemo(
@ -358,11 +363,11 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
await onSetPolicy(
appId,
environmentId,
detailPolicy?.channel ?? channel,
detailPolicy?.enabled ?? summaryPolicy?.enabled ?? true,
'webapp',
true,
permissionKeyToAccessMode(nextKind),
nextKind === 'specific' ? policySubjects(nextSubjects) : [],
detailPolicy?.version ?? summaryPolicy?.version ?? 0,
detailPolicy?.version ?? 0,
)
await policyQuery.refetch()
setDraft({})

View File

@ -10,6 +10,7 @@ import {
} from '@langgenius/dify-ui/dropdown-menu'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSourceApps } from '../hooks/use-source-apps'
import { useDeploymentsStore } from '../store'
import {
activeRelease,
@ -22,7 +23,6 @@ import {
environmentName,
releaseCommit,
releaseLabel,
targetRelease,
} from '../utils'
import { DeploymentPanel } from './deploy-tab/deployment-panel'
import { DeploymentStatusSummary } from './deploy-tab/deployment-status-summary'
@ -38,14 +38,15 @@ const DeployTab: FC<DeployTabProps> = ({ instanceId: appId }) => {
const appData = useDeploymentsStore(state => state.appData[appId])
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const undeployDeployment = useDeploymentsStore(state => state.undeployDeployment)
const { environmentOptions } = useSourceApps()
const rows = useMemo(
() => deployedRows(appData?.environmentDeployments.environmentDeployments),
[appData?.environmentDeployments.environmentDeployments],
() => deployedRows(appData?.environmentDeployments.data),
[appData?.environmentDeployments.data],
)
const deployedEnvIds = new Set(rows.map(row => environmentId(row.environment)))
const availableEnvs = appData?.candidates.environmentOptions?.filter(env => env.id && !deployedEnvIds.has(env.id)) ?? []
const availableEnvs = environmentOptions.filter(env => env.id && !deployedEnvIds.has(env.id))
const [expanded, setExpanded] = useState<string | null>(() => rows[0] ? environmentId(rows[0].environment) : null)
const toggle = (id: string) => setExpanded(prev => (prev === id ? null : id))
const [deployMenuOpen, setDeployMenuOpen] = useState(false)
@ -131,7 +132,7 @@ const DeployTab: FC<DeployTabProps> = ({ instanceId: appId }) => {
const envId = environmentId(row.environment)
const isExpanded = expanded === envId
const status = deploymentStatus(row)
const release = activeRelease(row) || targetRelease(row)
const release = activeRelease(row)
const actions = (
<div className="flex shrink-0 items-center gap-1" onClick={e => e.stopPropagation()}>
<Button size="small" variant="secondary" onClick={() => openDeployDrawer({ appId, environmentId: envId })}>

View File

@ -13,9 +13,13 @@ import {
environmentMode,
environmentName,
formatDate,
isRuntimeEnvVarBinding,
isRuntimeModelBinding,
isRuntimePluginBinding,
releaseCommit,
releaseLabel,
targetRelease,
runtimeBindingLabel,
runtimeBindingValue,
} from '../../utils'
type InfoBlockProps = {
@ -54,12 +58,12 @@ type DeploymentPanelProps = {
export const DeploymentPanel: FC<DeploymentPanelProps> = ({ row }) => {
const { t } = useTranslation('deployments')
const observed = activeRelease(row)
const pending = targetRelease(row)
const env = row.environment
const observedBindings = row.observedRuntime?.bindings
const pendingBindings = row.pendingDeployment?.bindings
const credentials = [...observedBindings?.credentials ?? [], ...pendingBindings?.credentials ?? []]
const envVars = [...observedBindings?.envVars ?? [], ...pendingBindings?.envVars ?? []]
const endpoints = row.detail?.endpoints
const detailBindings = row.detail?.bindings ?? []
const modelCredentials = detailBindings.filter(isRuntimeModelBinding)
const pluginCredentials = detailBindings.filter(isRuntimePluginBinding)
const envVars = detailBindings.filter(isRuntimeEnvVarBinding)
return (
<div className="border-t border-divider-subtle bg-background-default-subtle px-6 py-4">
@ -67,7 +71,7 @@ export const DeploymentPanel: FC<DeploymentPanelProps> = ({ row }) => {
<span className="system-sm-semibold text-text-primary">
{environmentName(env)}
{' · '}
{releaseLabel(observed || pending)}
{releaseLabel(observed)}
</span>
<ModeBadge mode={environmentMode(env)} />
<HealthBadge health={environmentHealth(env)} />
@ -75,35 +79,44 @@ export const DeploymentPanel: FC<DeploymentPanelProps> = ({ row }) => {
<div className="grid grid-cols-1 gap-x-8 gap-y-4 md:grid-cols-2">
<InfoBlock title={t('deployTab.panel.instanceInfo')}>
<InfoRow label={t('deployTab.panel.deploymentId')} value={deploymentId(row) || '—'} mono />
<InfoRow label={t('deployTab.panel.replicas')} value={row.instance?.replicas != null ? String(row.instance.replicas) : '—'} />
<InfoRow label={t('deployTab.panel.runtimeMode')} value={t(environmentMode(env) === 'isolated' ? 'mode.isolated' : 'mode.shared')} suffix={` / ${environmentBackend(env).toUpperCase()}`} />
<InfoRow label={t('deployTab.panel.runtimeNote')} value={row.instance?.status ?? '—'} />
<InfoRow label={t('deployTab.panel.replicas')} value={row.detail?.replicas != null ? String(row.detail.replicas) : '—'} />
<InfoRow label={t('deployTab.panel.runtimeMode')} value={row.detail?.runtimeMode ?? t(environmentMode(env) === 'isolated' ? 'mode.isolated' : 'mode.shared')} suffix={` / ${environmentBackend(env).toUpperCase()}`} />
<InfoRow label={t('deployTab.panel.runtimeNote')} value={row.detail?.runtimeNote ?? row.status ?? '—'} />
</InfoBlock>
<InfoBlock title={t('deployTab.panel.releaseInfo')}>
<InfoRow label={t('deployTab.panel.release')} value={releaseLabel(observed || pending)} mono />
<InfoRow label={t('deployTab.panel.commit')} value={releaseCommit(observed || pending)} mono />
<InfoRow label={t('deployTab.panel.createdAt')} value={formatDate((observed || pending)?.createdAt)} />
{pending && (
<InfoRow label={t('deployTab.panel.targetRelease')} value={`${releaseLabel(pending)} / ${releaseCommit(pending)}`} mono />
)}
{row.instance?.lastError?.releaseId && (
<InfoRow label={t('deployTab.panel.failedRelease')} value={row.instance.lastError.releaseId} mono />
)}
<InfoRow label={t('deployTab.panel.release')} value={releaseLabel(observed)} mono />
<InfoRow label={t('deployTab.panel.commit')} value={releaseCommit(observed)} mono />
<InfoRow label={t('deployTab.panel.createdAt')} value={formatDate(observed?.createdAt)} />
<InfoRow label={t('deployTab.panel.targetRelease')} value="—" mono />
<InfoRow label={t('deployTab.panel.failedRelease')} value="—" mono />
</InfoBlock>
<InfoBlock title={t('deployTab.panel.endpoints')}>
<InfoRow label={t('deployTab.panel.run')} value={row.observedRuntime?.endpoints?.run ?? '—'} mono />
<InfoRow label={t('deployTab.panel.health')} value={row.observedRuntime?.endpoints?.health ?? '—'} mono />
<InfoRow label={t('deployTab.panel.run')} value={endpoints?.run ?? '—'} mono />
<InfoRow label={t('deployTab.panel.health')} value={endpoints?.health ?? '—'} mono />
</InfoBlock>
{credentials.length > 0 && (
{modelCredentials.length > 0 && (
<InfoBlock title={t('deployTab.panel.modelCreds')}>
{credentials.map(c => (
{modelCredentials.map(c => (
<InfoRow
key={`${c.slot}-${c.displayName}-${c.maskedValue}`}
label={c.slot ?? '—'}
value={c.displayName || c.maskedValue || '—'}
key={`${c.kind}-${c.slot}-${c.label}-${c.displayName}-${c.displayValue}-${c.maskedValue}`}
label={runtimeBindingLabel(c)}
value={runtimeBindingValue(c)}
mono
/>
))}
</InfoBlock>
)}
{pluginCredentials.length > 0 && (
<InfoBlock title={t('deployTab.panel.pluginCreds')}>
{pluginCredentials.map(c => (
<InfoRow
key={`${c.kind}-${c.slot}-${c.label}-${c.displayName}-${c.displayValue}-${c.maskedValue}`}
label={runtimeBindingLabel(c)}
value={runtimeBindingValue(c)}
mono
/>
))}
@ -114,9 +127,9 @@ export const DeploymentPanel: FC<DeploymentPanelProps> = ({ row }) => {
<InfoBlock title={t('deployTab.panel.envVars')}>
{envVars.map(v => (
<InfoRow
key={`${v.slot}-${v.displayName}`}
label={v.slot ?? '—'}
value={v.maskedValue || v.displayName || '—'}
key={`${v.kind}-${v.slot}-${v.label}-${v.displayName}-${v.displayValue}`}
label={runtimeBindingLabel(v)}
value={runtimeBindingValue(v)}
mono
/>
))}
@ -124,9 +137,9 @@ export const DeploymentPanel: FC<DeploymentPanelProps> = ({ row }) => {
)}
</div>
{row.instance?.lastError?.message && (
{row.status?.toLowerCase().includes('fail') && row.detail?.runtimeNote && (
<div className="mt-4 rounded-lg border border-util-colors-red-red-200 bg-util-colors-red-red-50 px-3 py-2 system-xs-regular text-util-colors-red-red-700">
{row.instance.lastError.message}
{row.detail.runtimeNote}
</div>
)}
</div>

View File

@ -7,7 +7,6 @@ import {
activeRelease,
deploymentStatus,
releaseLabel,
targetRelease,
} from '../../utils'
type DeploymentStatusSummaryProps = {
@ -22,7 +21,7 @@ export const DeploymentStatusSummary: FC<DeploymentStatusSummaryProps> = ({ row
return (
<span className="inline-flex items-center gap-1.5 system-sm-medium text-util-colors-blue-blue-700">
<span className="i-ri-loader-4-line h-3.5 w-3.5 animate-spin" />
{t('deployTab.status.deployingRelease', { release: releaseLabel(targetRelease(row) || activeRelease(row)) })}
{t('deployTab.status.deployingRelease', { release: releaseLabel(activeRelease(row)) })}
</span>
)
}

View File

@ -41,7 +41,7 @@ const InstanceDetail: FC<InstanceDetailProps> = ({ instanceId, children }) => {
const detailApps = useMemo(() => app ? [app] : [], [app])
useDeploymentData(detailApps, { enabled: detailApps.length > 0 })
const appDeployments = useMemo(
() => deployedRows(appData[instanceId]?.environmentDeployments.environmentDeployments),
() => deployedRows(appData[instanceId]?.environmentDeployments.data),
[appData, instanceId],
)

View File

@ -10,7 +10,7 @@ import { useRouter } from '@/next/navigation'
import { StatusBadge } from '../components/status-badge'
import { useSourceApps } from '../hooks/use-source-apps'
import { useDeploymentsStore } from '../store'
import { webappUrl } from '../utils'
import { releaseLabel, webappUrl } from '../utils'
type OverviewTabProps = {
instanceId: string
@ -96,9 +96,9 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
const { appMap } = useSourceApps()
const app = appMap.get(instanceId)
const overview = appData?.overview
const overviewApp = overview?.app
const overviewApp = overview?.instance
const deployments = useMemo(
() => overview?.deployments?.filter(row => row.environmentId && row.status?.toLowerCase() !== 'undeployed') ?? [],
() => overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? [],
[overview?.deployments],
)
@ -110,10 +110,9 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
}
const appModeLabel = getAppModeLabel(overviewApp?.mode ?? app.mode, tCommon)
const webappRow = appData?.accessConfig.webapp?.rows?.find(row => row.url)
const webappAccessUrl = webappUrl(webappRow?.url)
const cliUrl = appData?.accessConfig.cli?.url
const apiKeysCount = appData?.accessConfig.developerApi?.apiKeys?.length ?? 0
const webappAccessUrl = webappUrl(overview?.access?.webappUrl)
const cliUrl = overview?.access?.cliUrl
const apiKeysCount = overview?.access?.apiKeyCount ?? appData?.accessConfig.developerApi?.apiKeys?.length ?? 0
return (
<div className="flex flex-col gap-5 p-6">
@ -121,7 +120,7 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
<div className="flex flex-col divide-y divide-divider-subtle">
<InfoRow label={t('overview.name')} value={overviewApp?.name ?? app.name} />
<InfoRow label={t('overview.description')} value={overviewApp?.description ?? app.description ?? t('overview.emptyValue')} />
<InfoRow label={t('overview.sourceApp')} value={app.name} />
<InfoRow label={t('overview.sourceApp')} value={overviewApp?.sourceAppName ?? app.sourceAppName ?? app.name} />
<InfoRow label={t('overview.appMode')} value={appModeLabel} />
</div>
</Section>
@ -150,11 +149,11 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
{deployments.map((row) => {
const status = overviewDeploymentStatus(row.status)
return (
<div key={row.environmentId} className="flex items-center justify-between gap-3 py-2">
<div key={row.environment?.id} className="flex items-center justify-between gap-3 py-2">
<div className="flex min-w-0 flex-col">
<span className="system-sm-medium text-text-primary">{row.environmentName || row.environmentId}</span>
<span className="system-sm-medium text-text-primary">{row.environment?.name || row.environment?.id}</span>
<span className="system-xs-regular text-text-tertiary">
{row.releaseDisplayId || row.releaseId || t('overview.emptyValue')}
{releaseLabel(row.release) || t('overview.emptyValue')}
</span>
</div>
<StatusBadge status={status} />
@ -177,18 +176,18 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
<div className="flex flex-col divide-y divide-divider-subtle">
<AccessOverviewRow
label={t('overview.webapp')}
enabled={overview?.access?.webapp?.enabled ?? false}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={webappAccessUrl || t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.cli')}
enabled={overview?.access?.cli?.enabled ?? false}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={cliUrl ?? t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.api')}
enabled={overview?.access?.api?.enabled ?? false}
hint={overview?.access?.api?.enabled
enabled={overview?.access?.developerApiEnabled ?? false}
hint={overview?.access?.developerApiEnabled
? t('overview.apiKeysCount', { count: apiKeysCount })
: t('overview.notConfigured')}
/>

View File

@ -1,9 +1,24 @@
'use client'
import type { FC } from 'react'
import type { AppInfo } from '../types'
import type { GetAppInstanceSettingsReply } from '@/contract/console/deployments'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { useSourceApps } from '../hooks/use-source-apps'
import { useDeploymentsStore } from '../store'
import { deployedRows } from '../utils'
@ -14,17 +29,67 @@ type SettingsTabProps = {
type SettingsFormProps = {
app: AppInfo
settings?: GetAppInstanceSettingsReply
hasDeployments: boolean
onSave: (patch: Pick<AppInfo, 'name' | 'description'>) => Promise<void>
onDelete: () => Promise<void>
}
const SettingsForm: FC<SettingsFormProps> = ({ app, hasDeployments }) => {
const SettingsForm: FC<SettingsFormProps> = ({ app, settings, hasDeployments, onSave, onDelete }) => {
const { t } = useTranslation('deployments')
const [name, setName] = useState(settings?.name ?? app.name)
const [description, setDescription] = useState(settings?.description ?? app.description ?? '')
const [isSaving, setIsSaving] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const initialName = settings?.name ?? app.name
const initialDescription = settings?.description ?? app.description ?? ''
const canSave = Boolean(name.trim() && (name !== initialName || description !== initialDescription) && !isSaving)
const canDelete = !hasDeployments && Boolean(settings) && settings?.deleteGuard?.canDelete !== false
const handleSave = () => {
if (!canSave)
return
void (async () => {
setIsSaving(true)
try {
await onSave({
name: name.trim(),
description: description.trim() || undefined,
})
toast.success(t('settings.updated'))
}
catch {
toast.error(t('settings.updateFailed'))
}
finally {
setIsSaving(false)
}
})()
}
const handleDelete = () => {
void (async () => {
setIsDeleting(true)
try {
await onDelete()
toast.success(t('settings.deleted'))
}
catch {
toast.error(t('settings.deleteFailed'))
}
finally {
setIsDeleting(false)
setShowDeleteConfirm(false)
}
})()
}
return (
<div className="flex max-w-[640px] flex-col gap-5 p-6">
<div className="flex flex-col gap-3 rounded-xl border border-components-panel-border bg-components-panel-bg p-4">
<div className="system-sm-semibold text-text-primary">{t('settings.general')}</div>
<div className="system-xs-regular text-text-tertiary">{t('settings.readOnly')}</div>
<div className="system-xs-regular text-text-tertiary">{t('settings.descriptionHelp')}</div>
<div className="flex flex-col gap-2">
<label className="system-xs-medium-uppercase text-text-tertiary" htmlFor="settings-name">
{t('settings.name')}
@ -32,10 +97,9 @@ const SettingsForm: FC<SettingsFormProps> = ({ app, hasDeployments }) => {
<input
id="settings-name"
type="text"
value={app.name}
readOnly
disabled
className="flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-[13px] font-medium text-text-secondary outline-hidden placeholder:text-text-quaternary disabled:opacity-70"
value={name}
onChange={e => setName(e.target.value)}
className="flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-[13px] font-medium text-text-secondary outline-hidden placeholder:text-text-quaternary"
/>
</div>
<div className="flex flex-col gap-2">
@ -44,17 +108,23 @@ const SettingsForm: FC<SettingsFormProps> = ({ app, hasDeployments }) => {
</label>
<textarea
id="settings-desc"
value={app.description ?? ''}
readOnly
disabled
className="min-h-[96px] rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 py-2 text-[13px] text-text-secondary outline-hidden placeholder:text-text-quaternary disabled:opacity-70"
value={description}
onChange={e => setDescription(e.target.value)}
className="min-h-[96px] rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 py-2 text-[13px] text-text-secondary outline-hidden placeholder:text-text-quaternary"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" disabled>
<Button
variant="secondary"
disabled={isSaving || (name === initialName && description === initialDescription)}
onClick={() => {
setName(initialName)
setDescription(initialDescription)
}}
>
{t('settings.reset')}
</Button>
<Button variant="primary" disabled>
<Button variant="primary" disabled={!canSave} onClick={handleSave}>
{t('settings.save')}
</Button>
</div>
@ -69,34 +139,81 @@ const SettingsForm: FC<SettingsFormProps> = ({ app, hasDeployments }) => {
<div className="system-xs-regular text-text-tertiary">
{hasDeployments
? t('settings.undeployFirst')
: t('settings.deleteUnsupported')}
: settings?.deleteGuard?.disabledReason || t('settings.safeToDelete')}
</div>
<Button
variant="primary"
tone="destructive"
disabled
disabled={!canDelete || isDeleting}
onClick={() => setShowDeleteConfirm(true)}
>
{t('settings.delete')}
</Button>
</div>
</div>
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
<AlertDialogContent className="w-[520px]">
<div className="flex flex-col gap-3 px-6 pt-6 pb-2">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
{t('settings.deleteConfirmTitle')}
</AlertDialogTitle>
<AlertDialogDescription className="system-md-regular text-text-tertiary">
{t('settings.deleteConfirmDesc', { name: app.name })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton variant="secondary">
{t('createModal.cancel')}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={handleDelete}>
{t('settings.delete')}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
const SettingsTab: FC<SettingsTabProps> = ({ instanceId }) => {
const router = useRouter()
const sourceApps = useDeploymentsStore(state => state.sourceApps)
const appData = useDeploymentsStore(state => state.appData[instanceId])
const updateInstance = useDeploymentsStore(state => state.updateInstance)
const deleteInstance = useDeploymentsStore(state => state.deleteInstance)
const { appMap } = useSourceApps()
const app = sourceApps.find(item => item.id === instanceId) ?? appMap.get(instanceId)
const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({
input: {
params: {
appInstanceId: instanceId,
},
},
staleTime: 30 * 1000,
}))
if (!app)
return null
const hasDeployments = deployedRows(appData?.environmentDeployments.environmentDeployments).length > 0
const formKey = `${app.id}-${app.name}-${app.description ?? ''}`
const hasDeployments = deployedRows(appData?.environmentDeployments.data).length > 0
const formKey = `${app.id}-${settingsQuery.data?.name ?? app.name}-${settingsQuery.data?.description ?? app.description ?? ''}`
return <SettingsForm key={formKey} app={app} hasDeployments={hasDeployments} />
return (
<SettingsForm
key={formKey}
app={app}
settings={settingsQuery.data}
hasDeployments={hasDeployments}
onSave={async (patch) => {
await updateInstance(instanceId, patch)
await settingsQuery.refetch()
}}
onDelete={async () => {
await deleteInstance(instanceId)
router.push('/deployments')
}}
/>
)
}
export default SettingsTab

View File

@ -25,12 +25,12 @@ const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
const { t } = useTranslation('deployments')
const appData = useDeploymentsStore(state => state.appData[appId])
const releaseRows = useMemo(
() => appData?.releaseHistory.data?.filter(row => row.release?.id) ?? [],
() => appData?.releaseHistory.data?.filter(row => (row.release ?? row).id) ?? [],
[appData?.releaseHistory.data],
)
const deploymentRows = useMemo(
() => deployedRows(appData?.environmentDeployments.environmentDeployments),
[appData?.environmentDeployments.environmentDeployments],
() => deployedRows(appData?.environmentDeployments.data),
[appData?.environmentDeployments.data],
)
return (
@ -68,7 +68,7 @@ const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
</div>
{releaseRows.map((row) => {
const release = row.release!
const release = row.release ?? row
const releaseDeployments = getReleaseDeployments(row, deploymentRows)
return (
<div key={release.id} className="border-b border-divider-subtle last:border-b-0">
@ -93,7 +93,7 @@ const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 system-xs-regular text-text-tertiary">
<span>{formatDate(release.createdAt)}</span>
<span aria-hidden>·</span>
<span>{row.createdBy?.displayName ?? '—'}</span>
<span>{row.createdBy?.displayName ?? row.createdBy?.name ?? '—'}</span>
</div>
</div>
<div className="flex shrink-0 justify-end gap-1">
@ -136,7 +136,7 @@ const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
</Tooltip>
</div>
<div className="system-sm-regular text-text-secondary">{formatDate(release.createdAt)}</div>
<div className="system-sm-regular text-text-secondary">{row.createdBy?.displayName ?? '—'}</div>
<div className="system-sm-regular text-text-secondary">{row.createdBy?.displayName ?? row.createdBy?.name ?? '—'}</div>
<div className="flex flex-wrap gap-1">
{releaseDeployments.length === 0
? <span className="system-sm-regular text-text-quaternary"></span>

View File

@ -10,6 +10,7 @@ import {
} from '@langgenius/dify-ui/dropdown-menu'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSourceApps } from '../../hooks/use-source-apps'
import { useDeploymentsStore } from '../../store'
import {
activeRelease,
@ -31,9 +32,10 @@ export const DeployReleaseMenu: FC<DeployReleaseMenuProps> = ({ appId, releaseId
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal)
const [open, setOpen] = useState(false)
const { environmentOptions } = useSourceApps({ enabled: open })
const environments = appData?.candidates.environmentOptions?.filter(env => env.id) ?? []
const deploymentRows = deployedRows(appData?.environmentDeployments.environmentDeployments)
const environments = environmentOptions.filter(env => env.id)
const deploymentRows = deployedRows(appData?.environmentDeployments.data)
return (
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>

View File

@ -1,9 +1,9 @@
import type { DeployedToSummary, EnvironmentDeploymentRow, ReleaseHistoryRow } from '@/contract/console/deployments'
import {
activeRelease,
deploymentStatus,
environmentId,
environmentName,
targetRelease,
} from '../../utils'
export type ReleaseDeploymentState = 'active' | 'deploying' | 'failed'
@ -42,7 +42,7 @@ function dedupeReleaseDeployments(items: ReleaseDeployment[]) {
}
export function getReleaseDeployments(row: ReleaseHistoryRow, deploymentRows: EnvironmentDeploymentRow[]) {
const releaseId = row.release?.id
const releaseId = (row.release ?? row).id
if (!releaseId)
return []
@ -57,21 +57,7 @@ export function getReleaseDeployments(row: ReleaseHistoryRow, deploymentRows: En
items.push({
environmentId: envId,
environmentName: environmentName(deployment.environment),
state: 'active',
})
}
if (targetRelease(deployment)?.id === releaseId) {
items.push({
environmentId: envId,
environmentName: environmentName(deployment.environment),
state: 'deploying',
})
}
if (deployment.instance?.lastError?.releaseId === releaseId) {
items.push({
environmentId: envId,
environmentName: environmentName(deployment.environment),
state: 'failed',
state: releaseDeploymentState(deploymentStatus(deployment)),
})
}
return items

View File

@ -1,6 +1,6 @@
'use client'
import type { AppInfo, AppMode } from '../types'
import type { AppDeploymentSummary, ConsoleAppSummary, EnvironmentOption } from '@/contract/console/deployments'
import type { AppDeploymentSummary, EnvironmentOption } from '@/contract/console/deployments'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo } from 'react'
import { consoleQuery } from '@/service/client'
@ -8,17 +8,19 @@ import { useDeploymentsStore } from '../store'
const MAX_SOURCE_APPS = 100
function toAppInfo(app: ConsoleAppSummary): AppInfo | undefined {
if (!app.id || !app.name)
function toAppInfo(summary: AppDeploymentSummary): AppInfo | undefined {
if (!summary.id || !summary.name)
return undefined
return {
id: app.id,
name: app.name,
mode: (app.mode || 'workflow') as AppMode,
id: summary.id,
name: summary.name,
mode: (summary.mode || 'workflow') as AppMode,
iconType: 'emoji',
icon: app.icon,
description: app.description ?? undefined,
icon: summary.icon,
description: summary.description ?? undefined,
sourceAppId: summary.sourceAppId,
sourceAppName: summary.sourceAppName,
}
}
@ -26,18 +28,20 @@ type UseSourceAppsOptions = {
enabled?: boolean
environmentId?: string
keyword?: string
notDeployed?: boolean
}
export function useSourceApps(options: UseSourceAppsOptions = {}) {
const { enabled = true, environmentId, keyword } = options
const { enabled = true, environmentId, keyword, notDeployed } = options
const seedInstancesFromApps = useDeploymentsStore(state => state.seedInstancesFromApps)
const query = useMemo(() => ({
pageNumber: 1,
resultsPerPage: MAX_SOURCE_APPS,
...(environmentId ? { environmentId } : {}),
...(keyword?.trim() ? { keyword: keyword.trim() } : {}),
}), [environmentId, keyword])
...(notDeployed ? { notDeployed: true } : {}),
...(keyword?.trim() ? { query: keyword.trim() } : {}),
}), [environmentId, keyword, notDeployed])
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
input: { query },
@ -47,7 +51,7 @@ export function useSourceApps(options: UseSourceAppsOptions = {}) {
const apps = useMemo<AppInfo[]>(() => {
return (listQuery.data?.data ?? [])
.map(summary => summary.app ? toAppInfo(summary.app) : undefined)
.map(toAppInfo)
.filter((app): app is AppInfo => Boolean(app))
}, [listQuery.data?.data])
@ -58,14 +62,19 @@ export function useSourceApps(options: UseSourceAppsOptions = {}) {
const summaries = useMemo<Record<string, AppDeploymentSummary>>(() => {
return Object.fromEntries(
(listQuery.data?.data ?? [])
.filter(summary => summary.app?.id)
.map(summary => [summary.app!.id!, summary]),
.filter(summary => summary.id)
.map(summary => [summary.id!, summary]),
)
}, [listQuery.data?.data])
const environmentOptions = useMemo<EnvironmentOption[]>(() => {
return listQuery.data?.environmentOptions ?? []
}, [listQuery.data?.environmentOptions])
return listQuery.data?.filters
?.filter(filter => filter.kind === 'environment' && filter.id)
.map(filter => ({
id: filter.id,
name: filter.name,
})) ?? []
}, [listQuery.data?.filters])
useEffect(() => {
if (!enabled || listQuery.isLoading)

View File

@ -49,6 +49,7 @@ const DeploymentsMain: FC = () => {
environmentOptions,
} = useSourceApps({
environmentId: requestedEnvironmentId,
notDeployed: envFilter === 'not-deployed',
keyword: keywords.trim() || undefined,
})
@ -91,10 +92,8 @@ const DeploymentsMain: FC = () => {
}, [environments, t])
const visibleInstances = useMemo(() => {
return activeFilter === 'not-deployed'
? apps.filter(app => summaries[app.id]?.deployed === false)
: apps
}, [apps, activeFilter, summaries])
return apps
}, [apps])
return (
<>

View File

@ -46,11 +46,11 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
}
const deployments = useMemo(
() => deployedRows(appData?.environmentDeployments.environmentDeployments),
[appData?.environmentDeployments.environmentDeployments],
() => deployedRows(appData?.environmentDeployments.data),
[appData?.environmentDeployments.data],
)
const statusCount = (status: string) =>
summary?.statusCounts?.find(item => item.status === status)?.count ?? 0
summary?.statuses?.find(item => item.status === status)?.count ?? 0
const hasSummary = Boolean(summary)
const failedCount = hasSummary
? statusCount('failed') + statusCount('deploy_failed')
@ -62,7 +62,7 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
? statusCount('ready')
: deployments.filter(row => deploymentStatus(row) === 'ready').length
const envCount = hasSummary
? (summary?.deployed ? failedCount + deployingCount + readyCount : 0)
? failedCount + deployingCount + readyCount
: deployments.length
const lastDeployedAt = useMemo(() => {
@ -71,7 +71,7 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
if (deployments.length === 0)
return null
return deployments.reduce((latest, row) => {
const t = new Date(row.instance?.lastDeployedAt || row.instance?.lastReadyAt || '').getTime()
const t = new Date(row.currentRelease?.createdAt || '').getTime()
return t > latest ? t : latest
}, 0)
}, [deployments, summary?.lastDeployedAt])
@ -113,7 +113,7 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
return status || 'unknown'
}
const statusSummaryTooltip = summary?.statusCounts?.filter(item => item.count && item.status !== 'undeployed') ?? []
const statusSummaryTooltip = summary?.statuses?.filter(item => item.count && item.status !== 'undeployed') ?? []
const statusTooltip = primaryStatus === 'none'
? t('card.tooltip.notDeployed')
: deployments.length > 0
@ -130,7 +130,7 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
<span className="shrink-0 text-text-secondary">
{statusLabel(status)}
{' · '}
{releaseLabel(deployment.observedRuntime?.release || deployment.pendingDeployment?.release)}
{releaseLabel(deployment.currentRelease)}
</span>
</div>
)
@ -225,8 +225,8 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, appData, summary }) =
</Tooltip>
<div className="flex min-w-0 items-center gap-1.5 system-xs-regular text-text-tertiary">
<span aria-hidden className="i-ri-apps-2-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
<span className="truncate" title={app.name}>
{t('card.fromApp', { name: app.name })}
<span className="truncate" title={app.sourceAppName ?? app.name}>
{t('card.fromApp', { name: app.sourceAppName ?? app.name })}
</span>
</div>
</div>

View File

@ -1,16 +1,19 @@
import type { DeploymentAppData } from './data'
import type { AppInfo } from './types'
import type { AccessSubject, APIToken, BindingsProto } from '@/contract/console/deployments'
import type { AccessSubject, APIToken, ConsoleReleaseSummary } from '@/contract/console/deployments'
import { create } from 'zustand'
import {
cancelDeployment,
createApiKey,
createAppInstance,
createDeployment,
deleteApiKey,
deleteAppInstance,
patchAccessChannel,
patchDeveloperAPI,
refreshDeploymentAppData,
rollbackEnvironment,
undeployEnvironment,
updateAppInstance,
updateEnvironmentAccessPolicy,
} from './data'
@ -19,7 +22,6 @@ export type StartDeployParams = {
environmentId: string
releaseId?: string
releaseNote?: string
bindings?: BindingsProto
}
type OpenDeployDrawerParams = {
@ -41,11 +43,16 @@ type CreatedApiToken = Pick<APIToken, 'id' | 'environmentId' | 'maskedPrefix' |
}
export type CreateInstanceParams = {
appId: string
sourceAppId: string
name: string
description?: string
}
export type CreateInstanceResult = {
appInstanceId: string
initialRelease?: ConsoleReleaseSummary
}
type DeploymentsState = {
sourceApps: AppInfo[]
appData: Record<string, DeploymentAppData>
@ -79,10 +86,10 @@ type DeploymentsState = {
applyAppData: (data: DeploymentAppData) => void
refreshAppData: (appId: string) => Promise<void>
createInstance: (params: CreateInstanceParams) => string
updateInstance: (appId: string, patch: Partial<Pick<AppInfo, 'name' | 'description'>>) => void
createInstance: (params: CreateInstanceParams) => Promise<CreateInstanceResult>
updateInstance: (appId: string, patch: Pick<AppInfo, 'name' | 'description'>) => Promise<void>
switchSourceApp: (appId: string, nextAppId: string) => void
deleteInstance: (appId: string) => void
deleteInstance: (appId: string) => Promise<void>
startDeploy: (params: StartDeployParams) => Promise<void>
retryDeploy: (appId: string, environmentId: string, targetReleaseId: string) => Promise<void>
@ -150,48 +157,76 @@ export const useDeploymentsStore = create<DeploymentsState>((set, get) => ({
get().applyAppData(data)
},
createInstance: ({ appId }) => {
createInstance: async ({ sourceAppId, name, description }) => {
const response = await createAppInstance({ sourceAppId, name, description })
if (!response.appInstanceId)
throw new Error('Create app instance did not return an appInstanceId.')
set({ createInstanceModal: { open: false } })
return appId
return {
appInstanceId: response.appInstanceId,
initialRelease: response.initialRelease,
}
},
updateInstance: () => undefined,
updateInstance: async (appId, patch) => {
await updateAppInstance(appId, {
name: patch.name,
description: patch.description,
})
await get().refreshAppData(appId)
set(state => ({
sourceApps: state.sourceApps.map(app => app.id === appId ? { ...app, ...patch } : app),
}))
},
switchSourceApp: () => undefined,
deleteInstance: () => undefined,
deleteInstance: async (appId) => {
await deleteAppInstance(appId)
set((state) => {
const { [appId]: _removed, ...appData } = state.appData
return {
sourceApps: state.sourceApps.filter(app => app.id !== appId),
appData,
}
})
},
startDeploy: async ({ appId, environmentId, releaseId, releaseNote, bindings }) => {
startDeploy: async ({ appId, environmentId, releaseId, releaseNote }) => {
set({ deployDrawer: { open: false } })
await createDeployment({ appId, environmentId, releaseId, releaseNote, bindings })
await createDeployment({ appId, environmentId, releaseId, releaseNote })
await get().refreshAppData(appId)
},
retryDeploy: async (appId, environmentId, targetReleaseId) => {
await rollbackEnvironment(appId, environmentId, targetReleaseId)
await createDeployment({ appId, environmentId, releaseId: targetReleaseId })
await get().refreshAppData(appId)
},
rollbackDeployment: async (appId, environmentId, targetReleaseId) => {
set({ rollbackModal: { open: false } })
await rollbackEnvironment(appId, environmentId, targetReleaseId)
await createDeployment({ appId, environmentId, releaseId: targetReleaseId })
await get().refreshAppData(appId)
},
undeployDeployment: async (appId, environmentId, deploymentId, isDeploying) => {
if (isDeploying && deploymentId)
await cancelDeployment(appId, environmentId, deploymentId)
undeployDeployment: async (appId, _environmentId, runtimeInstanceId, isDeploying) => {
if (!runtimeInstanceId)
return
if (isDeploying)
await cancelDeployment(appId, runtimeInstanceId)
else
await undeployEnvironment(appId, environmentId)
await undeployEnvironment(appId, runtimeInstanceId)
await get().refreshAppData(appId)
},
generateApiKey: async (appId, environmentId) => {
const appData = get().appData[appId]
const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key => key.environmentId === environmentId).length ?? 0
const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key =>
(key.environmentId ?? key.environment?.id) === environmentId,
).length ?? 0
const environmentName = appData
?.environmentDeployments
.environmentDeployments
.data
?.find(row => row.environment?.id === environmentId)
?.environment
?.name ?? 'env'
@ -203,8 +238,8 @@ export const useDeploymentsStore = create<DeploymentsState>((set, get) => ({
createdApiToken: {
id: response.apiToken.id,
appId,
environmentId,
maskedPrefix: response.apiToken.maskedPrefix,
environmentId: response.apiToken.environmentId ?? response.apiToken.environment?.id,
maskedPrefix: response.apiToken.maskedPrefix ?? response.apiToken.maskedKey,
name: response.apiToken.name || label,
token: response.apiToken.token,
},
@ -212,20 +247,23 @@ export const useDeploymentsStore = create<DeploymentsState>((set, get) => ({
}
},
revokeApiKey: async (appId, environmentId, apiKeyId) => {
await deleteApiKey(appId, environmentId, apiKeyId)
revokeApiKey: async (appId, _environmentId, apiKeyId) => {
await deleteApiKey(appId, apiKeyId)
await get().refreshAppData(appId)
},
clearCreatedApiToken: () => set({ createdApiToken: undefined }),
toggleAccessChannel: async (appId, channel, enabled, expectedVersion) => {
await patchAccessChannel(appId, channel, enabled, expectedVersion)
toggleAccessChannel: async (appId, channel, enabled) => {
if (channel === 'api')
await patchDeveloperAPI(appId, enabled)
else
await patchAccessChannel(appId, enabled)
await get().refreshAppData(appId)
},
setEnvironmentAccessPolicy: async (appId, environmentId, channel, enabled, accessMode, subjects, expectedVersion) => {
await updateEnvironmentAccessPolicy(appId, environmentId, channel, enabled, accessMode, subjects, expectedVersion)
setEnvironmentAccessPolicy: async (appId, environmentId, _channel, _enabled, accessMode, subjects) => {
await updateEnvironmentAccessPolicy(appId, environmentId, accessMode, subjects)
await get().refreshAppData(appId)
},
}))

View File

@ -18,4 +18,6 @@ export type AppInfo = {
iconBackground?: string
iconUrl?: string | null
description?: string
sourceAppId?: string
sourceAppName?: string
}

View File

@ -4,6 +4,7 @@ import type {
ConsoleReleaseSummary,
EnvironmentDeploymentRow,
EnvironmentOption,
RuntimeBindingDisplay,
} from '@/contract/console/deployments'
import { PUBLIC_API_PREFIX } from '@/config'
@ -25,7 +26,7 @@ export const environmentMode = (environment?: ConsoleEnvironmentSummary | Enviro
}
export const environmentBackend = (environment?: ConsoleEnvironmentSummary) => {
const runtime = environment?.runtime?.toLowerCase() ?? ''
const runtime = (environment?.backend || environment?.runtime)?.toLowerCase() ?? ''
return runtime.includes('host') ? 'host' : 'k8s'
}
@ -36,9 +37,24 @@ export const environmentHealth = (environment?: ConsoleEnvironmentSummary | Envi
export const releaseId = (release?: ConsoleReleaseSummary) => release?.id ?? ''
export const releaseLabel = (release?: ConsoleReleaseSummary) => release?.displayId || release?.id || '—'
export const releaseLabel = (release?: ConsoleReleaseSummary) => release?.name || release?.displayId || release?.id || '—'
export const releaseCommit = (release?: ConsoleReleaseSummary) => release?.commitId || '—'
export const releaseCommit = (release?: ConsoleReleaseSummary) => release?.shortCommitId || release?.commitId || '—'
export const runtimeBindingLabel = (binding?: RuntimeBindingDisplay) =>
binding?.label || binding?.slot || binding?.kind || '—'
export const runtimeBindingValue = (binding?: RuntimeBindingDisplay) =>
binding?.displayValue || binding?.maskedValue || binding?.displayName || '—'
export const isRuntimeEnvVarBinding = (binding?: RuntimeBindingDisplay) =>
(binding?.kind?.toLowerCase() ?? '').includes('env')
export const isRuntimeModelBinding = (binding?: RuntimeBindingDisplay) =>
(binding?.kind?.toLowerCase() ?? '').includes('model')
export const isRuntimePluginBinding = (binding?: RuntimeBindingDisplay) =>
!isRuntimeEnvVarBinding(binding) && !isRuntimeModelBinding(binding)
const absoluteUrlRegExp = /^[a-z][a-z\d+.-]*:\/\//i
@ -64,30 +80,21 @@ export const webappUrl = (url?: string) => {
}
export const deploymentId = (row?: EnvironmentDeploymentRow) =>
row?.pendingDeployment?.deploymentId || row?.instance?.currentDeploymentId || ''
row?.id || ''
export const activeRelease = (row?: EnvironmentDeploymentRow) => row?.observedRuntime?.release
export const targetRelease = (row?: EnvironmentDeploymentRow) => row?.pendingDeployment?.release
export const failedReleaseId = (row?: EnvironmentDeploymentRow) => row?.instance?.lastError?.releaseId
export const activeRelease = (row?: EnvironmentDeploymentRow) => row?.currentRelease
export const deploymentStatus = (row: EnvironmentDeploymentRow): DeploymentUiStatus => {
if (row.pendingDeployment)
const runtimeStatus = row.status?.toLowerCase() ?? ''
if (runtimeStatus.includes('deploying') || runtimeStatus.includes('pending'))
return 'deploying'
if (row.instance?.lastError)
return 'deploy_failed'
const status = row.instance?.status?.toLowerCase() ?? ''
if (status.includes('deploying') || status.includes('pending'))
return 'deploying'
if (status.includes('fail') || status.includes('error'))
if (runtimeStatus.includes('fail') || runtimeStatus.includes('error'))
return 'deploy_failed'
return 'ready'
}
export const deployedRows = (rows?: EnvironmentDeploymentRow[]) =>
rows?.filter(row => row.environment?.id && (row.instance || row.observedRuntime || row.pendingDeployment)) ?? []
rows?.filter(row => row.environment?.id && (row.id || row.status || row.currentRelease || row.detail)) ?? []
export const accessModeToPermissionKey = (mode?: string): AccessPermissionKind => {
const normalized = mode?.toLowerCase() ?? ''

View File

@ -96,6 +96,7 @@
"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",
"createModal.descriptionPlaceholder": "Describe what this instance is used for",
@ -106,6 +107,7 @@
"createModal.selected": "Selected",
"createModal.sourceApp": "Source app (required)",
"createModal.title": "Create app instance",
"deployDrawer.bindingsDisabled": "Disabled until deployment binding options are available.",
"deployDrawer.cancel": "Cancel",
"deployDrawer.defaultSelect": "Select...",
"deployDrawer.deploy": "Deploy",
@ -238,17 +240,21 @@
"rollback.sourceApp": "Source app",
"rollback.title": "Deploy {{release}}",
"settings.danger": "Danger zone",
"settings.dangerDesc": "Backend-managed deployment instances cannot be deleted from the console yet.",
"settings.dangerDesc": "Deleting this app instance removes deployment metadata after all environments are undeployed.",
"settings.delete": "Delete instance",
"settings.deleteUnsupported": "Instance deletion is not available for backend-managed deployments yet.",
"settings.deleteConfirmDesc": "Delete {{name}}? This cannot be undone.",
"settings.deleteConfirmTitle": "Delete app instance",
"settings.deleteFailed": "Failed to delete instance.",
"settings.deleted": "Instance deleted",
"settings.description": "Description",
"settings.descriptionHelp": "Update metadata for this app instance.",
"settings.general": "General",
"settings.name": "Instance name",
"settings.readOnly": "Instance metadata is read from the source Studio app.",
"settings.reset": "Reset",
"settings.safeToDelete": "No active deployments. Safe to delete.",
"settings.save": "Save changes",
"settings.undeployFirst": "Undeploy from all environments before deleting.",
"settings.updateFailed": "Failed to update instance.",
"settings.updated": "Instance updated",
"status.deployFailed": "Deploy failed",
"status.deploying": "Deploying",

View File

@ -96,6 +96,7 @@
"createModal.cancel": "取消",
"createModal.create": "创建",
"createModal.createAndDeploy": "创建并部署",
"createModal.createFailed": "创建应用实例失败。",
"createModal.description": "从 Studio 选择一个源应用并创建可部署的实例。",
"createModal.descriptionLabel": "描述",
"createModal.descriptionPlaceholder": "描述该实例的用途",
@ -106,6 +107,7 @@
"createModal.selected": "已选择",
"createModal.sourceApp": "源应用(必选)",
"createModal.title": "创建应用实例",
"deployDrawer.bindingsDisabled": "等待后端提供部署绑定选项后启用。",
"deployDrawer.cancel": "取消",
"deployDrawer.defaultSelect": "选择...",
"deployDrawer.deploy": "部署",
@ -238,17 +240,21 @@
"rollback.sourceApp": "源应用",
"rollback.title": "部署 {{release}}",
"settings.danger": "危险区域",
"settings.dangerDesc": "后端托管的部署实例暂不能在控制台删除。",
"settings.dangerDesc": "删除应用实例会在所有环境取消部署后移除部署元数据。",
"settings.delete": "删除实例",
"settings.deleteUnsupported": "后端托管的部署暂不支持删除实例。",
"settings.deleteConfirmDesc": "确定删除 {{name}}?此操作无法撤销。",
"settings.deleteConfirmTitle": "删除应用实例",
"settings.deleteFailed": "删除实例失败。",
"settings.deleted": "实例已删除",
"settings.description": "描述",
"settings.descriptionHelp": "更新该应用实例的元数据。",
"settings.general": "常规",
"settings.name": "实例名称",
"settings.readOnly": "实例元数据来自源 Studio 应用,当前以只读方式展示。",
"settings.reset": "重置",
"settings.safeToDelete": "无活动部署,可安全删除。",
"settings.save": "保存修改",
"settings.undeployFirst": "请先在所有环境取消部署后再删除。",
"settings.updateFailed": "更新实例失败。",
"settings.updated": "实例已更新",
"status.deployFailed": "部署失败",
"status.deploying": "部署中",