mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
tweaks
This commit is contained in:
parent
e8ec7c7ff5
commit
663818f411
@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DeploymentsMain from '@/features/deployments/list'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
@ -10,4 +9,4 @@ const DeploymentsPage = () => {
|
||||
return <DeploymentsMain />
|
||||
}
|
||||
|
||||
export default React.memo(DeploymentsPage)
|
||||
export default DeploymentsPage
|
||||
|
||||
@ -7,7 +7,6 @@ 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'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
@ -46,8 +45,6 @@ export const AppPicker: FC<AppPickerProps> = ({ apps, isLoading, value, onChange
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState<number | undefined>(undefined)
|
||||
|
||||
const selected = useMemo(() => apps.find(a => a.id === value), [apps, value])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = keywords.trim().toLowerCase()
|
||||
if (!q)
|
||||
@ -71,6 +68,8 @@ export const AppPicker: FC<AppPickerProps> = ({ apps, isLoading, value, onChange
|
||||
)
|
||||
}
|
||||
|
||||
const selected = apps.find(a => a.id === value)
|
||||
|
||||
if (apps.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-components-panel-border bg-components-panel-bg-blur px-4 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
|
||||
@ -4,7 +4,6 @@ import type { FC } from 'react'
|
||||
import type { EnvironmentOption } from '@/contract/console/deployments'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { environmentHealth, environmentMode, environmentName } from '../../utils'
|
||||
import { HealthBadge, ModeBadge } from '../status-badge'
|
||||
@ -36,10 +35,7 @@ type SelectProps = {
|
||||
|
||||
export const DeploymentSelect: FC<SelectProps> = ({ value, onChange, options, placeholder }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const selectedOption = useMemo(
|
||||
() => options.find(option => option.value === value),
|
||||
[options, value],
|
||||
)
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
|
||||
return (
|
||||
<Select
|
||||
@ -71,17 +67,6 @@ export const DeploymentSelect: FC<SelectProps> = ({ value, onChange, options, pl
|
||||
)
|
||||
}
|
||||
|
||||
type LabeledSelectProps = SelectProps & { label: string }
|
||||
|
||||
export const LabeledSelect: FC<LabeledSelectProps> = ({ label, ...rest }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-20 shrink-0 system-xs-medium text-text-secondary">{label}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<DeploymentSelect {...rest} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
type EnvironmentRowProps = { env: EnvironmentOption }
|
||||
|
||||
export const EnvironmentRow: FC<EnvironmentRowProps> = ({ env }) => (
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store'
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { DeployStatus, EnvironmentHealth, EnvironmentMode } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type StatusBadgeProps = {
|
||||
|
||||
@ -134,10 +134,6 @@ export const deploymentAppDataQueryOptions = (appId: string) =>
|
||||
staleTime: DEPLOYMENT_APP_DATA_STALE_TIME,
|
||||
})
|
||||
|
||||
export const refreshDeploymentAppData = async (appId: string): Promise<DeploymentAppData> => {
|
||||
return fetchDeploymentAppData(appId)
|
||||
}
|
||||
|
||||
const wait = (delay: number) => new Promise(resolve => setTimeout(resolve, delay))
|
||||
|
||||
export const refreshDeploymentAppDataWhenReady = async (appId: string): Promise<DeploymentAppData> => {
|
||||
@ -148,7 +144,7 @@ export const refreshDeploymentAppDataWhenReady = async (appId: string): Promise<
|
||||
await wait(delay)
|
||||
|
||||
try {
|
||||
return await refreshDeploymentAppData(appId)
|
||||
return await fetchDeploymentAppData(appId)
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
AccessPermission,
|
||||
ConsoleEnvironmentSummary,
|
||||
} from '@/contract/console/deployments'
|
||||
import { useMemo } from 'react'
|
||||
@ -15,6 +16,8 @@ import { DeveloperApiSection } from './access-tab/developer-api-section'
|
||||
import { AccessPermissionsSection } from './access-tab/permissions-section'
|
||||
import { getUrlOrigin } from './access-tab/url'
|
||||
|
||||
const EMPTY_ACCESS_PERMISSIONS: AccessPermission[] = []
|
||||
|
||||
function uniqueEnvironments(environments: (ConsoleEnvironmentSummary | undefined)[]) {
|
||||
return environments.filter((environment, index): environment is ConsoleEnvironmentSummary => {
|
||||
if (!environment?.id)
|
||||
@ -41,10 +44,7 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
)
|
||||
const policies = useMemo(
|
||||
() => accessConfig?.permissions ?? [],
|
||||
[accessConfig?.permissions],
|
||||
)
|
||||
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
|
||||
const deployedEnvs = useMemo(
|
||||
() => uniqueEnvironments([
|
||||
...deploymentRows.map(row => row.environment),
|
||||
@ -68,7 +68,6 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
|
||||
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
|
||||
const visibleCreatedApiToken = createdApiToken?.appId === appId ? createdApiToken : undefined
|
||||
const webappChannelVersion = 0
|
||||
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
|
||||
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
|
||||
|
||||
@ -85,14 +84,14 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
webappRows={webappRows}
|
||||
cliDomain={cliDomain}
|
||||
cliDocsUrl={cliDocsUrl}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'webapp', enabled, webappChannelVersion)}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'webapp', enabled)}
|
||||
/>
|
||||
<DeveloperApiSection
|
||||
apiEnabled={apiEnabled}
|
||||
environments={deployedEnvs}
|
||||
apiKeys={apiKeys}
|
||||
createdToken={visibleCreatedApiToken?.token}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'api', enabled, 0)}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'api', enabled)}
|
||||
onGenerate={handleGenerateApiKey}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onClearCreatedToken={clearCreatedApiToken}
|
||||
|
||||
@ -13,11 +13,8 @@ type AccessPermissionsSectionProps = {
|
||||
onSetPolicy: (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
channel: string,
|
||||
enabled: boolean,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
expectedVersion: number,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
@ -304,11 +304,8 @@ type EnvironmentPermissionRowProps = {
|
||||
onSetPolicy: (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
channel: string,
|
||||
enabled: boolean,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
expectedVersion: number,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
@ -363,11 +360,8 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
|
||||
await onSetPolicy(
|
||||
appId,
|
||||
environmentId,
|
||||
'webapp',
|
||||
true,
|
||||
permissionKeyToAccessMode(nextKind),
|
||||
nextKind === 'specific' ? policySubjects(nextSubjects) : [],
|
||||
detailPolicy?.version ?? 0,
|
||||
)
|
||||
await policyQuery.refetch()
|
||||
setDraft({})
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
|
||||
@ -20,8 +19,8 @@ type SwitchableTab = 'deploy' | 'versions' | 'access' | 'settings'
|
||||
|
||||
type SectionProps = {
|
||||
title: string
|
||||
action?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
action?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Section: FC<SectionProps> = ({ title, action, children }) => (
|
||||
@ -36,7 +35,7 @@ const Section: FC<SectionProps> = ({ title, action, children }) => (
|
||||
|
||||
type InfoRowProps = {
|
||||
label: string
|
||||
value: React.ReactNode
|
||||
value: ReactNode
|
||||
mono?: boolean
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
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'
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import type { EnvironmentDeploymentRow, ReleaseHistoryRow } from '@/contract/console/deployments'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getReleaseDeployments } from '../release-deployments'
|
||||
|
||||
describe('getReleaseDeployments', () => {
|
||||
it('should prefer runtime deployment state when history has the same environment', () => {
|
||||
// Arrange
|
||||
const releaseRow = {
|
||||
id: 'release-1',
|
||||
deployedTo: [
|
||||
{
|
||||
environmentId: 'env-1',
|
||||
environmentName: 'Production',
|
||||
instanceStatus: 'failed',
|
||||
},
|
||||
],
|
||||
} satisfies ReleaseHistoryRow
|
||||
const deploymentRows = [
|
||||
{
|
||||
id: 'deployment-1',
|
||||
environment: {
|
||||
id: 'env-1',
|
||||
name: 'Production',
|
||||
},
|
||||
status: 'ready',
|
||||
currentRelease: {
|
||||
id: 'release-1',
|
||||
},
|
||||
},
|
||||
] satisfies EnvironmentDeploymentRow[]
|
||||
|
||||
// Act
|
||||
const result = getReleaseDeployments(releaseRow, deploymentRows)
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{
|
||||
environmentId: 'env-1',
|
||||
environmentName: 'Production',
|
||||
state: 'active',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should merge history deployments with runtime deployments for different environments', () => {
|
||||
// Arrange
|
||||
const releaseRow = {
|
||||
release: {
|
||||
id: 'release-1',
|
||||
},
|
||||
deployedTo: [
|
||||
{
|
||||
environmentId: 'env-1',
|
||||
environmentName: 'Production',
|
||||
instanceStatus: 'ready',
|
||||
},
|
||||
],
|
||||
} satisfies ReleaseHistoryRow
|
||||
const deploymentRows = [
|
||||
{
|
||||
id: 'deployment-2',
|
||||
environment: {
|
||||
id: 'env-2',
|
||||
name: 'Staging',
|
||||
},
|
||||
status: 'deploying',
|
||||
currentRelease: {
|
||||
id: 'release-1',
|
||||
},
|
||||
},
|
||||
] satisfies EnvironmentDeploymentRow[]
|
||||
|
||||
// Act
|
||||
const result = getReleaseDeployments(releaseRow, deploymentRows)
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{
|
||||
environmentId: 'env-2',
|
||||
environmentName: 'Staging',
|
||||
state: 'deploying',
|
||||
},
|
||||
{
|
||||
environmentId: 'env-1',
|
||||
environmentName: 'Production',
|
||||
state: 'active',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should return no deployments when the release row has no release id', () => {
|
||||
// Arrange
|
||||
const releaseRow = {
|
||||
deployedTo: [
|
||||
{
|
||||
environmentId: 'env-1',
|
||||
environmentName: 'Production',
|
||||
instanceStatus: 'ready',
|
||||
},
|
||||
],
|
||||
} satisfies ReleaseHistoryRow
|
||||
|
||||
// Act
|
||||
const result = getReleaseDeployments(releaseRow, [])
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
@ -36,8 +36,7 @@ function fromDeployedTo(item: DeployedToSummary): ReleaseDeployment | undefined
|
||||
|
||||
function dedupeReleaseDeployments(items: ReleaseDeployment[]) {
|
||||
return items.filter((item, index) => {
|
||||
const key = `${item.environmentId}-${item.state}`
|
||||
return items.findIndex(candidate => `${candidate.environmentId}-${candidate.state}` === key) === index
|
||||
return items.findIndex(candidate => candidate.environmentId === item.environmentId) === index
|
||||
})
|
||||
}
|
||||
|
||||
@ -63,5 +62,5 @@ export function getReleaseDeployments(row: ReleaseHistoryRow, deploymentRows: En
|
||||
return items
|
||||
})
|
||||
|
||||
return dedupeReleaseDeployments([...historyItems, ...runtimeItems])
|
||||
return dedupeReleaseDeployments([...runtimeItems, ...historyItems])
|
||||
}
|
||||
|
||||
@ -90,10 +90,6 @@ const DeploymentsMain: FC = () => {
|
||||
]
|
||||
}, [environments, t])
|
||||
|
||||
const visibleInstances = useMemo(() => {
|
||||
return apps
|
||||
}, [apps])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
@ -117,7 +113,7 @@ const DeploymentsMain: FC = () => {
|
||||
</div>
|
||||
<div className="relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
<NewInstanceCard onOpen={openCreateInstanceModal} />
|
||||
{visibleInstances.map(app => (
|
||||
{apps.map(app => (
|
||||
<InstanceCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
listAppDeployments,
|
||||
patchAccessChannel,
|
||||
patchDeveloperAPI,
|
||||
refreshDeploymentAppData,
|
||||
refreshDeploymentAppDataWhenReady,
|
||||
toAppInfoFromOverview,
|
||||
toAppInfoFromSummary,
|
||||
@ -76,7 +75,6 @@ export type DeploymentsAction = {
|
||||
|
||||
createInstance: (params: CreateInstanceParams) => Promise<CreateInstanceResult>
|
||||
updateInstance: (appId: string, patch: Pick<AppInfo, 'name' | 'description'>) => Promise<void>
|
||||
switchSourceApp: (appId: string, nextAppId: string) => void
|
||||
deleteInstance: (appId: string) => Promise<void>
|
||||
|
||||
startDeploy: (params: StartDeployParams) => Promise<void>
|
||||
@ -87,15 +85,12 @@ export type DeploymentsAction = {
|
||||
generateApiKey: (appId: string, environmentId: string) => Promise<void>
|
||||
revokeApiKey: (appId: string, environmentId: string, apiKeyId: string) => Promise<void>
|
||||
clearCreatedApiToken: () => void
|
||||
toggleAccessChannel: (appId: string, channel: string, enabled: boolean, expectedVersion: number) => Promise<void>
|
||||
toggleAccessChannel: (appId: string, channel: string, enabled: boolean) => Promise<void>
|
||||
setEnvironmentAccessPolicy: (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
channel: string,
|
||||
enabled: boolean,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
expectedVersion: number,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
@ -203,7 +198,7 @@ class DeploymentsActionImpl implements DeploymentsAction {
|
||||
}
|
||||
|
||||
refreshAppData = async (appId: string) => {
|
||||
const data = await refreshDeploymentAppData(appId)
|
||||
const data = await fetchDeploymentAppData(appId)
|
||||
this.applyAppData(data)
|
||||
const app = toAppInfoFromOverview(data.overview.instance)
|
||||
if (app)
|
||||
@ -265,8 +260,6 @@ class DeploymentsActionImpl implements DeploymentsAction {
|
||||
}))
|
||||
}
|
||||
|
||||
switchSourceApp = () => undefined
|
||||
|
||||
deleteInstance = async (appId: string) => {
|
||||
await deleteAppInstance(appId)
|
||||
this.#set((state) => {
|
||||
@ -359,8 +352,6 @@ class DeploymentsActionImpl implements DeploymentsAction {
|
||||
setEnvironmentAccessPolicy = async (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
_channel: string,
|
||||
_enabled: boolean,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user