This commit is contained in:
Stephen Zhou 2026-05-07 18:26:49 +08:00
parent fe51c9fbdf
commit 64fc1e8281
No known key found for this signature in database
15 changed files with 112 additions and 191 deletions

View File

@ -7,7 +7,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitl
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon'
@ -45,12 +45,10 @@ export function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps)
const triggerRef = useRef<HTMLButtonElement>(null)
const [triggerWidth, setTriggerWidth] = useState<number | undefined>(undefined)
const filtered = useMemo(() => {
const q = keywords.trim().toLowerCase()
if (!q)
return apps
return apps.filter(a => a.name.toLowerCase().includes(q) || a.mode.toLowerCase().includes(q))
}, [apps, keywords])
const q = keywords.trim().toLowerCase()
const filtered = q
? apps.filter(a => a.name.toLowerCase().includes(q) || a.mode.toLowerCase().includes(q))
: apps
const handleOpenChange = (next: boolean) => {
if (next && triggerRef.current)
@ -218,9 +216,7 @@ function CreateInstanceForm({ onClose }: {
},
},
}))
const apps = useMemo<AppInfo[]>(() => {
return (appList?.data ?? []).map(toStudioSourceAppInfo)
}, [appList?.data])
const apps = (appList?.data ?? []).map(toStudioSourceAppInfo)
const [appId, setAppId] = useState<string>('')
const [name, setName] = useState('')

View File

@ -3,7 +3,6 @@
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { DEPLOYMENT_PAGE_SIZE } from '../data'
@ -34,11 +33,7 @@ export function DeployDrawer() {
enabled: open,
}))
const environmentOptions = useMemo(
() => environmentOptionsFromOptionsReply(environmentOptionsReply),
[environmentOptionsReply],
)
const environments = environmentOptions
const environments = environmentOptionsFromOptionsReply(environmentOptionsReply)
const releases = releaseHistory?.data?.filter(release => release.id) ?? []
const defaultReleaseId = releases[0]?.id
const formKey = `${drawer.appInstanceId ?? 'none'}-${drawer.environmentId ?? 'any'}-${drawer.releaseId ?? 'new'}-${open ? '1' : '0'}`

View File

@ -5,7 +5,7 @@ import type { ConsoleReleaseSummary, EnvironmentOption } from '@/features/deploy
import { Button } from '@langgenius/dify-ui/button'
import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import {
@ -105,6 +105,20 @@ function selectedDeploymentBindings(slots: DeploymentBindingOptionSlot[], select
.filter((binding): binding is DeploymentRuntimeBinding => Boolean(binding))
}
function selectedBindingSelections(slots: DeploymentBindingOptionSlot[], manualBindings: BindingSelections): BindingSelections {
const next: BindingSelections = {}
for (const slot of slots) {
const slotKey = bindingSlotKey(slot)
const candidates = bindingCandidateOptions(slot)
const existing = manualBindings[slotKey]
if (existing && candidates.some(candidate => candidate.value === existing))
next[slotKey] = existing
else if (candidates.length === 1 && candidates[0])
next[slotKey] = candidates[0].value
}
return next
}
function BindingOptionsPanel({
slots,
selections,
@ -204,10 +218,7 @@ export function DeployForm({
onSubmit,
}: DeployFormProps) {
const { t } = useTranslation('deployments')
const presetRelease = useMemo(
() => presetReleaseId ? releases.find(r => r.id === presetReleaseId) : undefined,
[releases, presetReleaseId],
)
const presetRelease = presetReleaseId ? releases.find(r => r.id === presetReleaseId) : undefined
const displayedRelease = presetRelease ?? (presetReleaseId ? { id: presetReleaseId } : undefined)
const isPromote = Boolean(presetReleaseId)
@ -231,28 +242,10 @@ export function DeployForm({
}
: skipToken,
}))
const bindingSlots = useMemo(
() => bindingOptions.data?.slots?.filter(slot => slot.slot) ?? [],
[bindingOptions.data?.slots],
)
const bindingSlots = bindingOptions.data?.slots?.filter(slot => slot.slot) ?? []
const [manualBindings, setManualBindings] = useState<BindingSelections>({})
const selectedBindings = useMemo(() => {
const next: BindingSelections = {}
for (const slot of bindingSlots) {
const slotKey = bindingSlotKey(slot)
const candidates = bindingCandidateOptions(slot)
const existing = manualBindings[slotKey]
if (existing && candidates.some(candidate => candidate.value === existing))
next[slotKey] = existing
else if (candidates.length === 1 && candidates[0])
next[slotKey] = candidates[0].value
}
return next
}, [bindingSlots, manualBindings])
const deploymentBindings = useMemo(
() => selectedDeploymentBindings(bindingSlots, selectedBindings),
[bindingSlots, selectedBindings],
)
const selectedBindings = selectedBindingSelections(bindingSlots, manualBindings)
const deploymentBindings = selectedDeploymentBindings(bindingSlots, selectedBindings)
const bindingOptionsLoading = Boolean(targetReleaseId && (bindingOptions.isLoading || bindingOptions.isFetching))
const bindingOptionsReady = Boolean(targetReleaseId && bindingOptions.data && !bindingOptionsLoading && !bindingOptions.isError)
const requiredBindingsReady = bindingSlots.every(slot => !hasMissingRequiredBinding(slot, selectedBindings[bindingSlotKey(slot)]))

View File

@ -9,7 +9,6 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { DEPLOYMENT_PAGE_SIZE } from '../data'
@ -68,10 +67,7 @@ export function RollbackModal() {
: skipToken,
enabled: modal.open && Boolean(modal.appInstanceId),
}))
const environmentOptions = useMemo(
() => environmentOptionsFromOptionsReply(environmentOptionsReply),
[environmentOptionsReply],
)
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
const currentRow = deployedRows(environmentDeployments?.data)
.find(row => environmentId(row.environment) === modal.environmentId)

