mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
update to the new apis
This commit is contained in:
parent
71b04fd48f
commit
da6fd82b6f
@ -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>())
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@ -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}>
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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!)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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({})
|
||||
|
||||
@ -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 })}>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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],
|
||||
)
|
||||
|
||||
|
||||
@ -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')}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
},
|
||||
}))
|
||||
|
||||
@ -18,4 +18,6 @@ export type AppInfo = {
|
||||
iconBackground?: string
|
||||
iconUrl?: string | null
|
||||
description?: string
|
||||
sourceAppId?: string
|
||||
sourceAppName?: string
|
||||
}
|
||||
|
||||
@ -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() ?? ''
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "部署中",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user