This commit is contained in:
Stephen Zhou 2026-05-07 18:39:28 +08:00
parent 64fc1e8281
commit 04124edd70
No known key found for this signature in database
3 changed files with 196 additions and 137 deletions

View File

@ -4,6 +4,7 @@ import type {
AccessPermission,
AccessSubject,
ConsoleEnvironmentSummary,
DeveloperAPIKeySummary,
} from '@/features/deployments/types'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useState } from 'react'
@ -26,44 +27,39 @@ function uniqueEnvironments(environments: (ConsoleEnvironmentSummary | undefined
})
}
export function AccessTab({ instanceId: appId }: {
instanceId: string
}) {
const appInput = { params: { appInstanceId: appId } }
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
type DeveloperApiAccessSectionProps = {
appId: string
apiEnabled: boolean
apiUrl?: string
environments: ConsoleEnvironmentSummary[]
apiKeys: DeveloperAPIKeySummary[]
}
function DeveloperApiAccessSection({
appId,
apiEnabled,
apiUrl,
environments,
apiKeys,
}: DeveloperApiAccessSectionProps) {
const [createdApiToken, setCreatedApiToken] = useState<{
appId: string
token: string
}>()
const generateApiKey = useMutation(consoleQuery.enterprise.appDeploy.createDeveloperApiKey.mutationOptions())
const revokeApiKey = useMutation(consoleQuery.enterprise.appDeploy.deleteDeveloperApiKey.mutationOptions())
const toggleAccessChannel = useMutation(consoleQuery.enterprise.appDeploy.updateAccessChannels.mutationOptions())
const toggleDeveloperAPI = useMutation(consoleQuery.enterprise.appDeploy.updateDeveloperApi.mutationOptions())
const setEnvironmentAccessPolicy = useMutation(consoleQuery.enterprise.appDeploy.updateEnvironmentAccessPolicy.mutationOptions())
const deploymentRows = deployedRows(environmentDeployments?.data)
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
const deployedEnvs = uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
])
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
const createApiKeyLabel = (environmentId: string) => {
function createApiKeyLabel(environmentId: string) {
const existingCount = apiKeys.filter(key =>
key.environment?.id === environmentId,
).length
const name = deployedEnvs.find(env => env.id === environmentId)?.name ?? 'env'
const name = environments.find(env => env.id === environmentId)?.name ?? 'env'
return `${name}-key-${String(existingCount + 1).padStart(3, '0')}`
}
const handleGenerateApiKey = (environmentId: string) => {
function handleGenerateApiKey(environmentId: string) {
generateApiKey.mutate(
{
params: {
@ -82,7 +78,8 @@ export function AccessTab({ instanceId: appId }: {
},
)
}
const handleRevokeApiKey = (_environmentId: string, apiKeyId: string) => {
function handleRevokeApiKey(_environmentId: string, apiKeyId: string) {
revokeApiKey.mutate({
params: {
appInstanceId: appId,
@ -90,7 +87,8 @@ export function AccessTab({ instanceId: appId }: {
},
})
}
const handleCopyApiKey = async (apiKeyId: string) => {
async function handleCopyApiKey(apiKeyId: string) {
const response = await consoleClient.enterprise.appDeploy.revealDeveloperApiKey({
params: {
appInstanceId: appId,
@ -101,6 +99,52 @@ export function AccessTab({ instanceId: appId }: {
throw new Error('Reveal developer API key did not return a token.')
return response.token
}
const visibleCreatedApiToken = createdApiToken?.appId === appId
? createdApiToken.token
: undefined
return (
<DeveloperApiSection
apiEnabled={apiEnabled}
apiUrl={apiUrl}
environments={environments}
apiKeys={apiKeys}
createdToken={visibleCreatedApiToken}
onToggle={enabled => toggleDeveloperAPI.mutate({
params: { appInstanceId: appId },
body: { enabled },
})}
onGenerate={handleGenerateApiKey}
onCopyApiKey={handleCopyApiKey}
onRevoke={handleRevokeApiKey}
onClearCreatedToken={() => setCreatedApiToken(undefined)}
/>
)
}
export function AccessTab({ instanceId: appId }: {
instanceId: string
}) {
const appInput = { params: { appInstanceId: appId } }
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const toggleAccessChannel = useMutation(consoleQuery.enterprise.appDeploy.updateAccessChannels.mutationOptions())
const setEnvironmentAccessPolicy = useMutation(consoleQuery.enterprise.appDeploy.updateEnvironmentAccessPolicy.mutationOptions())
const deploymentRows = deployedRows(environmentDeployments?.data)
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
const deployedEnvs = uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
])
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
const handleSetEnvironmentAccessPolicy = async (
appId: string,
environmentId: string,
@ -120,9 +164,6 @@ export function AccessTab({ instanceId: appId }: {
}
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
const visibleCreatedApiToken = createdApiToken?.appId === appId
? createdApiToken.token
: undefined
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
@ -144,20 +185,12 @@ export function AccessTab({ instanceId: appId }: {
body: { enabled },
})}
/>
<DeveloperApiSection
<DeveloperApiAccessSection
appId={appId}
apiEnabled={apiEnabled}
apiUrl={accessConfig?.developerApi?.apiUrl}
environments={deployedEnvs}
apiKeys={apiKeys}
createdToken={visibleCreatedApiToken}
onToggle={enabled => toggleDeveloperAPI.mutate({
params: { appInstanceId: appId },
body: { enabled },
})}
onGenerate={handleGenerateApiKey}
onCopyApiKey={handleCopyApiKey}
onRevoke={handleRevokeApiKey}
onClearCreatedToken={() => setCreatedApiToken(undefined)}
/>
</div>
)

View File

@ -1,5 +1,6 @@
'use client'
import type { KeyboardEvent } from 'react'
import type { EnvironmentOption } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
@ -32,6 +33,66 @@ import { DeploymentStatusSummary } from './deploy-tab/deployment-status-summary'
const GRID_TEMPLATE = 'lg:grid-cols-[minmax(180px,1fr)_minmax(140px,0.75fr)_minmax(180px,0.85fr)_240px]'
function NewDeploymentMenu({ appInstanceId, availableEnvs }: {
appInstanceId: string
availableEnvs: EnvironmentOption[]
}) {
const { t } = useTranslation('deployments')
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const [open, setOpen] = useState(false)
return (
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
className={cn(
'inline-flex h-8 shrink-0 items-center gap-1 rounded-lg px-3 system-sm-medium',
'border border-components-button-primary-border bg-components-button-primary-bg text-components-button-primary-text',
'hover:bg-components-button-primary-bg-hover',
)}
>
<span className="i-ri-rocket-line h-3.5 w-3.5" />
{t('deployTab.newDeployment')}
<span className="i-ri-arrow-down-s-line h-3.5 w-3.5" />
</DropdownMenuTrigger>
{open && (
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[220px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={() => {
setOpen(false)
openDeployDrawer({ appInstanceId })
}}
>
<span className="system-sm-regular text-text-secondary">{t('deployTab.deployToNewEnv')}</span>
</DropdownMenuItem>
{availableEnvs.length > 0 && (
<>
<div className="px-3 py-1 system-xs-medium-uppercase text-text-quaternary">{t('deployTab.shortcut')}</div>
{availableEnvs.map(env => (
<DropdownMenuItem
key={env.id}
className="gap-2 px-3"
disabled={env.disabled}
onClick={() => {
if (env.disabled)
return
setOpen(false)
openDeployDrawer({ appInstanceId, environmentId: env.id })
}}
>
<span className="system-sm-regular text-text-secondary">
{t('deployTab.deployToEnv', { name: environmentName(env) })}
</span>
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
)}
</DropdownMenu>
)
}
export function DeployTab({ instanceId: appInstanceId }: {
instanceId: string
}) {
@ -64,7 +125,6 @@ export function DeployTab({ instanceId: appInstanceId }: {
return current === id ? null : id
})
}
const [deployMenuOpen, setDeployMenuOpen] = useState(false)
return (
<div className="flex w-full max-w-[960px] flex-col gap-4 p-6">
@ -78,54 +138,7 @@ export function DeployTab({ instanceId: appInstanceId }: {
)
</span>
</div>
<DropdownMenu modal={false} open={deployMenuOpen} onOpenChange={setDeployMenuOpen}>
<DropdownMenuTrigger
className={cn(
'inline-flex h-8 shrink-0 items-center gap-1 rounded-lg px-3 system-sm-medium',
'border border-components-button-primary-border bg-components-button-primary-bg text-components-button-primary-text',
'hover:bg-components-button-primary-bg-hover',
)}
>
<span className="i-ri-rocket-line h-3.5 w-3.5" />
{t('deployTab.newDeployment')}
<span className="i-ri-arrow-down-s-line h-3.5 w-3.5" />
</DropdownMenuTrigger>
{deployMenuOpen && (
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[220px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={() => {
setDeployMenuOpen(false)
openDeployDrawer({ appInstanceId })
}}
>
<span className="system-sm-regular text-text-secondary">{t('deployTab.deployToNewEnv')}</span>
</DropdownMenuItem>
{availableEnvs.length > 0 && (
<>
<div className="px-3 py-1 system-xs-medium-uppercase text-text-quaternary">{t('deployTab.shortcut')}</div>
{availableEnvs.map(env => (
<DropdownMenuItem
key={env.id}
className="gap-2 px-3"
disabled={env.disabled}
onClick={() => {
if (env.disabled)
return
setDeployMenuOpen(false)
openDeployDrawer({ appInstanceId, environmentId: env.id })
}}
>
<span className="system-sm-regular text-text-secondary">
{t('deployTab.deployToEnv', { name: environmentName(env) })}
</span>
</DropdownMenuItem>
))}
</>
)}
</DropdownMenuContent>
)}
</DropdownMenu>
<NewDeploymentMenu appInstanceId={appInstanceId} availableEnvs={availableEnvs} />
</div>
{rows.length === 0

View File

@ -23,37 +23,19 @@ import { getReleaseDeployments } from './versions-tab/release-deployments'
const GRID_TEMPLATE = 'grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1.5fr)_96px]'
export function VersionsTab({ instanceId: appId }: {
instanceId: string
function CreateReleaseControl({ appId, canCreateRelease }: {
appId: string
canCreateRelease: boolean
}) {
const { t } = useTranslation('deployments')
const input = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input,
}))
const { data: releaseHistory } = useQuery(consoleQuery.enterprise.appDeploy.listReleases.queryOptions({
input: {
...input,
query: {
pageNumber: 1,
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
},
},
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input,
}))
const createRelease = useMutation(consoleQuery.enterprise.appDeploy.createRelease.mutationOptions())
const [isCreating, setIsCreating] = useState(false)
const [releaseName, setReleaseName] = useState('')
const [releaseDescription, setReleaseDescription] = useState('')
const releaseRows = releaseHistory?.data?.filter(row => row.id) ?? []
const deploymentRows = deployedRows(environmentDeployments?.data)
const canCreateRelease = overview?.instance?.canCreateRelease ?? true
const trimmedReleaseName = releaseName.trim()
const canSubmitRelease = Boolean(canCreateRelease && trimmedReleaseName && !createRelease.isPending)
const handleCreateRelease = async () => {
async function handleCreateRelease() {
if (!canSubmitRelease)
return
@ -79,33 +61,16 @@ export function VersionsTab({ instanceId: appId }: {
}
return (
<div className="flex w-full max-w-[960px] flex-col gap-4 p-6">
<div className="flex items-center justify-between gap-3">
<div className="system-sm-semibold text-text-primary">
{t('versions.releaseHistory')}
{' '}
<span className="system-sm-regular text-text-tertiary">
(
{releaseRows.length}
)
</span>
</div>
<Button
size="small"
variant="primary"
disabled={!canCreateRelease}
onClick={() => setIsCreating(true)}
>
<span className="i-ri-add-line h-3.5 w-3.5" />
{t('versions.createRelease')}
</Button>
</div>
{!canCreateRelease && (
<div className="rounded-lg border border-divider-subtle bg-background-default-subtle px-3 py-2 system-sm-regular text-text-tertiary">
{t('versions.sourceAppUnavailable')}
</div>
)}
<>
<Button
size="small"
variant="primary"
disabled={!canCreateRelease}
onClick={() => setIsCreating(true)}
>
<span className="i-ri-add-line h-3.5 w-3.5" />
{t('versions.createRelease')}
</Button>
<Dialog open={isCreating} onOpenChange={setIsCreating}>
<DialogContent className="w-[560px] overflow-hidden p-0">
@ -192,6 +157,54 @@ export function VersionsTab({ instanceId: appId }: {
</form>
</DialogContent>
</Dialog>
</>
)
}
export function VersionsTab({ instanceId: appId }: {
instanceId: string
}) {
const { t } = useTranslation('deployments')
const input = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input,
}))
const { data: releaseHistory } = useQuery(consoleQuery.enterprise.appDeploy.listReleases.queryOptions({
input: {
...input,
query: {
pageNumber: 1,
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
},
},
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input,
}))
const releaseRows = releaseHistory?.data?.filter(row => row.id) ?? []
const deploymentRows = deployedRows(environmentDeployments?.data)
const canCreateRelease = overview?.instance?.canCreateRelease ?? true
return (
<div className="flex w-full max-w-[960px] flex-col gap-4 p-6">
<div className="flex items-center justify-between gap-3">
<div className="system-sm-semibold text-text-primary">
{t('versions.releaseHistory')}
{' '}
<span className="system-sm-regular text-text-tertiary">
(
{releaseRows.length}
)
</span>
</div>
<CreateReleaseControl appId={appId} canCreateRelease={canCreateRelease} />
</div>
{!canCreateRelease && (
<div className="rounded-lg border border-divider-subtle bg-background-default-subtle px-3 py-2 system-sm-regular text-text-tertiary">
{t('versions.sourceAppUnavailable')}
</div>
)}
{releaseRows.length === 0
? (