diff --git a/web/features/deployments/detail/deployment-sidebar.tsx b/web/features/deployments/detail/deployment-sidebar.tsx
index aab6678c8f..43d805dd76 100644
--- a/web/features/deployments/detail/deployment-sidebar.tsx
+++ b/web/features/deployments/detail/deployment-sidebar.tsx
@@ -53,6 +53,18 @@ function VersionsIcon({ className }: TailwindNavIconProps) {
function VersionsSelectedIcon({ className }: TailwindNavIconProps) {
return
}
+function AccessIcon({ className }: TailwindNavIconProps) {
+ return
+}
+function AccessSelectedIcon({ className }: TailwindNavIconProps) {
+ return
+}
+function ApiIcon({ className }: TailwindNavIconProps) {
+ return
+}
+function ApiSelectedIcon({ className }: TailwindNavIconProps) {
+ return
+}
function SettingsIcon({ className }: TailwindNavIconProps) {
return
}
@@ -64,6 +76,8 @@ const TABS: TabDef[] = [
{ key: 'overview', icon: OverviewIcon, selectedIcon: OverviewSelectedIcon },
{ key: 'deploy', icon: DeployIcon, selectedIcon: DeploySelectedIcon },
{ key: 'releases', icon: VersionsIcon, selectedIcon: VersionsSelectedIcon },
+ { key: 'access', icon: AccessIcon, selectedIcon: AccessSelectedIcon },
+ { key: 'api', icon: ApiIcon, selectedIcon: ApiSelectedIcon },
{ key: 'settings', icon: SettingsIcon, selectedIcon: SettingsSelectedIcon },
]
diff --git a/web/features/deployments/detail/developer-api-tab.tsx b/web/features/deployments/detail/developer-api-tab.tsx
new file mode 100644
index 0000000000..d2bde2d848
--- /dev/null
+++ b/web/features/deployments/detail/developer-api-tab.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import { DeveloperApiSection } from './settings-tab/access/developer-api-section'
+
+export function DeveloperApiTab({ appInstanceId }: {
+ appInstanceId: string
+}) {
+ return (
+
+
+
+ )
+}
diff --git a/web/features/deployments/detail/index.tsx b/web/features/deployments/detail/index.tsx
index 486759ee79..ce8324f920 100644
--- a/web/features/deployments/detail/index.tsx
+++ b/web/features/deployments/detail/index.tsx
@@ -7,6 +7,7 @@ import useDocumentTitle from '@/hooks/use-document-title'
import { useSelectedLayoutSegment } from '@/next/navigation'
import { DeployDrawer } from '../components/deploy-drawer'
import { DeploymentSidebar } from './deployment-sidebar'
+import { DeveloperApiHeaderActions } from './settings-tab/access/developer-api-section'
import { isInstanceDetailTabKey } from './tabs'
export function InstanceDetail({ appInstanceId, children }: {
@@ -27,8 +28,17 @@ export function InstanceDetail({ appInstanceId, children }: {
-
{t(`tabs.${activeTab}.name`)}
-
{t(`tabs.${activeTab}.description`)}
+
+
+
{t(`tabs.${activeTab}.name`)}
+
{t(`tabs.${activeTab}.description`)}
+
+ {activeTab === 'api' && (
+
+
+
+ )}
+
{children}
diff --git a/web/features/deployments/detail/settings-tab.tsx b/web/features/deployments/detail/settings-tab.tsx
index 635fbf1931..50a3f4b926 100644
--- a/web/features/deployments/detail/settings-tab.tsx
+++ b/web/features/deployments/detail/settings-tab.tsx
@@ -22,9 +22,6 @@ import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { isUndeployedDeploymentRow } from '../runtime-status'
import { Section, SectionState } from './common'
-import { AccessChannelsSection } from './settings-tab/access/channels-section'
-import { DeveloperApiSection } from './settings-tab/access/developer-api-section'
-import { AccessPermissionsSection } from './settings-tab/access/permissions-section'
type AppInstanceWithId = AppInstanceBasicInfo & { id: string }
@@ -404,9 +401,6 @@ export function SettingsTab({ appInstanceId }: {
}) {
return (
diff --git a/web/features/deployments/detail/settings-tab/access/api-keys.tsx b/web/features/deployments/detail/settings-tab/access/api-keys.tsx
index bb62b36203..c61bb03de5 100644
--- a/web/features/deployments/detail/settings-tab/access/api-keys.tsx
+++ b/web/features/deployments/detail/settings-tab/access/api-keys.tsx
@@ -38,26 +38,79 @@ function ApiKeyRow({ appInstanceId, apiKey }: {
}
return (
-
-
- {apiKey.name || apiKey.id}
-
- {t('access.api.envPrefix', { env: environmentLabel })}
-
-
-
-
- {displayValue}
+
+ |
+
+ {apiKey.name || apiKey.id}
+ |
+
+
+ {environmentLabel}
+
+ |
+
+
+ |
+
-
+ |
+
+ )
+}
+
+function ApiKeyTableHeader() {
+ const { t } = useTranslation('deployments')
+
+ return (
+
+
+ |
+ {t('access.api.table.name')}
+ |
+
+ {t('access.api.table.environment')}
+ |
+
+ {t('access.api.table.key')}
+ |
+
+ {t('access.api.table.action')}
+ |
+
+
+ )
+}
+
+function ApiKeyTable({ appInstanceId, apiKeys }: {
+ appInstanceId: string
+ apiKeys: DeveloperApiKeyRow[]
+}) {
+ return (
+
+
+
+
+ {apiKeys.map(apiKey => (
+
+ ))}
+
+
)
}
@@ -67,15 +120,7 @@ export function ApiKeyList({ appInstanceId, apiKeys }: {
apiKeys: DeveloperApiKeyRow[]
}) {
return (
-
- {apiKeys.map(apiKey => (
-
- ))}
-
+
)
}
diff --git a/web/features/deployments/detail/settings-tab/access/channels-section.tsx b/web/features/deployments/detail/settings-tab/access/channels-section.tsx
index cd0a4bdbca..7742269ff8 100644
--- a/web/features/deployments/detail/settings-tab/access/channels-section.tsx
+++ b/web/features/deployments/detail/settings-tab/access/channels-section.tsx
@@ -1,5 +1,6 @@
'use client'
+import type { ReactNode } from 'react'
import { Switch, SwitchSkeleton } from '@langgenius/dify-ui/switch'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
@@ -12,8 +13,8 @@ import { CopyPill, EndpointRow } from './common'
import { getUrlOrigin } from './url'
const ACCESS_CHANNEL_SKELETON_SECTIONS = [
- { key: 'webapp', className: 'flex flex-col gap-2' },
- { key: 'cli', className: 'flex flex-col gap-2 border-t border-divider-subtle pt-3' },
+ { key: 'webapp' },
+ { key: 'cli' },
]
function AccessChannelsSwitch({ appInstanceId, checked, disabled }: {
@@ -39,28 +40,55 @@ function AccessChannelsSwitch({ appInstanceId, checked, disabled }: {
function AccessChannelsSkeleton() {
return (
-
+
{ACCESS_CHANNEL_SKELETON_SECTIONS.map(section => (
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
))}
)
}
+function ChannelStatusBadge() {
+ const { t } = useTranslation('deployments')
+
+ return (
+
+ {t('access.channels.followPermission')}
+
+ )
+}
+
+function ChannelInfo({ icon, title, description, status }: {
+ icon: ReactNode
+ title: string
+ description: string
+ status: ReactNode
+}) {
+ return (
+
+
+ {icon}
+
+
+
+ {title}
+ {status}
+
+
{description}
+
+
+ )
+}
+
export function AccessChannelsSection({
appInstanceId,
}: {
@@ -82,7 +110,6 @@ export function AccessChannelsSection({
@@ -101,90 +128,78 @@ export function AccessChannelsSection({
?
{t('common.loadFailed')}
: runEnabled
? (
-
-
-
-
-
- {t('access.runAccess.webapp')}
-
-
- {t('access.channels.followPermission')}
-
-
-
- {t('access.runAccess.webappDesc')}
-
- {webappRows.length > 0
- ? (
-
- {webappRows.map((row) => {
- const endpointUrl = webappUrl(row.url)
+
+
+
}
+ title={t('access.runAccess.webapp')}
+ description={t('access.runAccess.webappDesc')}
+ status={
}
+ />
+ {webappRows.length > 0
+ ? (
+
+ {webappRows.map((row) => {
+ const endpointUrl = webappUrl(row.url)
- return (
-
- )
- })}
-
- )
- : (
-
- {t('access.runAccess.webappEmpty')}
-
- )}
-
-
-
-
- {t('access.cli.title')}
-
-
- {t('access.channels.followPermission')}
-
-
-
- {t('access.cli.description')}
-
- {cliDomain
- ? (
-
- )
- : (
-
- {t('access.cli.empty')}
-
- )}
-
+ return (
+
+ )
+ })}
+
+ )
+ : (
+
+ {t('access.runAccess.webappEmpty')}
+
+ )}
+
+
+
}
+ title={t('access.cli.title')}
+ description={t('access.cli.description')}
+ status={
}
+ />
+ {cliDomain
+ ? (
+
+ )
+ : (
+
+ {t('access.cli.empty')}
+
+ )}
)
diff --git a/web/features/deployments/detail/settings-tab/access/developer-api-section.tsx b/web/features/deployments/detail/settings-tab/access/developer-api-section.tsx
index 6ca0931b00..957ac908e2 100644
--- a/web/features/deployments/detail/settings-tab/access/developer-api-section.tsx
+++ b/web/features/deployments/detail/settings-tab/access/developer-api-section.tsx
@@ -6,11 +6,11 @@ import type {
} from '@dify/contracts/enterprise/types.gen'
import { Switch, SwitchSkeleton } from '@langgenius/dify-ui/switch'
import { useMutation, useQuery } from '@tanstack/react-query'
-import { useState } from 'react'
+import { atom, useAtom, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { consoleQuery } from '@/service/client'
-import { Section, SectionState } from '../../common'
+import { SectionState } from '../../common'
import { ApiKeyGenerateMenu, ApiKeyList } from './api-keys'
import { CopyPill } from './common'
@@ -19,6 +19,8 @@ type CreatedApiToken = {
token: string
}
+const createdApiTokenAtom = atom
(undefined)
+
const DEVELOPER_API_KEY_SKELETON_KEYS = ['primary-key', 'secondary-key']
function permissionEnvironment(row: EnvironmentAccessRow): AppDeployEnvironment | undefined {
@@ -46,6 +48,50 @@ function DeveloperApiSwitch({ appInstanceId, checked, disabled }: {
)
}
+export function DeveloperApiHeaderActions({ appInstanceId }: {
+ appInstanceId: string
+}) {
+ const setCreatedApiToken = useSetAtom(createdApiTokenAtom)
+ const accessConfigQuery = useQuery(consoleQuery.enterprise.appDeployAccessService.getAppInstanceAccess.queryOptions({
+ input: {
+ params: { appInstanceId },
+ },
+ }))
+ const accessConfig = accessConfigQuery.data
+ const apiEnabled = accessConfig?.developerApi?.enabled ?? false
+ const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
+ const environments = accessConfig?.permissions
+ ?.map(permissionEnvironment)
+ .filter((environment): environment is AppDeployEnvironment => Boolean(environment)) ?? []
+
+ if (accessConfigQuery.isLoading) {
+ return (
+
+
+
+
+ )
+ }
+
+ return (
+
+ {apiEnabled && (
+
setCreatedApiToken({ appInstanceId, token })}
+ />
+ )}
+
+
+ )
+}
+
function CreatedApiTokenCard({ token, onDismiss }: {
token: string
onDismiss: () => void
@@ -82,18 +128,18 @@ function CreatedApiTokenCard({ token, onDismiss }: {
function DeveloperApiSkeleton() {
return (
-
+
-
+
-
+
{DEVELOPER_API_KEY_SKELETON_KEYS.map(key => (
-
+
@@ -112,7 +158,7 @@ export function DeveloperApiSection({
appInstanceId: string
}) {
const { t } = useTranslation('deployments')
- const [createdApiToken, setCreatedApiToken] = useState
()
+ const [createdApiToken, setCreatedApiToken] = useAtom(createdApiTokenAtom)
const accessConfigQuery = useQuery(consoleQuery.enterprise.appDeployAccessService.getAppInstanceAccess.queryOptions({
input: {
params: { appInstanceId },
@@ -130,51 +176,20 @@ export function DeveloperApiSection({
: undefined
return (
-
- : (
-
- )
- )}
- >
+ <>
{accessConfigQuery.isLoading
?
: accessConfigQuery.isError
? {t('common.loadFailed')}
: apiEnabled
? (
-
+
{apiUrl && (
)}
-
-
-
- {t('access.api.backendTitle')}
-
-
- {t('access.api.keyList')}
-
-
-
setCreatedApiToken({ appInstanceId, token })}
- />
-
{visibleCreatedApiToken && (
)}
-
+ >
)
}
diff --git a/web/features/deployments/detail/settings-tab/access/permissions-section.tsx b/web/features/deployments/detail/settings-tab/access/permissions-section.tsx
index c4b2617ec4..7e79fd6e62 100644
--- a/web/features/deployments/detail/settings-tab/access/permissions-section.tsx
+++ b/web/features/deployments/detail/settings-tab/access/permissions-section.tsx
@@ -20,11 +20,15 @@ function hasEnvironment(row: EnvironmentAccessRow): row is EnvironmentAccessRow
function AccessPermissionsSkeleton() {
return (
-
+
{ACCESS_PERMISSIONS_SKELETON_KEYS.map(key => (
-
-
-
+
+
+
+
))}
@@ -49,7 +53,7 @@ export function AccessPermissionsSection({
{accessConfigQuery.isLoading
?
@@ -62,7 +66,7 @@ export function AccessPermissionsSection({
)
: (
-
+
{permissionRows.map(row => (
-
- {environmentName(environment)}
-
+
+
+
+ {t('access.permissions.col.environment')}
+
+
+
+
+ {environmentName(environment)}
+
+
+
+
+ {t('access.permissions.col.permission')}
+
- {permissionKind === 'specific' && (
-
-
- {subjects.length === 0 && (
-
- {t('access.members.emptySelection')}
-
- )}
+
+
+ {t('access.permissions.col.subjects')}
- )}
+ {permissionKind === 'specific'
+ ? (
+ <>
+
+ {subjects.length === 0 && (
+
+ {t('access.members.emptySelection')}
+
+ )}
+ >
+ )
+ : (
+
+ {t(`access.permission.${permissionKind}Desc`)}
+
+ )}
+
)
}
diff --git a/web/features/deployments/detail/tabs.ts b/web/features/deployments/detail/tabs.ts
index d0ec75d63b..304a4919d9 100644
--- a/web/features/deployments/detail/tabs.ts
+++ b/web/features/deployments/detail/tabs.ts
@@ -1,4 +1,4 @@
-export const INSTANCE_DETAIL_TAB_KEYS = ['overview', 'deploy', 'releases', 'settings'] as const
+export const INSTANCE_DETAIL_TAB_KEYS = ['overview', 'deploy', 'releases', 'access', 'api', 'settings'] as const
export type InstanceDetailTabKey = typeof INSTANCE_DETAIL_TAB_KEYS[number]
diff --git a/web/features/deployments/list/instance-card.tsx b/web/features/deployments/list/instance-card.tsx
index ed23d68799..94a7a9750f 100644
--- a/web/features/deployments/list/instance-card.tsx
+++ b/web/features/deployments/list/instance-card.tsx
@@ -244,26 +244,29 @@ function DeploymentAccessLinks({ appInstanceId, access, isLoading }: {
const links = [
access?.accessChannelsEnabled && access.webappUrl
? {
- href: getInstanceTabHref(appInstanceId, 'overview'),
+ key: 'webapp',
+ href: getInstanceTabHref(appInstanceId, 'access'),
label: t('card.access.webApp'),
icon: 'i-ri-global-line',
}
: undefined,
access?.accessChannelsEnabled && access.cliUrl
? {
- href: getInstanceTabHref(appInstanceId, 'deploy'),
+ key: 'cli',
+ href: getInstanceTabHref(appInstanceId, 'access'),
label: t('card.access.cli'),
icon: 'i-ri-terminal-box-line',
}
: undefined,
access?.developerApiEnabled && access.apiUrl
? {
- href: getInstanceTabHref(appInstanceId, 'settings'),
+ key: 'api',
+ href: getInstanceTabHref(appInstanceId, 'api'),
label: t('card.access.api'),
icon: 'i-ri-code-s-slash-line',
}
: undefined,
- ].filter((link): link is { href: string, label: string, icon: string } => Boolean(link))
+ ].filter((link): link is { key: string, href: string, label: string, icon: string } => Boolean(link))
if (links.length === 0)
return
@@ -271,7 +274,7 @@ function DeploymentAccessLinks({ appInstanceId, access, isLoading }: {
return (
{links.map(link => (
-
+
)
: (
-
- {description || t('card.noDescription')}
-
+ description
+ ? (
+
+ {description}
+
+ )
+ :
)}
diff --git a/web/i18n/en-US/deployments.json b/web/i18n/en-US/deployments.json
index 53fcc50da7..e5d23566c9 100644
--- a/web/i18n/en-US/deployments.json
+++ b/web/i18n/en-US/deployments.json
@@ -14,7 +14,14 @@
"access.api.newTokenLabel": "Token",
"access.api.newTokenTitle": "API key created",
"access.api.noKeys": "No API keys yet. Create one to start calling the API.",
+ "access.api.table.action": "Action",
+ "access.api.table.environment": "Environment",
+ "access.api.table.key": "API key",
+ "access.api.table.name": "Name",
"access.api.title": "API",
+ "access.channels.col.channel": "Channel",
+ "access.channels.col.endpoint": "Entry point",
+ "access.channels.col.status": "Status",
"access.channels.description": "WebApp and CLI entry points use the access permissions above.",
"access.channels.disabled": "Access channels are turned off for this instance.",
"access.channels.followPermission": "Follows permissions",
@@ -55,6 +62,9 @@
"access.permission.specificDesc": "Pick groups or individual members",
"access.permission.specificUnavailable": "Specific member selection is disabled until real workspace subjects are connected.",
"access.permission.updateFailed": "Failed to update access policy.",
+ "access.permissions.col.environment": "Environment",
+ "access.permissions.col.permission": "Access",
+ "access.permissions.col.subjects": "Allowed subjects",
"access.permissions.description": "Configure who can access this instance in each deployed environment.",
"access.permissions.title": "Access permissions",
"access.revoke": "Revoke",
@@ -418,13 +428,17 @@
"status.ready": "Ready",
"status.unknown": "Unknown",
"subtitle": "Deploy and manage your apps across environments.",
+ "tabs.access.description": "Configure environment permissions and WebApp or CLI access channels.",
+ "tabs.access.name": "Access",
+ "tabs.api.description": "Manage HTTP endpoint access and API keys for this instance.",
+ "tabs.api.name": "Developer API",
"tabs.deploy.description": "Environments this instance is deployed to and their current releases.",
"tabs.deploy.name": "Deploy",
"tabs.overview.description": "Deploy releases and review target environments.",
"tabs.overview.name": "Overview",
"tabs.releases.description": "All releases for this app. Deploy any release to an environment.",
"tabs.releases.name": "Releases",
- "tabs.settings.description": "Access, API keys, metadata, and backend-managed settings.",
+ "tabs.settings.description": "Metadata and backend-managed settings for this instance.",
"tabs.settings.name": "Settings",
"title": "App instances",
"versions.cancelCreate": "Cancel",
diff --git a/web/i18n/zh-Hans/deployments.json b/web/i18n/zh-Hans/deployments.json
index bcb5e2f237..aeb80956f0 100644
--- a/web/i18n/zh-Hans/deployments.json
+++ b/web/i18n/zh-Hans/deployments.json
@@ -14,7 +14,14 @@
"access.api.newTokenLabel": "密钥",
"access.api.newTokenTitle": "API Key 已创建",
"access.api.noKeys": "尚无 API 密钥,创建一个即可调用 API。",
+ "access.api.table.action": "操作",
+ "access.api.table.environment": "环境",
+ "access.api.table.key": "API Key",
+ "access.api.table.name": "名称",
"access.api.title": "API",
+ "access.channels.col.channel": "渠道",
+ "access.channels.col.endpoint": "入口",
+ "access.channels.col.status": "状态",
"access.channels.description": "WebApp 与 CLI 入口遵循上方访问权限。",
"access.channels.disabled": "该实例的接入渠道已关闭。",
"access.channels.followPermission": "随权限开放",
@@ -55,6 +62,9 @@
"access.permission.specificDesc": "选择指定的分组或单个成员",
"access.permission.specificUnavailable": "特定成员暂未启用,需接入真实工作区成员与分组后再开放。",
"access.permission.updateFailed": "更新访问策略失败。",
+ "access.permissions.col.environment": "环境",
+ "access.permissions.col.permission": "访问范围",
+ "access.permissions.col.subjects": "授权对象",
"access.permissions.description": "配置该实例在每个已部署环境中的访问人员。",
"access.permissions.title": "访问权限",
"access.revoke": "撤销",
@@ -418,13 +428,17 @@
"status.ready": "就绪",
"status.unknown": "未知",
"subtitle": "在不同环境中部署和管理你的应用。",
+ "tabs.access.description": "配置环境访问权限,以及 WebApp 或 CLI 接入渠道。",
+ "tabs.access.name": "访问",
+ "tabs.api.description": "管理该实例的 HTTP 调用入口和 API Key。",
+ "tabs.api.name": "开发者 API",
"tabs.deploy.description": "该实例已部署的环境及其当前发布版本。",
"tabs.deploy.name": "部署",
"tabs.overview.description": "部署发布版本并查看目标环境。",
"tabs.overview.name": "概览",
"tabs.releases.description": "此应用的所有发布版本,可将任一发布版本部署到环境。",
"tabs.releases.name": "发布版本",
- "tabs.settings.description": "管理接入、API Key、元数据和后端托管设置。",
+ "tabs.settings.description": "管理该实例的元数据和后端托管设置。",
"tabs.settings.name": "设置",
"title": "应用实例",
"versions.cancelCreate": "取消",