View File

@ -6,7 +6,7 @@ import type {
ConsoleEnvironmentSummary,
} from '@/features/deployments/types'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { consoleClient, consoleQuery } from '@/service/client'
import {
deployedRows,
@ -46,19 +46,13 @@ export function AccessTab({ instanceId: appId }: {
const toggleDeveloperAPI = useMutation(consoleQuery.enterprise.appDeploy.updateDeveloperApi.mutationOptions())
const setEnvironmentAccessPolicy = useMutation(consoleQuery.enterprise.appDeploy.updateEnvironmentAccessPolicy.mutationOptions())
const deploymentRows = useMemo(
() => deployedRows(environmentDeployments?.data),
[environmentDeployments?.data],
)
const deploymentRows = deployedRows(environmentDeployments?.data)
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
const deployedEnvs = useMemo(
() => uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
]),
[accessConfig?.accessChannels?.webappRows, deploymentRows, policies],
)
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) => {

View File

@ -19,7 +19,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
import { toast } from '@langgenius/dify-ui/toast'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import {
@ -181,10 +181,7 @@ function SubjectPicker({
const [open, setOpen] = useState(false)
const [keyword, setKeyword] = useState('')
const debouncedKeyword = useDebounce(keyword, { wait: 300 })
const selectedKeys = useMemo(
() => new Set(selectedSubjects.map(subjectKey)),
[selectedSubjects],
)
const selectedKeys = new Set(selectedSubjects.map(subjectKey))
const subjectsQuery = useQuery(consoleQuery.enterprise.appDeploy.searchAccessSubjects.queryOptions({
input: open
? {
@ -196,12 +193,9 @@ function SubjectPicker({
}
: skipToken,
}))
const subjects = useMemo(
() => subjectsQuery.data?.data
?.map(normalizeSubject)
.filter((subject): subject is SelectableAccessSubject => Boolean(subject)) ?? [],
[subjectsQuery.data?.data],
)
const subjects = subjectsQuery.data?.data
?.map(normalizeSubject)
.filter((subject): subject is SelectableAccessSubject => Boolean(subject)) ?? []
const toggleSubject = (subject: SelectableAccessSubject) => {
const key = subjectKey(subject)
@ -330,10 +324,7 @@ export function EnvironmentPermissionRow({
detailPolicy?.accessMode ?? summaryPolicy?.accessMode ?? '',
policySubjectFingerprint ?? '',
].join(':')
const policySelectedSubjects = useMemo(
() => policyKind === 'specific' ? selectedSubjectsFromPolicy(detailPolicy) : [],
[detailPolicy, policyKind],
)
const policySelectedSubjects = policyKind === 'specific' ? selectedSubjectsFromPolicy(detailPolicy) : []
const [draft, setDraft] = useState<{
fingerprint?: string
kind?: AccessPermissionKind

View File

@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { useDeploymentsStore } from '../store'
@ -45,26 +45,13 @@ export function DeployTab({ instanceId: appInstanceId }: {
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const cancelDeployment = useMutation(consoleQuery.enterprise.appDeploy.cancelRuntimeDeployment.mutationOptions())
const undeployDeployment = useMutation(consoleQuery.enterprise.appDeploy.undeployRuntimeInstance.mutationOptions())
const environmentOptions = useMemo(
() => environmentOptionsFromOptionsReply(environmentOptionsReply),
[environmentOptionsReply],
)
const rows = useMemo(
() => environmentDeployments?.data?.filter(row => row.environment?.id) ?? [],
[environmentDeployments?.data],
)
const deployedRuntimeRows = useMemo(
() => deployedRows(environmentDeployments?.data),
[environmentDeployments?.data],
)
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
const rows = environmentDeployments?.data?.filter(row => row.environment?.id) ?? []
const deployedRuntimeRows = deployedRows(environmentDeployments?.data)
const deployedEnvIds = new Set(deployedRuntimeRows.map(row => environmentId(row.environment)))
const availableEnvs = environmentOptions.filter(env => env.id && !deployedEnvIds.has(env.id))
const expandableEnvIds = useMemo(
() => rows.filter(row => !isUndeployedDeploymentRow(row)).map(row => environmentId(row.environment)),
[rows],
)
const expandableEnvIds = rows.filter(row => !isUndeployedDeploymentRow(row)).map(row => environmentId(row.environment))
const [expanded, setExpanded] = useState<string | null>()
const activeExpanded = expanded === undefined
? expandableEnvIds[0] ?? null

View File

@ -6,7 +6,7 @@ import type { InstanceDetailTabKey } from './tabs'
import type { NavIcon } from '@/app/components/app-sidebar/nav-link'
import { cn } from '@langgenius/dify-ui/cn'
import { useHover, useKeyPress } from 'ahooks'
import { useCallback, useEffect, useRef } from 'react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import NavLink from '@/app/components/app-sidebar/nav-link'
@ -103,9 +103,9 @@ export function DeploymentSidebar({
const sidebarMode = appSidebarExpand || 'expand'
const expand = sidebarMode === 'expand'
const handleToggle = useCallback(() => {
function handleToggle() {
setAppSidebarExpand(sidebarMode === 'expand' ? 'collapse' : 'expand')
}, [setAppSidebarExpand, sidebarMode])
}
useEffect(() => {
const persistedMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'

View File

@ -4,7 +4,6 @@ import type { ReactNode } from 'react'
import type { InstanceDetailTabKey } from './tabs'
import { Button } from '@langgenius/dify-ui/button'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
import useDocumentTitle from '@/hooks/use-document-title'
@ -34,10 +33,7 @@ export function InstanceDetail({ instanceId, children }: {
useDocumentTitle(t('documentTitle.detail'))
const app = useMemo(
() => toAppInfoFromOverview(overviewQuery.data?.instance),
[overviewQuery.data?.instance],
)
const app = toAppInfoFromOverview(overviewQuery.data?.instance)
if (!app && overviewQuery.isLoading) {
return (

View File

@ -3,7 +3,6 @@ import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
import { useRouter } from '@/next/navigation'
@ -115,10 +114,7 @@ export function OverviewTab({ instanceId }: {
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const app = toAppInfoFromOverview(overview?.instance)
const overviewApp = overview?.instance
const deployments = useMemo(
() => overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? [],
[overview?.deployments],
)
const deployments = overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? []
const releaseRows = releaseHistory?.data?.filter(row => row.id) ?? []
const canCreateRelease = overviewApp?.canCreateRelease ?? true

View File

@ -13,7 +13,7 @@ import {
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
@ -183,10 +183,7 @@ export function SettingsTab({ instanceId }: {
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const app = useMemo(
() => toAppInfoFromOverview(overview?.instance),
[overview?.instance],
)
const app = toAppInfoFromOverview(overview?.instance)
const settingsQuery = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceSettings.queryOptions({
input: appInput,
}))

View File

@ -5,7 +5,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitl
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
@ -47,14 +47,8 @@ export function VersionsTab({ instanceId: appId }: {
const [isCreating, setIsCreating] = useState(false)
const [releaseName, setReleaseName] = useState('')
const [releaseDescription, setReleaseDescription] = useState('')
const releaseRows = useMemo(
() => releaseHistory?.data?.filter(row => row.id) ?? [],
[releaseHistory?.data],
)
const deploymentRows = useMemo(
() => deployedRows(environmentDeployments?.data),
[environmentDeployments?.data],
)
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)

View File

@ -8,7 +8,7 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { useDeploymentsStore } from '../../store'
@ -38,10 +38,7 @@ export function DeployReleaseMenu({ appInstanceId, releaseId }: {
enabled: open,
}))
const environmentOptions = useMemo(
() => environmentOptionsFromOptionsReply(environmentOptionsReply),
[environmentOptionsReply],
)
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
const environments = environmentOptions.filter(env => env.id)
const deploymentRows = deployedRows(environmentDeployments?.data)

View File

@ -3,7 +3,6 @@
import { useQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { debounce, parseAsString, useQueryState } from 'nuqs'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { consoleQuery } from '@/service/client'
@ -56,47 +55,42 @@ export function DeploymentsMain() {
},
}))
const { data: environmentOptionsReply } = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions())
const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
const summaries = useMemo(() => deploymentSummariesFromList(listQuery.data), [listQuery.data])
const environments = useMemo(() => {
return environmentOptionsReply?.environments?.flatMap((env) => {
if (!env.id)
return []
return [{
id: env.id,
name: env.name || env.id,
disabled: env.deployable === false,
disabledReason: env.disabledReason,
}]
}) ?? []
}, [environmentOptionsReply])
const envIdSet = useMemo(() => new Set(environments.map(e => e.id)), [environments])
const apps = sourceAppsFromList(listQuery.data)
const summaries = deploymentSummariesFromList(listQuery.data)
const environments = environmentOptionsReply?.environments?.flatMap((env) => {
if (!env.id)
return []
return [{
id: env.id,
name: env.name || env.id,
disabled: env.deployable === false,
disabledReason: env.disabledReason,
}]
}) ?? []
const envIdSet = new Set(environments.map(e => e.id))
const activeFilter = envFilter === 'all' || envFilter === 'not-deployed' || envIdSet.has(envFilter)
? envFilter
: 'all'
const filterOptions = useMemo(() => {
return [
{
value: 'all',
text: t('filter.allEnvs'),
icon: <span className="i-ri-apps-2-line h-[14px] w-[14px]" />,
},
...environments.map(env => ({
value: env.id,
text: env.name,
icon: <span className="i-ri-stack-line h-[14px] w-[14px]" />,
disabled: env.disabled,
disabledReason: env.disabledReason,
})),
{
value: 'not-deployed',
text: t('filter.notDeployed'),
icon: <span className="i-ri-inbox-line h-[14px] w-[14px]" />,
},
]
}, [environments, t])
const filterOptions = [
{
value: 'all',
text: t('filter.allEnvs'),
icon: <span className="i-ri-apps-2-line h-[14px] w-[14px]" />,
},
...environments.map(env => ({
value: env.id,
text: env.name,
icon: <span className="i-ri-stack-line h-[14px] w-[14px]" />,
disabled: env.disabled,
disabledReason: env.disabledReason,
})),
{
value: 'not-deployed',
text: t('filter.notDeployed'),
icon: <span className="i-ri-inbox-line h-[14px] w-[14px]" />,
},
]
return (
<>

View File

@ -3,7 +3,6 @@
import type { NavItem } from '@/app/components/header/nav/nav-selector'
import type { AppIconType, AppModeEnum } from '@/types/app'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Nav from '@/app/components/header/nav'
import { useParams, useRouter, useSelectedLayoutSegment } from '@/next/navigation'
@ -41,39 +40,35 @@ export function DeploymentsNav() {
},
enabled: isActive,
}))
const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
const apps = sourceAppsFromList(listQuery.data)
const navigationItems = useMemo<NavItem[]>(() => {
if (!isActive)
return []
const navApps = currentInstance && !apps.some(app => app.id === currentInstance.id)
? [...apps, currentInstance]
: apps
return navApps.map((app) => {
return {
id: app.id,
name: app.name,
link: `/deployments/${app.id}/overview`,
icon_type: (app.iconType ?? null) as AppIconType | null,
icon: app.icon ?? '',
icon_background: app.iconBackground ?? null,
icon_url: app.iconUrl ?? null,
mode: app.mode as unknown as AppModeEnum | undefined,
}
})
}, [apps, currentInstance, isActive])
const navApps = currentInstance && !apps.some(app => app.id === currentInstance.id)
? [...apps, currentInstance]
: apps
const navigationItems: NavItem[] = isActive
? navApps.map((app) => {
return {
id: app.id,
name: app.name,
link: `/deployments/${app.id}/overview`,
icon_type: (app.iconType ?? null) as AppIconType | null,
icon: app.icon ?? '',
icon_background: app.iconBackground ?? null,
icon_url: app.iconUrl ?? null,
mode: app.mode as unknown as AppModeEnum | undefined,
}
})
: []
const curNav = useMemo(() => {
if (!instanceId)
return undefined
return navigationItems.find(item => item.id === instanceId)
}, [instanceId, navigationItems])
const curNav = instanceId
? navigationItems.find(item => item.id === instanceId)
: undefined
const handleCreate = useCallback(() => {
function handleCreate() {
openCreateInstanceModal()
if (selectedSegment !== 'deployments' || instanceId)
router.push('/deployments')
}, [openCreateInstanceModal, router, selectedSegment, instanceId])
}
return (
<Nav