diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md
index 4a5a73088ec..ee5c0d80887 100644
--- a/.agents/skills/how-to-write-component/SKILL.md
+++ b/.agents/skills/how-to-write-component/SKILL.md
@@ -12,6 +12,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
| Question | Default | Promote or extract only when |
| --- | --- | --- |
| Where should code live? | Keep it local to the feature workflow, route, or owner. | Multiple verticals need the same stable primitive. |
+| How should route/tab folders be named? | Match the current route segment, tab name, or user-visible surface. | Keep a historical or broader parent only when it still owns multiple surfaces. |
| Who owns state, data, and handlers? | The lowest component that uses them. | A parent coordinates shared loading, errors, empty UI, selection, submission, navigation, or one consistent snapshot. |
| Should this become Jotai state? | Keep synchronous UI/form state in component or DOM state. | Siblings need one source of truth, the value drives atoms, or scoped workflow state must survive hidden/unmounted steps. |
| Should URL state enter Jotai? | Let Next.js route params and `nuqs` own URL state and updates. | Query atoms or shared derived atoms need a read-only bridge hydrated at the route/surface boundary. |
@@ -23,7 +24,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
- Search before adding UI, hooks, helpers, query utilities, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit.
- Follow Dify's CSS-first Tailwind v4 contract from `packages/dify-ui/README.md` and `packages/dify-ui/AGENTS.md`. Prefer design-system tokens, utilities, and radius mappings over generic Tailwind choices.
-- Group feature code by workflow, route, or ownership area: components, hooks, local types, query helpers, atoms, constants, and small utilities should live near the code that changes with them.
+- Group feature code by workflow, route, or ownership area with route-aligned names: components, hooks, local types, query helpers, atoms, constants, tests, and small utilities should live near the code that changes with them.
- Keep source/default selection, validation, dirty checks, and payload shaping close to the workflow that owns submit behavior. Do not hide flow-specific priority order, fallback behavior, or submit semantics in generic utilities.
- Prefer direct conditionals for small branch-specific decisions, especially form source selection and request payload assembly.
- Loading states for page sections, cards, lists, tables, forms, and drawers should be skeletons scoped to the content being loaded. Use spinners only for small inline busy indicators.
@@ -32,6 +33,8 @@ Use this as the component decision guide for Dify web. Existing code is referenc
- State-heavy wizards, drawers, modals, and secondary workflows can be a small feature surface: an entry file, one feature-local state file when Jotai is actually needed, and shallow `ui/` owners that match real visual regions.
- The entry file handles route integration, provider wiring, close behavior, and surface mounting. The composition owner handles high-level workflow branching. The closest visual owner handles section branching.
+- When a page or tab maps to a route segment, name its feature folder after that route/tab surface instead of a stale parent grouping. Remove misleading intermediate folders when only one surface remains.
+- When a tab folder grows into several independent sections or action areas, split the first level by product/visual owners. Keep the root for the entry component and cross-owner state, colocate tests with the owner folder, and put truly shared local UI under a specifically named `components/` file.
- Repeated TanStack query calls in sibling components are acceptable when each component independently consumes the data; TanStack Query deduplicates and shares cache.
- Pass stable domain identity across boundaries. Do not forward derived presentation state when the receiver can derive it from its own data source.
- A component that owns a visual surface should also own data access, loading, empty, and error states for content rendered inside it unless a parent truly coordinates that state.
@@ -60,8 +63,10 @@ Use this as the component decision guide for Dify web. Existing code is referenc
- Type component signatures directly; do not use `FC` or `React.FC`.
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
+- Avoid barrel files that only re-export secondary owners. `index.tsx` is acceptable for a route/tab entry component; import header controls, switches, sections, and row owners from their concrete owner files.
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers and one-off UI extensions beside the component that needs them.
+- Avoid `common.tsx` buckets for shared UI. Use a feature-local `components/` folder with concrete filenames that describe the shared role.
- Do not create type aliases that only rename another type. Use aliases only for real UI concepts, refinements, or reusable local contracts.
- Name values by their domain role and backend API contract, especially persistent IDs and route params. Normalize framework or route params at the boundary.
- Put fallback and invariant checks in the lowest component that already handles that state. Do not extract helpers whose only behavior is hiding missing display data.
@@ -96,7 +101,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
## Boundaries And Overlays
-- Use the first level below a page or tab to organize independent page sections when it adds structure. This layer is layout/semantic first, not automatically the data owner.
+- Use the first level below a page or tab to organize independent page sections when it adds structure or the root folder becomes noisy. This layer is layout/semantic first, not automatically the data owner.
- Treat component names, semantic roles, and user- or design-marked visual regions as boundary constraints. Keep adjacent UI as a sibling owner or introduce a correctly named broader owner.
- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary.
- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component when hidden content would obscure the parent.
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/access/page.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/access/page.tsx
index 5fa2ff42250..f8db21c90d1 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/access/page.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/access/page.tsx
@@ -1,8 +1,5 @@
import { AccessTab } from '@/features/deployments/detail/access-tab'
-export default async function InstanceDetailAccessPage({ params }: {
- params: Promise<{ appInstanceId: string }>
-}) {
- const { appInstanceId } = await params
- return
+export default function InstanceDetailAccessPage() {
+ return
}
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/api-tokens/page.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/api-tokens/page.tsx
index 6fb643008ad..55d4812aa4c 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/api-tokens/page.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/api-tokens/page.tsx
@@ -1,8 +1,5 @@
-import { DeveloperApiTab } from '@/features/deployments/detail/developer-api-tab'
+import { DeveloperApiTab } from '@/features/deployments/detail/access-tab/developer-api'
-export default async function InstanceDetailApiTokensPage({ params }: {
- params: Promise<{ appInstanceId: string }>
-}) {
- const { appInstanceId } = await params
- return
+export default function InstanceDetailApiTokensPage() {
+ return
}
diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx
deleted file mode 100644
index 92b463e3970..00000000000
--- a/web/features/deployments/detail/access-tab.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client'
-
-import { useAtomValue } from 'jotai'
-import { AccessChannelsSection } from './settings-tab/access/channels-section'
-import { AccessPermissionsSection } from './settings-tab/access/permissions-section'
-import { accessSettingsQueryAtom } from './settings-tab/access/state'
-
-export function AccessTab({ appInstanceId }: {
- appInstanceId: string
-}) {
- const accessSettingsQuery = useAtomValue(accessSettingsQueryAtom)
-
- return (
-
-
-
-
- )
-}
diff --git a/web/features/deployments/detail/settings-tab/access/__tests__/state.spec.ts b/web/features/deployments/detail/access-tab/__tests__/state.spec.ts
similarity index 97%
rename from web/features/deployments/detail/settings-tab/access/__tests__/state.spec.ts
rename to web/features/deployments/detail/access-tab/__tests__/state.spec.ts
index 77e3d9b2c22..65ec27d8773 100644
--- a/web/features/deployments/detail/settings-tab/access/__tests__/state.spec.ts
+++ b/web/features/deployments/detail/access-tab/__tests__/state.spec.ts
@@ -48,7 +48,7 @@ async function loadState() {
function setDeploymentRoute(store: ReturnType, appInstanceId = 'app-instance-1') {
store.set(setNextRouteStateAtom, {
- pathname: `/deployments/${appInstanceId}/settings/access`,
+ pathname: `/deployments/${appInstanceId}/access`,
params: { appInstanceId },
})
}
diff --git a/web/features/deployments/detail/settings-tab/access/__tests__/channels-section.spec.tsx b/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
similarity index 61%
rename from web/features/deployments/detail/settings-tab/access/__tests__/channels-section.spec.tsx
rename to web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
index fb4195115a7..488ae1a7732 100644
--- a/web/features/deployments/detail/settings-tab/access/__tests__/channels-section.spec.tsx
+++ b/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
@@ -1,9 +1,20 @@
import type { AccessChannels, AccessEndpoint } from '@dify/contracts/enterprise/types.gen'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { AccessChannelsSection } from '../channels-section'
+import { deploymentRouteAppInstanceIdAtom } from '../../../../route-state'
+import { accessSettingsQueryAtom } from '../../state'
+import { AccessChannelsSection } from '../section'
const mockToggleAccessChannel = vi.hoisted(() => vi.fn())
+const mockUseAtomValue = vi.hoisted(() => vi.fn())
+
+vi.mock('jotai', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ useAtomValue: mockUseAtomValue,
+ }
+})
vi.mock('@tanstack/react-query', () => ({
useMutation: () => ({
@@ -49,19 +60,26 @@ function createEndpoint(endpointUrl: string): AccessEndpoint {
describe('AccessChannelsSection', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockUseAtomValue.mockImplementation((atom) => {
+ if (atom === deploymentRouteAppInstanceIdAtom)
+ return 'app-instance-1'
+ if (atom === accessSettingsQueryAtom) {
+ return {
+ data: {
+ accessChannels: createAccessChannels(),
+ webAppEndpoints: [createEndpoint('https://app.example.com/webapp')],
+ cliEndpoint: createEndpoint('https://cli.example.com/entry'),
+ },
+ isLoading: false,
+ isError: false,
+ }
+ }
+ return undefined
+ })
})
it('should render channel descriptions when access channels are enabled', () => {
- render(
- ,
- )
+ render()
expect(screen.getByText('deployments.access.runAccess.webappDesc')).toBeInTheDocument()
expect(screen.getByText('deployments.access.cli.description')).toBeInTheDocument()
diff --git a/web/features/deployments/detail/settings-tab/access/channels-section.tsx b/web/features/deployments/detail/access-tab/channels/section.tsx
similarity index 89%
rename from web/features/deployments/detail/settings-tab/access/channels-section.tsx
rename to web/features/deployments/detail/access-tab/channels/section.tsx
index d3dde7936a4..5f08cc032c3 100644
--- a/web/features/deployments/detail/settings-tab/access/channels-section.tsx
+++ b/web/features/deployments/detail/access-tab/channels/section.tsx
@@ -4,12 +4,15 @@ import type { AccessChannels, AccessEndpoint } from '@dify/contracts/enterprise/
import type { ReactNode } from 'react'
import { Switch, SwitchSkeleton } from '@langgenius/dify-ui/switch'
import { useMutation } from '@tanstack/react-query'
+import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { consoleQuery } from '@/service/client'
import { DeploymentEmptyState, DeploymentNoticeState, DeploymentStateMessage } from '../../../components/empty-state'
-import { Section } from '../../common'
-import { CopyPill, EndpointRow } from './common'
+import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
+import { Section } from '../../components/section'
+import { CopyPill, EndpointRow } from '../components/endpoint'
+import { accessSettingsQueryAtom } from '../state'
import { getUrlOrigin } from './url'
const ACCESS_CHANNEL_SKELETON_SECTIONS = [
@@ -17,22 +20,25 @@ const ACCESS_CHANNEL_SKELETON_SECTIONS = [
{ key: 'cli' },
]
-function AccessChannelsSwitch({ appInstanceId, checked, accessChannels, disabled }: {
- appInstanceId: string
+function AccessChannelsSwitch({ checked, accessChannels, disabled }: {
checked: boolean
accessChannels?: AccessChannels
disabled?: boolean
}) {
const { t } = useTranslation('deployments')
+ const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
const toggleAccessChannel = useMutation(consoleQuery.enterprise.accessService.updateAccessChannels.mutationOptions())
return (
{
+ if (!appInstanceId)
+ return
+
toggleAccessChannel.mutate({
params: { appInstanceId },
body: {
@@ -106,22 +112,15 @@ function ChannelRow({ info, children }: {
)
}
-export function AccessChannelsSection({
- appInstanceId,
- accessChannels,
- webAppEndpoints,
- cliEndpoint,
- isLoading,
- isError,
-}: {
- appInstanceId: string
- accessChannels?: AccessChannels
- webAppEndpoints?: AccessEndpoint[]
- cliEndpoint?: AccessEndpoint
- isLoading: boolean
- isError: boolean
-}) {
+export function AccessChannelsSection() {
const { t } = useTranslation('deployments')
+ const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
+ const accessSettingsQuery = useAtomValue(accessSettingsQueryAtom)
+ const accessChannels = accessSettingsQuery.data?.accessChannels
+ const webAppEndpoints: AccessEndpoint[] | undefined = accessSettingsQuery.data?.webAppEndpoints
+ const cliEndpoint: AccessEndpoint | undefined = accessSettingsQuery.data?.cliEndpoint
+ const isLoading = accessSettingsQuery.isLoading
+ const isError = accessSettingsQuery.isError
const runEnabled = accessChannels?.webAppEnabled ?? false
const webappRows = webAppEndpoints?.flatMap((endpoint) => {
const endpointUrl = endpoint.endpointUrl
@@ -148,7 +147,6 @@ export function AccessChannelsSection({
{runEnabled ? t('overview.enabled') : t('overview.disabled')}
{isLoading
?
- : isError
+ : isError || !appInstanceId
? {t('common.loadFailed')}
: runEnabled
? (
diff --git a/web/features/deployments/detail/settings-tab/access/url.ts b/web/features/deployments/detail/access-tab/channels/url.ts
similarity index 100%
rename from web/features/deployments/detail/settings-tab/access/url.ts
rename to web/features/deployments/detail/access-tab/channels/url.ts
diff --git a/web/features/deployments/detail/settings-tab/access/common.tsx b/web/features/deployments/detail/access-tab/components/endpoint.tsx
similarity index 100%
rename from web/features/deployments/detail/settings-tab/access/common.tsx
rename to web/features/deployments/detail/access-tab/components/endpoint.tsx
diff --git a/web/features/deployments/detail/settings-tab/access/__tests__/api-key-generate-menu.spec.tsx b/web/features/deployments/detail/access-tab/developer-api/__tests__/api-key-generate-menu.spec.tsx
similarity index 73%
rename from web/features/deployments/detail/settings-tab/access/__tests__/api-key-generate-menu.spec.tsx
rename to web/features/deployments/detail/access-tab/developer-api/__tests__/api-key-generate-menu.spec.tsx
index a83974fba6f..900836db3cd 100644
--- a/web/features/deployments/detail/settings-tab/access/__tests__/api-key-generate-menu.spec.tsx
+++ b/web/features/deployments/detail/access-tab/developer-api/__tests__/api-key-generate-menu.spec.tsx
@@ -1,9 +1,19 @@
import type { Environment } from '@dify/contracts/enterprise/types.gen'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
+import { deploymentRouteAppInstanceIdAtom } from '../../../../route-state'
import { ApiKeyGenerateMenu } from '../api-key-generate-menu'
const mockMutate = vi.hoisted(() => vi.fn())
+const mockUseAtomValue = vi.hoisted(() => vi.fn())
+
+vi.mock('jotai', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ useAtomValue: mockUseAtomValue,
+ }
+})
vi.mock('@tanstack/react-query', () => ({
useMutation: () => ({
@@ -34,12 +44,16 @@ function createEnvironment(): Environment {
describe('ApiKeyGenerateMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockUseAtomValue.mockImplementation((atom) => {
+ if (atom === deploymentRouteAppInstanceIdAtom)
+ return 'app-instance-1'
+ return undefined
+ })
})
it('should show the required name error when submitting an empty name', () => {
render(
,
@@ -58,7 +72,6 @@ describe('ApiKeyGenerateMenu', () => {
it('should clear the required name error when typing a valid name', () => {
render(
,
@@ -79,4 +92,17 @@ describe('ApiKeyGenerateMenu', () => {
expect(screen.queryByText('deployments.access.api.nameRequired')).not.toBeInTheDocument()
})
+
+ it('should disable the trigger when route app instance is missing', () => {
+ mockUseAtomValue.mockReturnValue(undefined)
+
+ render(
+ ,
+ )
+
+ expect(screen.getByRole('button', { name: 'deployments.access.api.newKey' })).toBeDisabled()
+ })
})
diff --git a/web/features/deployments/detail/settings-tab/access/api-key-generate-menu.tsx b/web/features/deployments/detail/access-tab/developer-api/api-key-generate-menu.tsx
similarity index 96%
rename from web/features/deployments/detail/settings-tab/access/api-key-generate-menu.tsx
rename to web/features/deployments/detail/access-tab/developer-api/api-key-generate-menu.tsx
index bde6eb5d667..b79ddd5b9f8 100644
--- a/web/features/deployments/detail/settings-tab/access/api-key-generate-menu.tsx
+++ b/web/features/deployments/detail/access-tab/developer-api/api-key-generate-menu.tsx
@@ -24,20 +24,20 @@ import {
} from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation } from '@tanstack/react-query'
+import { useAtomValue } from 'jotai'
import { useEffect, useId, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
+import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
import { generateApiTokenName } from './api-token-name'
export function ApiKeyGenerateMenu({
- appInstanceId,
environments,
onCreatedToken,
triggerVariant = 'secondary',
triggerClassName,
children,
}: {
- appInstanceId: string
environments: Environment[]
onCreatedToken: (token: string) => void
triggerVariant?: ButtonProps['variant']
@@ -45,6 +45,7 @@ export function ApiKeyGenerateMenu({
children?: (props: { trigger: ReactNode }) => ReactNode
}) {
const { t } = useTranslation('deployments')
+ const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
const nameInputId = useId()
const nameInputRef = useRef(null)
const [createDialogOpen, setCreateDialogOpen] = useState(false)
@@ -56,7 +57,7 @@ export function ApiKeyGenerateMenu({
const selectedEnvironment = selectedEnvironmentId
? selectableEnvironments.find(env => env.id === selectedEnvironmentId)
: undefined
- const disabled = selectableEnvironments.length === 0
+ const disabled = !appInstanceId || selectableEnvironments.length === 0
const isCreating = generateApiKey.isPending
useEffect(() => {
@@ -105,7 +106,7 @@ export function ApiKeyGenerateMenu({
const name = draftName.trim()
- if (!selectedEnvironmentId || !name) {
+ if (!appInstanceId || !selectedEnvironmentId || !name) {
setNameError(true)
return
}
diff --git a/web/features/deployments/detail/settings-tab/access/api-key-list.tsx b/web/features/deployments/detail/access-tab/developer-api/api-key-list.tsx
similarity index 98%
rename from web/features/deployments/detail/settings-tab/access/api-key-list.tsx
rename to web/features/deployments/detail/access-tab/developer-api/api-key-list.tsx
index cf2a8553aee..4bcbc9152c1 100644
--- a/web/features/deployments/detail/settings-tab/access/api-key-list.tsx
+++ b/web/features/deployments/detail/access-tab/developer-api/api-key-list.tsx
@@ -28,10 +28,10 @@ import {
DetailTableHead,
DetailTableHeader,
DetailTableRow,
-} from '../../table'
+} from '../../components/detail-table'
import {
API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES,
-} from '../../table-styles'
+} from '../../components/detail-table-styles'
function ApiKeyName({ apiKey }: {
apiKey: ApiKey
diff --git a/web/features/deployments/detail/settings-tab/access/api-token-name.ts b/web/features/deployments/detail/access-tab/developer-api/api-token-name.ts
similarity index 100%
rename from web/features/deployments/detail/settings-tab/access/api-token-name.ts
rename to web/features/deployments/detail/access-tab/developer-api/api-token-name.ts
diff --git a/web/features/deployments/detail/settings-tab/access/developer-api-created-token-dialog.tsx b/web/features/deployments/detail/access-tab/developer-api/created-token-dialog.tsx
similarity index 98%
rename from web/features/deployments/detail/settings-tab/access/developer-api-created-token-dialog.tsx
rename to web/features/deployments/detail/access-tab/developer-api/created-token-dialog.tsx
index 3582e53bdcc..16778ec84fc 100644
--- a/web/features/deployments/detail/settings-tab/access/developer-api-created-token-dialog.tsx
+++ b/web/features/deployments/detail/access-tab/developer-api/created-token-dialog.tsx
@@ -6,7 +6,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitl
import { toast } from '@langgenius/dify-ui/toast'
import { useClipboard } from 'foxact/use-clipboard'
import { useTranslation } from 'react-i18next'
-import { CopyPill } from './common'
+import { CopyPill } from '../components/endpoint'
function buildCurlExample(apiUrl: string, token: string) {
return `curl -X POST '${apiUrl}' \\
diff --git a/web/features/deployments/detail/settings-tab/access/api-docs-drawer.tsx b/web/features/deployments/detail/access-tab/developer-api/docs-drawer.tsx
similarity index 94%
rename from web/features/deployments/detail/settings-tab/access/api-docs-drawer.tsx
rename to web/features/deployments/detail/access-tab/developer-api/docs-drawer.tsx
index fb88bf0c9cf..013b5872878 100644
--- a/web/features/deployments/detail/settings-tab/access/api-docs-drawer.tsx
+++ b/web/features/deployments/detail/access-tab/developer-api/docs-drawer.tsx
@@ -13,6 +13,7 @@ import {
DrawerTitle,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
+import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import TemplateWorkflowEn from '@/app/components/develop/template/template_workflow.en.mdx'
import TemplateWorkflowJa from '@/app/components/develop/template/template_workflow.ja.mdx'
@@ -21,6 +22,7 @@ import { useLocale } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { getDocLanguage } from '@/i18n-config/language'
import { AppModeEnum, Theme } from '@/types/app'
+import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
type PromptVariable = { key: string, name: string }
type WorkflowApiDocAppDetail = Pick
@@ -67,19 +69,22 @@ function WorkflowDocTemplate({ docLanguage, appDetail, variables, inputs }: Work
export function DeveloperApiDocsDrawer({
open,
- appInstanceId,
apiBaseUrl,
onOpenChange,
}: {
open: boolean
- appInstanceId: string
apiBaseUrl: string
onOpenChange: (open: boolean) => void
}) {
const { t } = useTranslation('deployments')
+ const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
const locale = useLocale()
const { theme } = useTheme()
const docLanguage = getDocLanguage(locale)
+
+ if (!appInstanceId)
+ return null
+
const appDetail: WorkflowApiDocAppDetail = {
id: appInstanceId,
mode: AppModeEnum.WORKFLOW,
diff --git a/web/features/deployments/detail/access-tab/developer-api/index.tsx b/web/features/deployments/detail/access-tab/developer-api/index.tsx
new file mode 100644
index 00000000000..a223de437d5
--- /dev/null
+++ b/web/features/deployments/detail/access-tab/developer-api/index.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import { DeveloperApiSection } from './section'
+
+export function DeveloperApiTab() {
+ return (
+