mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
tweaks
This commit is contained in:
parent
d02c80e220
commit
7f2d094cf3
@ -2,11 +2,17 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import { useStartDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { environmentOptionsFromList } from '../utils'
|
||||
import { DeployForm } from './deploy-drawer/form'
|
||||
|
||||
const DeployDrawer: FC = () => {
|
||||
@ -16,13 +22,31 @@ const DeployDrawer: FC = () => {
|
||||
const closeDeployDrawer = useDeploymentsStore(state => state.closeDeployDrawer)
|
||||
const startDeploy = useStartDeployment()
|
||||
const open = drawer.open
|
||||
const { environmentOptions } = useSourceApps({ enabled: open })
|
||||
const { data: appData } = useDeploymentAppData(drawerAppId, {
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
enabled: open,
|
||||
}))
|
||||
const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({
|
||||
input: drawerAppId
|
||||
? {
|
||||
params: { appInstanceId: drawerAppId },
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
enabled: open && Boolean(drawerAppId),
|
||||
})
|
||||
}))
|
||||
|
||||
const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data])
|
||||
const environments = environmentOptions
|
||||
const releases = appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => release.id) ?? []
|
||||
const releases = releaseHistory?.data?.map(row => row.release ?? row).filter(release => release.id) ?? []
|
||||
const defaultReleaseId = releases[0]?.id
|
||||
const formKey = `${drawer.appId ?? 'none'}-${drawer.environmentId ?? 'any'}-${drawer.releaseId ?? 'new'}-${open ? '1' : '0'}`
|
||||
|
||||
@ -35,7 +59,7 @@ const DeployDrawer: FC = () => {
|
||||
<DialogCloseButton />
|
||||
{!drawerAppId
|
||||
? <div className="p-4 text-text-tertiary">{t('deployDrawer.notFound')}</div>
|
||||
: !appData
|
||||
: !releaseHistory
|
||||
? (
|
||||
<div className="flex items-center gap-2 p-4 system-sm-regular text-text-tertiary">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-components-panel-border border-t-transparent" />
|
||||
|
||||
@ -112,7 +112,6 @@ export const DeployForm: FC<DeployFormProps> = ({
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
staleTime: 30 * 1000,
|
||||
}))
|
||||
const previewBindings = releasePreview.data?.bindings ?? []
|
||||
const modelBindings = previewBindings.filter(isRuntimeModelBinding)
|
||||
|
||||
@ -9,19 +9,27 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import { useStartDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
environmentId,
|
||||
environmentName,
|
||||
environmentOptionsFromList,
|
||||
releaseCommit,
|
||||
releaseLabel,
|
||||
sourceAppMapFromApps,
|
||||
sourceAppsFromList,
|
||||
toAppInfoFromOverview,
|
||||
} from '../utils'
|
||||
|
||||
const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => {
|
||||
@ -36,20 +44,54 @@ const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => {
|
||||
const RollbackModal: FC = () => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const modal = useDeploymentsStore(state => state.rollbackModal)
|
||||
const { data: appData } = useCachedDeploymentAppData(modal.appId)
|
||||
const closeRollbackModal = useDeploymentsStore(state => state.closeRollbackModal)
|
||||
const rollbackDeployment = useStartDeployment()
|
||||
const { appMap, environmentOptions } = useSourceApps()
|
||||
const appInput = modal.appId
|
||||
? { params: { appInstanceId: modal.appId } }
|
||||
: undefined
|
||||
const pagedInput = appInput
|
||||
? {
|
||||
...appInput,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({
|
||||
input: appInput ?? skipToken,
|
||||
enabled: modal.open && Boolean(modal.appId),
|
||||
}))
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: pagedInput ?? skipToken,
|
||||
enabled: modal.open && Boolean(modal.appId),
|
||||
}))
|
||||
const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({
|
||||
input: pagedInput ?? skipToken,
|
||||
enabled: modal.open && Boolean(modal.appId),
|
||||
}))
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
enabled: modal.open,
|
||||
}))
|
||||
const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps])
|
||||
const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data])
|
||||
|
||||
const currentRow = deployedRows(appData?.environmentDeployments.data)
|
||||
const currentRow = deployedRows(environmentDeployments?.data)
|
||||
.find(row => environmentId(row.environment) === modal.environmentId)
|
||||
const targetRelease = [
|
||||
...(appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => !!release?.id) ?? []),
|
||||
...(releaseHistory?.data?.map(row => row.release ?? row).filter(release => !!release?.id) ?? []),
|
||||
].find(release => release?.id === modal.targetReleaseId)
|
||||
const currentRelease = activeRelease(currentRow)
|
||||
const environment = currentRow?.environment
|
||||
?? environmentOptions.find(env => env.id === modal.environmentId)
|
||||
const app = toAppInfoFromOverview(appData?.overview.instance)
|
||||
const app = toAppInfoFromOverview(overview?.instance)
|
||||
?? (modal.appId ? appMap.get(modal.appId) : undefined)
|
||||
|
||||
const confirm = () => {
|
||||
|
||||
@ -1,346 +1,2 @@
|
||||
import type { AppInfo, AppMode } from './types'
|
||||
import type {
|
||||
AccessSubject,
|
||||
AppDeploymentSummary,
|
||||
AppInstanceOverview,
|
||||
ConsoleReleaseSummary,
|
||||
CreateAppInstanceReply,
|
||||
GetAccessConfigReply,
|
||||
GetDeploymentOverviewReply,
|
||||
ListAppDeploymentsReply,
|
||||
ListEnvironmentDeploymentsReply,
|
||||
ListReleaseHistoryReply,
|
||||
} from '@/contract/console/deployments'
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { consoleClient } from '@/service/client'
|
||||
|
||||
const DEPLOYMENT_PAGE_SIZE = 100
|
||||
const DEPLOYMENT_APP_DATA_STALE_TIME = 30 * 1000
|
||||
const DEPLOYMENT_READINESS_RETRY_DELAYS = [0, 300, 700, 1200]
|
||||
|
||||
export type DeploymentAppData = {
|
||||
appId: string
|
||||
overview: GetDeploymentOverviewReply
|
||||
environmentDeployments: ListEnvironmentDeploymentsReply
|
||||
releaseHistory: ListReleaseHistoryReply
|
||||
accessConfig: GetAccessConfigReply
|
||||
}
|
||||
|
||||
export type CreateDeploymentParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
releaseId?: string
|
||||
releaseNote?: string
|
||||
}
|
||||
|
||||
export type CreateInstanceParams = {
|
||||
sourceAppId: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type UpdateInstanceParams = {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type ListAppDeploymentsQuery = {
|
||||
environmentId?: string
|
||||
notDeployed?: boolean
|
||||
query?: string
|
||||
pageNumber?: number
|
||||
resultsPerPage?: number
|
||||
}
|
||||
|
||||
export function toAppInfoFromSummary(summary: AppDeploymentSummary): AppInfo | undefined {
|
||||
if (!summary.id || !summary.name)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id: summary.id,
|
||||
name: summary.name,
|
||||
mode: (summary.mode || 'workflow') as AppMode,
|
||||
iconType: 'emoji',
|
||||
icon: summary.icon,
|
||||
description: summary.description ?? undefined,
|
||||
sourceAppId: summary.sourceAppId,
|
||||
sourceAppName: summary.sourceAppName,
|
||||
}
|
||||
}
|
||||
|
||||
export function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | undefined {
|
||||
if (!instance?.id)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
name: instance.name ?? instance.id,
|
||||
mode: (instance.mode || 'workflow') as AppMode,
|
||||
iconType: 'emoji',
|
||||
icon: instance.icon,
|
||||
description: instance.description ?? undefined,
|
||||
sourceAppId: instance.sourceAppId,
|
||||
sourceAppName: instance.sourceAppName,
|
||||
}
|
||||
}
|
||||
|
||||
export const deploymentAppDataQueryKey = (appId: string) => ['console', 'deployments', 'app-data', appId] as const
|
||||
|
||||
export const listAppDeployments = async (query: ListAppDeploymentsQuery): Promise<ListAppDeploymentsReply> => {
|
||||
return consoleClient.deployments.list({
|
||||
query,
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchDeploymentAppData = async (appId: string): Promise<DeploymentAppData> => {
|
||||
const input = { params: { appInstanceId: appId } }
|
||||
const [
|
||||
overview,
|
||||
environmentDeployments,
|
||||
releaseHistory,
|
||||
accessConfig,
|
||||
] = await Promise.all([
|
||||
consoleClient.deployments.overview(input),
|
||||
consoleClient.deployments.environmentDeployments({
|
||||
...input,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
}),
|
||||
consoleClient.deployments.releaseHistory({
|
||||
...input,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
}),
|
||||
consoleClient.deployments.accessConfig(input),
|
||||
])
|
||||
|
||||
return {
|
||||
appId,
|
||||
overview,
|
||||
environmentDeployments,
|
||||
releaseHistory,
|
||||
accessConfig,
|
||||
}
|
||||
}
|
||||
|
||||
export const deploymentAppDataQueryOptions = (appId: string) =>
|
||||
queryOptions<DeploymentAppData>({
|
||||
queryKey: deploymentAppDataQueryKey(appId),
|
||||
queryFn: () => fetchDeploymentAppData(appId),
|
||||
staleTime: DEPLOYMENT_APP_DATA_STALE_TIME,
|
||||
})
|
||||
|
||||
const wait = (delay: number) => new Promise(resolve => setTimeout(resolve, delay))
|
||||
|
||||
export const refreshDeploymentAppDataWhenReady = async (appId: string): Promise<DeploymentAppData> => {
|
||||
let lastError: unknown
|
||||
|
||||
for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) {
|
||||
if (delay > 0)
|
||||
await wait(delay)
|
||||
|
||||
try {
|
||||
return await fetchDeploymentAppData(appId)
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
export const waitForAppInstanceInDeploymentList = async (appInstanceId: string): Promise<ListAppDeploymentsReply | undefined> => {
|
||||
let lastError: unknown
|
||||
|
||||
for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) {
|
||||
if (delay > 0)
|
||||
await wait(delay)
|
||||
|
||||
try {
|
||||
const response = await listAppDeployments({
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
})
|
||||
if (response.data?.some(app => app.id === appInstanceId))
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError)
|
||||
throw lastError
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const createRelease = async (appId: string, releaseNote?: string): Promise<ConsoleReleaseSummary> => {
|
||||
const trimmedReleaseNote = releaseNote?.trim()
|
||||
const response = await consoleClient.deployments.createRelease({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
name: trimmedReleaseNote || 'Release',
|
||||
description: trimmedReleaseNote || undefined,
|
||||
},
|
||||
})
|
||||
if (!response.release)
|
||||
throw new Error('Create release did not return a release.')
|
||||
return response.release
|
||||
}
|
||||
|
||||
export const createDeployment = async ({
|
||||
appId,
|
||||
environmentId,
|
||||
releaseId,
|
||||
releaseNote,
|
||||
}: CreateDeploymentParams) => {
|
||||
let targetReleaseId = releaseId
|
||||
await consoleClient.deployments.previewRelease({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
releaseId: targetReleaseId,
|
||||
},
|
||||
})
|
||||
if (!targetReleaseId) {
|
||||
const release = await createRelease(appId, releaseNote)
|
||||
targetReleaseId = release.id
|
||||
}
|
||||
if (!targetReleaseId)
|
||||
throw new Error('Failed to create a deployable release.')
|
||||
|
||||
return consoleClient.deployments.createDeployment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
environmentId,
|
||||
releaseId: targetReleaseId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const cancelDeployment = async (appId: string, runtimeInstanceId: string) => {
|
||||
return consoleClient.deployments.cancelDeployment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
runtimeInstanceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const undeployEnvironment = async (appId: string, runtimeInstanceId: string) => {
|
||||
return consoleClient.deployments.undeployEnvironment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
runtimeInstanceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createApiKey = async (appId: string, environmentId: string, name: string) => {
|
||||
return consoleClient.deployments.createEnvironmentAPIToken({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
environmentId,
|
||||
name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteApiKey = async (appId: string, apiKeyId: string) => {
|
||||
return consoleClient.deployments.deleteEnvironmentAPIToken({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const patchAccessChannel = async (appId: string, enabled: boolean) => {
|
||||
return consoleClient.deployments.patchAccessChannel({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const patchDeveloperAPI = async (appId: string, enabled: boolean) => {
|
||||
return consoleClient.deployments.patchDeveloperAPI({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateEnvironmentAccessPolicy = async (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[] = [],
|
||||
) => {
|
||||
return consoleClient.deployments.updateEnvironmentAccessPolicy({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
environmentId,
|
||||
},
|
||||
body: {
|
||||
accessMode,
|
||||
subjects,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createAppInstance = async ({
|
||||
sourceAppId,
|
||||
name,
|
||||
description,
|
||||
}: CreateInstanceParams): Promise<CreateAppInstanceReply> => {
|
||||
return consoleClient.deployments.createInstance({
|
||||
body: {
|
||||
sourceAppId,
|
||||
name,
|
||||
description,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateAppInstance = async (
|
||||
appId: string,
|
||||
{ name, description }: UpdateInstanceParams,
|
||||
) => {
|
||||
return consoleClient.deployments.updateInstance({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteAppInstance = async (appId: string) => {
|
||||
return consoleClient.deployments.deleteInstance({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
})
|
||||
}
|
||||
export const DEPLOYMENT_PAGE_SIZE = 100
|
||||
export const SOURCE_APPS_PAGE_SIZE = 100
|
||||
|
||||
@ -6,8 +6,10 @@ import type {
|
||||
AccessSubject,
|
||||
ConsoleEnvironmentSummary,
|
||||
} from '@/contract/console/deployments'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../data'
|
||||
import {
|
||||
useGenerateDeploymentApiKey,
|
||||
useRevokeDeploymentApiKey,
|
||||
@ -37,7 +39,19 @@ type AccessTabProps = {
|
||||
}
|
||||
|
||||
const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const appInput = { params: { appInstanceId: appId } }
|
||||
const { data: accessConfig } = useQuery(consoleQuery.deployments.accessConfig.queryOptions({
|
||||
input: appInput,
|
||||
}))
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: {
|
||||
...appInput,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const [createdApiToken, setCreatedApiToken] = useState<{
|
||||
appId: string
|
||||
token: string
|
||||
@ -47,10 +61,9 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
const toggleAccessChannel = useToggleDeploymentAccessChannel()
|
||||
const setEnvironmentAccessPolicy = useSetEnvironmentAccessPolicy()
|
||||
|
||||
const accessConfig = appData?.accessConfig
|
||||
const deploymentRows = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
() => deployedRows(environmentDeployments?.data),
|
||||
[environmentDeployments?.data],
|
||||
)
|
||||
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
|
||||
const deployedEnvs = useMemo(
|
||||
@ -63,9 +76,17 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
)
|
||||
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
|
||||
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
|
||||
const createApiKeyLabel = (environmentId: string) => {
|
||||
const existingCount = apiKeys.filter(key =>
|
||||
(key.environmentId ?? key.environment?.id) === environmentId,
|
||||
).length
|
||||
const name = deployedEnvs.find(env => env.id === environmentId)?.name ?? 'env'
|
||||
|
||||
return `${name}-key-${String(existingCount + 1).padStart(3, '0')}`
|
||||
}
|
||||
const handleGenerateApiKey = (environmentId: string) => {
|
||||
generateApiKey.mutate(
|
||||
{ appId, environmentId },
|
||||
{ appId, environmentId, name: createApiKeyLabel(environmentId) },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
if (response.apiToken?.token)
|
||||
|
||||
@ -200,7 +200,6 @@ const SubjectPicker: FC<SubjectPickerProps> = ({
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
staleTime: 30 * 1000,
|
||||
}))
|
||||
const subjects = useMemo(
|
||||
() => subjectsQuery.data?.data
|
||||
@ -326,7 +325,6 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
staleTime: 30 * 1000,
|
||||
}))
|
||||
const detailPolicy = policyQuery.data?.policy
|
||||
const policyKind = accessModeToPermissionKey(detailPolicy?.accessMode ?? summaryPolicy?.accessMode)
|
||||
|
||||
@ -8,11 +8,15 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import { useUndeployDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import {
|
||||
activeRelease,
|
||||
@ -23,6 +27,7 @@ import {
|
||||
environmentId,
|
||||
environmentMode,
|
||||
environmentName,
|
||||
environmentOptionsFromList,
|
||||
isUndeployedDeploymentRow,
|
||||
releaseCommit,
|
||||
releaseLabel,
|
||||
@ -38,18 +43,34 @@ type DeployTabProps = {
|
||||
|
||||
const DeployTab: FC<DeployTabProps> = ({ instanceId: appId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: {
|
||||
params: { appInstanceId: appId },
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const undeployDeployment = useUndeployDeployment()
|
||||
const { environmentOptions } = useSourceApps()
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data])
|
||||
|
||||
const rows = useMemo(
|
||||
() => appData?.environmentDeployments.data?.filter(row => row.environment?.id) ?? [],
|
||||
[appData?.environmentDeployments.data],
|
||||
() => environmentDeployments?.data?.filter(row => row.environment?.id) ?? [],
|
||||
[environmentDeployments?.data],
|
||||
)
|
||||
const deployedRuntimeRows = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
() => deployedRows(environmentDeployments?.data),
|
||||
[environmentDeployments?.data],
|
||||
)
|
||||
|
||||
const deployedEnvIds = new Set(deployedRuntimeRows.map(row => environmentId(row.environment)))
|
||||
|
||||
@ -3,17 +3,23 @@
|
||||
import type { FC, 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'
|
||||
import { useRouter, useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import DeployDrawer from '../components/deploy-drawer'
|
||||
import RollbackModal from '../components/rollback-modal'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { deployedRows, deploymentStatus } from '../utils'
|
||||
import {
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import {
|
||||
deploymentSummariesFromList,
|
||||
sourceAppMapFromApps,
|
||||
sourceAppsFromList,
|
||||
} from '../utils'
|
||||
import { DeploymentSidebar } from './deployment-sidebar'
|
||||
import { isInstanceDetailTabKey } from './tabs'
|
||||
|
||||
@ -29,25 +35,31 @@ const InstanceDetail: FC<InstanceDetailProps> = ({ instanceId, children }) => {
|
||||
const selectedSegment = useSelectedLayoutSegment()
|
||||
const selectedTab = selectedSegment ?? undefined
|
||||
const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview'
|
||||
const detailQuery = useDeploymentAppData(instanceId, { enabled: Boolean(instanceId) })
|
||||
const appData = detailQuery.data
|
||||
const { appMap, isLoading: isLoadingApps } = useSourceApps()
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
useDocumentTitle(t('documentTitle.detail'))
|
||||
|
||||
const detailApp = useMemo(
|
||||
() => toAppInfoFromOverview(appData?.overview.instance),
|
||||
[appData?.overview.instance],
|
||||
)
|
||||
const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
const appMap = useMemo(() => sourceAppMapFromApps(apps), [apps])
|
||||
const summaries = useMemo(() => deploymentSummariesFromList(listQuery.data), [listQuery.data])
|
||||
const app = useMemo(
|
||||
() => detailApp ?? appMap.get(instanceId),
|
||||
[detailApp, instanceId, appMap],
|
||||
)
|
||||
const appDeployments = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
() => appMap.get(instanceId),
|
||||
[instanceId, appMap],
|
||||
)
|
||||
const summary = summaries[instanceId]
|
||||
const statusCount = (status: string) =>
|
||||
summary?.statuses?.find(item => item.status === status)?.count ?? 0
|
||||
const envCount = summary?.statuses
|
||||
?.filter(item => item.status !== 'undeployed')
|
||||
.reduce((total, item) => total + (item.count ?? 0), 0) ?? 0
|
||||
|
||||
if (!app && (isLoadingApps || detailQuery.isLoading || detailQuery.isFetching)) {
|
||||
if (!app && listQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<span className="h-6 w-6 animate-spin rounded-full border-2 border-components-panel-border border-t-transparent" />
|
||||
@ -67,8 +79,8 @@ const InstanceDetail: FC<InstanceDetailProps> = ({ instanceId, children }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const deployingCount = appDeployments.filter(row => deploymentStatus(row) === 'deploying').length
|
||||
const failedCount = appDeployments.filter(row => deploymentStatus(row) === 'deploy_failed').length
|
||||
const deployingCount = statusCount('deploying')
|
||||
const failedCount = statusCount('failed') + statusCount('deploy_failed')
|
||||
const appModeLabel = app ? getAppModeLabel(app.mode, tCommon) : t('detail.sourceAppDeleted')
|
||||
|
||||
return (
|
||||
@ -92,7 +104,7 @@ const InstanceDetail: FC<InstanceDetailProps> = ({ instanceId, children }) => {
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`tabs.${activeTab}.description`)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 system-xs-regular text-text-tertiary">
|
||||
<span>{t('detail.envCount', { count: appDeployments.length })}</span>
|
||||
<span>{t('detail.envCount', { count: envCount })}</span>
|
||||
{deployingCount > 0 && (
|
||||
<>
|
||||
<span>·</span>
|
||||
|
||||
@ -2,16 +2,24 @@
|
||||
import type { FC, 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'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { StatusBadge } from '../components/status-badge'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import {
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { releaseLabel, webappUrl } from '../utils'
|
||||
import {
|
||||
releaseLabel,
|
||||
sourceAppMapFromApps,
|
||||
sourceAppsFromList,
|
||||
toAppInfoFromOverview,
|
||||
webappUrl,
|
||||
} from '../utils'
|
||||
|
||||
type OverviewTabProps = {
|
||||
instanceId: string
|
||||
@ -92,10 +100,24 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { t: tCommon } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { data: appData } = useCachedDeploymentAppData(instanceId)
|
||||
const input = { params: { appInstanceId: instanceId } }
|
||||
const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({
|
||||
input,
|
||||
}))
|
||||
const { data: accessConfig } = useQuery(consoleQuery.deployments.accessConfig.queryOptions({
|
||||
input,
|
||||
}))
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const { appMap } = useSourceApps()
|
||||
const overview = appData?.overview
|
||||
const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps])
|
||||
const app = toAppInfoFromOverview(overview?.instance) ?? appMap.get(instanceId)
|
||||
const overviewApp = overview?.instance
|
||||
const deployments = useMemo(
|
||||
@ -113,7 +135,7 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
|
||||
const appModeLabel = getAppModeLabel(overviewApp?.mode ?? app.mode, tCommon)
|
||||
const webappAccessUrl = webappUrl(overview?.access?.webappUrl)
|
||||
const cliUrl = overview?.access?.cliUrl
|
||||
const apiKeysCount = overview?.access?.apiKeyCount ?? appData?.accessConfig.developerApi?.apiKeys?.length ?? 0
|
||||
const apiKeysCount = overview?.access?.apiKeyCount ?? accessConfig?.developerApi?.apiKeys?.length ?? 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 p-6">
|
||||
|
||||
@ -14,18 +14,24 @@ import {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import {
|
||||
useDeleteDeploymentInstance,
|
||||
useUpdateDeploymentInstance,
|
||||
} from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { deployedRows } from '../utils'
|
||||
import {
|
||||
deployedRows,
|
||||
sourceAppMapFromApps,
|
||||
sourceAppsFromList,
|
||||
toAppInfoFromOverview,
|
||||
} from '../utils'
|
||||
|
||||
type SettingsTabProps = {
|
||||
instanceId: string
|
||||
@ -181,24 +187,40 @@ const SettingsForm: FC<SettingsFormProps> = ({ app, settings, hasDeployments, on
|
||||
|
||||
const SettingsTab: FC<SettingsTabProps> = ({ instanceId }) => {
|
||||
const router = useRouter()
|
||||
const { data: appData } = useCachedDeploymentAppData(instanceId)
|
||||
const updateInstance = useUpdateDeploymentInstance()
|
||||
const deleteInstance = useDeleteDeploymentInstance()
|
||||
const { appMap } = useSourceApps()
|
||||
const app = toAppInfoFromOverview(appData?.overview.instance) ?? appMap.get(instanceId)
|
||||
const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({
|
||||
const appInput = { params: { appInstanceId: instanceId } }
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: instanceId,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
}))
|
||||
const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({
|
||||
input: appInput,
|
||||
}))
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: {
|
||||
...appInput,
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps])
|
||||
const app = toAppInfoFromOverview(overview?.instance) ?? appMap.get(instanceId)
|
||||
const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({
|
||||
input: appInput,
|
||||
}))
|
||||
|
||||
if (!app)
|
||||
return null
|
||||
|
||||
const hasDeployments = deployedRows(appData?.environmentDeployments.data).length > 0
|
||||
const hasDeployments = deployedRows(environmentDeployments?.data).length > 0
|
||||
const formKey = `${app.id}-${settingsQuery.data?.name ?? app.name}-${settingsQuery.data?.description ?? app.description ?? ''}`
|
||||
|
||||
return (
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../data'
|
||||
import {
|
||||
deployedRows,
|
||||
formatDate,
|
||||
@ -23,14 +25,29 @@ type VersionsTabProps = {
|
||||
|
||||
const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const query = {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
}
|
||||
const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({
|
||||
input: {
|
||||
params: { appInstanceId: appId },
|
||||
query,
|
||||
},
|
||||
}))
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: {
|
||||
params: { appInstanceId: appId },
|
||||
query,
|
||||
},
|
||||
}))
|
||||
const releaseRows = useMemo(
|
||||
() => appData?.releaseHistory.data?.filter(row => (row.release ?? row).id) ?? [],
|
||||
[appData?.releaseHistory.data],
|
||||
() => releaseHistory?.data?.filter(row => (row.release ?? row).id) ?? [],
|
||||
[releaseHistory?.data],
|
||||
)
|
||||
const deploymentRows = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
() => deployedRows(environmentDeployments?.data),
|
||||
[environmentDeployments?.data],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -8,10 +8,14 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCachedDeploymentAppData } from '../../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../../hooks/use-source-apps'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../../data'
|
||||
import { useDeploymentsStore } from '../../store'
|
||||
import {
|
||||
activeRelease,
|
||||
@ -20,6 +24,7 @@ import {
|
||||
deploymentStatus,
|
||||
environmentId,
|
||||
environmentName,
|
||||
environmentOptionsFromList,
|
||||
} from '../../utils'
|
||||
|
||||
type DeployReleaseMenuProps = {
|
||||
@ -29,14 +34,32 @@ type DeployReleaseMenuProps = {
|
||||
|
||||
export const DeployReleaseMenu: FC<DeployReleaseMenuProps> = ({ appId, releaseId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal)
|
||||
const [open, setOpen] = useState(false)
|
||||
const { environmentOptions } = useSourceApps({ enabled: open })
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
enabled: open,
|
||||
}))
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({
|
||||
input: {
|
||||
params: { appInstanceId: appId },
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
enabled: open,
|
||||
}))
|
||||
|
||||
const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data])
|
||||
const environments = environmentOptions.filter(env => env.id)
|
||||
const deploymentRows = deployedRows(appData?.environmentDeployments.data)
|
||||
const deploymentRows = deployedRows(environmentDeployments?.data)
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
deploymentAppDataQueryOptions,
|
||||
toAppInfoFromOverview,
|
||||
} from '../data'
|
||||
|
||||
type UseDeploymentDataOptions = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useDeploymentAppData(appId?: string, options: UseDeploymentDataOptions = {}) {
|
||||
const { enabled = true } = options
|
||||
|
||||
return useQuery({
|
||||
...deploymentAppDataQueryOptions(appId ?? ''),
|
||||
enabled: enabled && Boolean(appId),
|
||||
})
|
||||
}
|
||||
|
||||
export function useCachedDeploymentAppData(appId?: string) {
|
||||
return useQuery({
|
||||
...deploymentAppDataQueryOptions(appId ?? ''),
|
||||
enabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeploymentAppInfo(appId?: string, options: UseDeploymentDataOptions = {}) {
|
||||
const query = useDeploymentAppData(appId, options)
|
||||
const app = useMemo(
|
||||
() => toAppInfoFromOverview(query.data?.overview.instance),
|
||||
[query.data?.overview.instance],
|
||||
)
|
||||
|
||||
return {
|
||||
...query,
|
||||
data: app,
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
CreateDeploymentParams,
|
||||
CreateInstanceParams,
|
||||
DeploymentAppData,
|
||||
UpdateInstanceParams,
|
||||
} from '../data'
|
||||
import type { AccessSubject, ConsoleReleaseSummary } from '@/contract/console/deployments'
|
||||
AccessSubject,
|
||||
ConsoleReleaseSummary,
|
||||
} from '@/contract/console/deployments'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
cancelDeployment,
|
||||
createApiKey,
|
||||
createAppInstance,
|
||||
createDeployment,
|
||||
deleteApiKey,
|
||||
deleteAppInstance,
|
||||
deploymentAppDataQueryKey,
|
||||
patchAccessChannel,
|
||||
patchDeveloperAPI,
|
||||
refreshDeploymentAppDataWhenReady,
|
||||
undeployEnvironment,
|
||||
updateAppInstance,
|
||||
updateEnvironmentAccessPolicy,
|
||||
waitForAppInstanceInDeploymentList,
|
||||
} from '../data'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../data'
|
||||
|
||||
export type CreateDeploymentInstanceResult = {
|
||||
appInstanceId: string
|
||||
initialRelease?: ConsoleReleaseSummary
|
||||
appData?: DeploymentAppData
|
||||
}
|
||||
|
||||
type CreateDeploymentParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
releaseId?: string
|
||||
releaseNote?: string
|
||||
}
|
||||
|
||||
type CreateInstanceParams = {
|
||||
sourceAppId: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
type UpdateInstanceParams = {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
type UpdateDeploymentInstanceParams = {
|
||||
@ -45,9 +44,12 @@ type UndeployDeploymentParams = {
|
||||
type GenerateApiKeyParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type RevokeApiKeyParams = GenerateApiKeyParams & {
|
||||
type RevokeApiKeyParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
apiKeyId: string
|
||||
}
|
||||
|
||||
@ -64,22 +66,9 @@ type SetEnvironmentAccessPolicyParams = {
|
||||
subjects: AccessSubject[]
|
||||
}
|
||||
|
||||
const createApiKeyLabel = (
|
||||
appData: DeploymentAppData | undefined,
|
||||
environmentId: string,
|
||||
) => {
|
||||
const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key =>
|
||||
(key.environmentId ?? key.environment?.id) === environmentId,
|
||||
).length ?? 0
|
||||
const environmentName = appData
|
||||
?.environmentDeployments
|
||||
.data
|
||||
?.find(row => row.environment?.id === environmentId)
|
||||
?.environment
|
||||
?.name ?? 'env'
|
||||
const DEPLOYMENT_READINESS_RETRY_DELAYS = [0, 300, 700, 1200]
|
||||
|
||||
return `${environmentName}-key-${String(existingCount + 1).padStart(3, '0')}`
|
||||
}
|
||||
const wait = (delay: number) => new Promise(resolve => setTimeout(resolve, delay))
|
||||
|
||||
export const useCreateDeploymentInstance = () => {
|
||||
const queryClient = useQueryClient()
|
||||
@ -87,37 +76,39 @@ export const useCreateDeploymentInstance = () => {
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createInstance.mutationKey(),
|
||||
mutationFn: async (params: CreateInstanceParams): Promise<CreateDeploymentInstanceResult> => {
|
||||
const response = await createAppInstance(params)
|
||||
const response = await consoleClient.deployments.createInstance({
|
||||
body: {
|
||||
sourceAppId: params.sourceAppId,
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
},
|
||||
})
|
||||
if (!response.appInstanceId)
|
||||
throw new Error('Create app instance did not return an appInstanceId.')
|
||||
|
||||
const [appData] = await Promise.all([
|
||||
refreshDeploymentAppDataWhenReady(response.appInstanceId).catch(() => undefined),
|
||||
waitForAppInstanceInDeploymentList(response.appInstanceId).catch(() => undefined),
|
||||
])
|
||||
for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) {
|
||||
if (delay > 0)
|
||||
await wait(delay)
|
||||
|
||||
const listResponse = await consoleClient.deployments.list({
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: DEPLOYMENT_PAGE_SIZE,
|
||||
},
|
||||
}).catch(() => undefined)
|
||||
if (listResponse?.data?.some(app => app.id === response.appInstanceId))
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
appInstanceId: response.appInstanceId,
|
||||
initialRelease: response.initialRelease,
|
||||
appData,
|
||||
}
|
||||
},
|
||||
onSuccess: async (result) => {
|
||||
if (result.appData) {
|
||||
queryClient.setQueryData(
|
||||
deploymentAppDataQueryKey(result.appInstanceId),
|
||||
result.appData,
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(result.appInstanceId),
|
||||
}),
|
||||
])
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -128,25 +119,19 @@ export const useUpdateDeploymentInstance = () => {
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.updateInstance.mutationKey(),
|
||||
mutationFn: ({ appId, ...patch }: UpdateDeploymentInstanceParams) =>
|
||||
updateAppInstance(appId, patch),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.settings.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: variables.appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
consoleClient.deployments.updateInstance({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
name: patch.name,
|
||||
description: patch.description,
|
||||
},
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -156,22 +141,14 @@ export const useDeleteDeploymentInstance = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.deleteInstance.mutationKey(),
|
||||
mutationFn: (appId: string) => deleteAppInstance(appId),
|
||||
onSuccess: async (_data, appId) => {
|
||||
queryClient.removeQueries({
|
||||
queryKey: deploymentAppDataQueryKey(appId),
|
||||
})
|
||||
queryClient.removeQueries({
|
||||
queryKey: consoleQuery.deployments.settings.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
mutationFn: (appId: string) => consoleClient.deployments.deleteInstance({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
@ -182,16 +159,55 @@ export const useStartDeployment = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createDeployment.mutationKey(),
|
||||
mutationFn: (params: CreateDeploymentParams) => createDeployment(params),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
environmentId,
|
||||
releaseId,
|
||||
releaseNote,
|
||||
}: CreateDeploymentParams) => {
|
||||
let targetReleaseId = releaseId
|
||||
await consoleClient.deployments.previewRelease({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
releaseId: targetReleaseId,
|
||||
},
|
||||
})
|
||||
|
||||
if (!targetReleaseId) {
|
||||
const trimmedReleaseNote = releaseNote?.trim()
|
||||
const response = await consoleClient.deployments.createRelease({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
name: trimmedReleaseNote || 'Release',
|
||||
description: trimmedReleaseNote || undefined,
|
||||
},
|
||||
})
|
||||
if (!response.release)
|
||||
throw new Error('Create release did not return a release.')
|
||||
targetReleaseId = response.release.id
|
||||
}
|
||||
|
||||
if (!targetReleaseId)
|
||||
throw new Error('Failed to create a deployable release.')
|
||||
|
||||
return consoleClient.deployments.createDeployment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
environmentId,
|
||||
releaseId: targetReleaseId,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -204,19 +220,25 @@ export const useUndeployDeployment = () => {
|
||||
mutationFn: ({ appId, runtimeInstanceId, isDeploying }: UndeployDeploymentParams) => {
|
||||
if (!runtimeInstanceId)
|
||||
return Promise.resolve(undefined)
|
||||
if (isDeploying)
|
||||
return cancelDeployment(appId, runtimeInstanceId)
|
||||
return undeployEnvironment(appId, runtimeInstanceId)
|
||||
if (isDeploying) {
|
||||
return consoleClient.deployments.cancelDeployment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
runtimeInstanceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
return consoleClient.deployments.undeployEnvironment({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
runtimeInstanceId,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -226,22 +248,20 @@ export const useGenerateDeploymentApiKey = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createEnvironmentAPIToken.mutationKey(),
|
||||
mutationFn: ({ appId, environmentId }: GenerateApiKeyParams) => {
|
||||
const appData = queryClient.getQueryData<DeploymentAppData>(
|
||||
deploymentAppDataQueryKey(appId),
|
||||
)
|
||||
|
||||
return createApiKey(appId, environmentId, createApiKeyLabel(appData, environmentId))
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
mutationFn: ({ appId, environmentId, name }: GenerateApiKeyParams) =>
|
||||
consoleClient.deployments.createEnvironmentAPIToken({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
environmentId,
|
||||
name,
|
||||
},
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -251,16 +271,16 @@ export const useRevokeDeploymentApiKey = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.deleteEnvironmentAPIToken.mutationKey(),
|
||||
mutationFn: ({ appId, apiKeyId }: RevokeApiKeyParams) => deleteApiKey(appId, apiKeyId),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
mutationFn: ({ appId, apiKeyId }: RevokeApiKeyParams) => consoleClient.deployments.deleteEnvironmentAPIToken({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId,
|
||||
},
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -271,19 +291,29 @@ export const useToggleDeploymentAccessChannel = () => {
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.patchAccessChannel.mutationKey(),
|
||||
mutationFn: ({ appId, channel, enabled }: ToggleAccessChannelParams) => {
|
||||
if (channel === 'api')
|
||||
return patchDeveloperAPI(appId, enabled)
|
||||
return patchAccessChannel(appId, enabled)
|
||||
if (channel === 'api') {
|
||||
return consoleClient.deployments.patchDeveloperAPI({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
return consoleClient.deployments.patchAccessChannel({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -298,27 +328,20 @@ export const useSetEnvironmentAccessPolicy = () => {
|
||||
environmentId,
|
||||
accessMode,
|
||||
subjects,
|
||||
}: SetEnvironmentAccessPolicyParams) =>
|
||||
updateEnvironmentAccessPolicy(appId, environmentId, accessMode, subjects),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.environmentAccessPolicy.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: variables.appId,
|
||||
environmentId: variables.environmentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
}: SetEnvironmentAccessPolicyParams) => consoleClient.deployments.updateEnvironmentAccessPolicy({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
environmentId,
|
||||
},
|
||||
body: {
|
||||
accessMode,
|
||||
subjects,
|
||||
},
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
import type { AppInfo } from '../types'
|
||||
import type { AppDeploymentSummary, EnvironmentOption } from '@/contract/console/deployments'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { toAppInfoFromSummary } from '../data'
|
||||
|
||||
const MAX_SOURCE_APPS = 100
|
||||
|
||||
type UseSourceAppsOptions = {
|
||||
enabled?: boolean
|
||||
environmentId?: string
|
||||
keyword?: string
|
||||
notDeployed?: boolean
|
||||
}
|
||||
|
||||
type DeploymentEnvironmentFilter = {
|
||||
id?: string
|
||||
name?: string
|
||||
kind?: string
|
||||
disabled?: boolean
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
export function useSourceApps(options: UseSourceAppsOptions = {}) {
|
||||
const { enabled = true, environmentId, keyword, notDeployed } = options
|
||||
const query = useMemo(() => ({
|
||||
pageNumber: 1,
|
||||
resultsPerPage: MAX_SOURCE_APPS,
|
||||
...(environmentId ? { environmentId } : {}),
|
||||
...(notDeployed ? { notDeployed: true } : {}),
|
||||
...(keyword?.trim() ? { query: keyword.trim() } : {}),
|
||||
}), [environmentId, keyword, notDeployed])
|
||||
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: { query },
|
||||
enabled,
|
||||
staleTime: 30 * 1000,
|
||||
}))
|
||||
|
||||
const apps = useMemo<AppInfo[]>(() => {
|
||||
return (listQuery.data?.data ?? [])
|
||||
.map(toAppInfoFromSummary)
|
||||
.filter((app): app is AppInfo => Boolean(app))
|
||||
}, [listQuery.data?.data])
|
||||
|
||||
const appMap = useMemo<Map<string, AppInfo>>(() => {
|
||||
return new Map(apps.map(a => [a.id, a]))
|
||||
}, [apps])
|
||||
|
||||
const summaries = useMemo<Record<string, AppDeploymentSummary>>(() => {
|
||||
return Object.fromEntries(
|
||||
(listQuery.data?.data ?? [])
|
||||
.filter(summary => summary.id)
|
||||
.map(summary => [summary.id!, summary]),
|
||||
)
|
||||
}, [listQuery.data?.data])
|
||||
|
||||
const environmentOptions = useMemo<EnvironmentOption[]>(() => {
|
||||
return ((listQuery.data?.filters ?? []) as DeploymentEnvironmentFilter[])
|
||||
.filter(filter => filter.kind === 'environment' && filter.id)
|
||||
.map(filter => ({
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
disabled: filter.disabled,
|
||||
disabledReason: filter.disabledReason,
|
||||
}))
|
||||
}, [listQuery.data?.filters])
|
||||
|
||||
return {
|
||||
apps,
|
||||
appMap,
|
||||
summaries,
|
||||
environmentOptions,
|
||||
isLoading: listQuery.isLoading,
|
||||
isFetching: listQuery.isFetching,
|
||||
isError: listQuery.isError,
|
||||
isEmpty: !listQuery.isLoading && apps.length === 0,
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
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'
|
||||
import CreateInstanceModal from '../components/create-instance-modal'
|
||||
import DeployDrawer from '../components/deploy-drawer'
|
||||
import RollbackModal from '../components/rollback-modal'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import {
|
||||
SOURCE_APPS_PAGE_SIZE,
|
||||
} from '../data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { environmentId, environmentName } from '../utils'
|
||||
import {
|
||||
deploymentSummariesFromList,
|
||||
environmentId,
|
||||
environmentName,
|
||||
environmentOptionsFromList,
|
||||
sourceAppsFromList,
|
||||
} from '../utils'
|
||||
import { EnvironmentFilter } from './environment-filter'
|
||||
import { InstanceCard } from './instance-card'
|
||||
import { NewInstanceCard } from './new-instance-card'
|
||||
@ -40,15 +50,20 @@ const DeploymentsMain: FC = () => {
|
||||
const requestedEnvironmentId = envFilter !== 'all' && envFilter !== 'not-deployed'
|
||||
? envFilter
|
||||
: undefined
|
||||
const {
|
||||
apps,
|
||||
summaries,
|
||||
environmentOptions,
|
||||
} = useSourceApps({
|
||||
environmentId: requestedEnvironmentId,
|
||||
notDeployed: envFilter === 'not-deployed',
|
||||
keyword: queryKeywords.trim() || undefined,
|
||||
})
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
...(requestedEnvironmentId ? { environmentId: requestedEnvironmentId } : {}),
|
||||
...(envFilter === 'not-deployed' ? { notDeployed: true } : {}),
|
||||
...(queryKeywords.trim() ? { query: queryKeywords.trim() } : {}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
const summaries = useMemo(() => deploymentSummariesFromList(listQuery.data), [listQuery.data])
|
||||
const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data])
|
||||
|
||||
const environments = useMemo(() => {
|
||||
return environmentOptions
|
||||
|
||||
@ -13,15 +13,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { deployedRows, deploymentStatus, environmentId, environmentName, releaseLabel } from '../utils'
|
||||
|
||||
type InstanceCardProps = {
|
||||
app: AppInfo
|
||||
@ -33,7 +31,6 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, summary }) => {
|
||||
const router = useRouter()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const { data: appData } = useCachedDeploymentAppData(app.id)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
|
||||
const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`)
|
||||
@ -45,36 +42,16 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, summary }) => {
|
||||
action()
|
||||
}
|
||||
|
||||
const deployments = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
)
|
||||
const statusCount = (status: string) =>
|
||||
summary?.statuses?.find(item => item.status === status)?.count ?? 0
|
||||
const hasSummary = Boolean(summary)
|
||||
const failedCount = hasSummary
|
||||
? statusCount('failed') + statusCount('deploy_failed')
|
||||
: deployments.filter(row => deploymentStatus(row) === 'deploy_failed').length
|
||||
const deployingCount = hasSummary
|
||||
? statusCount('deploying')
|
||||
: deployments.filter(row => deploymentStatus(row) === 'deploying').length
|
||||
const readyCount = hasSummary
|
||||
? statusCount('ready')
|
||||
: deployments.filter(row => deploymentStatus(row) === 'ready').length
|
||||
const envCount = hasSummary
|
||||
? failedCount + deployingCount + readyCount
|
||||
: deployments.length
|
||||
const failedCount = statusCount('failed') + statusCount('deploy_failed')
|
||||
const deployingCount = statusCount('deploying')
|
||||
const readyCount = statusCount('ready')
|
||||
const envCount = failedCount + deployingCount + readyCount
|
||||
|
||||
const lastDeployedAt = useMemo(() => {
|
||||
if (summary?.lastDeployedAt)
|
||||
return new Date(summary.lastDeployedAt).getTime()
|
||||
if (deployments.length === 0)
|
||||
return null
|
||||
return deployments.reduce((latest, row) => {
|
||||
const t = new Date(row.currentRelease?.createdAt || '').getTime()
|
||||
return t > latest ? t : latest
|
||||
}, 0)
|
||||
}, [deployments, summary?.lastDeployedAt])
|
||||
const lastDeployedAt = summary?.lastDeployedAt
|
||||
? new Date(summary.lastDeployedAt).getTime()
|
||||
: null
|
||||
|
||||
const primaryStatus: 'none' | 'failed' | 'deploying' | 'ready' = envCount === 0
|
||||
? 'none'
|
||||
@ -98,11 +75,6 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, summary }) => {
|
||||
if ((primaryStatus === 'failed' || primaryStatus === 'deploying') && readyCount > 0)
|
||||
secondaryParts.push(t('card.ready', { count: readyCount }))
|
||||
|
||||
const statusLabel = (status: ReturnType<typeof deploymentStatus>) => {
|
||||
if (status === 'deploy_failed')
|
||||
return t('status.deployFailed')
|
||||
return t(`status.${status}`)
|
||||
}
|
||||
const statusSummaryLabel = (status?: string) => {
|
||||
if (status === 'failed' || status === 'deploy_failed')
|
||||
return t('status.deployFailed')
|
||||
@ -116,38 +88,17 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, summary }) => {
|
||||
const statusSummaryTooltip = summary?.statuses?.filter(item => item.count && item.status !== 'undeployed') ?? []
|
||||
const statusTooltip = primaryStatus === 'none'
|
||||
? t('card.tooltip.notDeployed')
|
||||
: deployments.length > 0
|
||||
? (
|
||||
<div className="flex min-w-[220px] flex-col gap-1">
|
||||
<div className="system-xs-medium text-text-secondary">{t('overview.deploymentStatus')}</div>
|
||||
{deployments.map((deployment) => {
|
||||
const status = deploymentStatus(deployment)
|
||||
return (
|
||||
<div key={environmentId(deployment.environment)} className="flex min-w-0 items-center justify-between gap-3">
|
||||
<span className="min-w-0 truncate text-text-tertiary">
|
||||
{environmentName(deployment.environment)}
|
||||
</span>
|
||||
<span className="shrink-0 text-text-secondary">
|
||||
{statusLabel(status)}
|
||||
{' · '}
|
||||
{releaseLabel(deployment.currentRelease)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex min-w-[180px] flex-col gap-1">
|
||||
<div className="system-xs-medium text-text-secondary">{t('overview.deploymentStatus')}</div>
|
||||
{statusSummaryTooltip.map(item => (
|
||||
<div key={item.status} className="flex justify-between gap-3">
|
||||
<span className="text-text-tertiary">{statusSummaryLabel(item.status)}</span>
|
||||
<span className="text-text-secondary">{item.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex min-w-[180px] flex-col gap-1">
|
||||
<div className="system-xs-medium text-text-secondary">{t('overview.deploymentStatus')}</div>
|
||||
{statusSummaryTooltip.map(item => (
|
||||
<div key={item.status} className="flex justify-between gap-3">
|
||||
<span className="text-text-tertiary">{statusSummaryLabel(item.status)}</span>
|
||||
<span className="text-text-secondary">{item.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const healthPillClass = primaryStatus === 'none'
|
||||
? 'text-text-tertiary bg-background-section-burn'
|
||||
|
||||
@ -2,13 +2,18 @@
|
||||
|
||||
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'
|
||||
import { useDeploymentAppInfo } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { SOURCE_APPS_PAGE_SIZE } from '../data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import {
|
||||
sourceAppsFromList,
|
||||
toAppInfoFromOverview,
|
||||
} from '../utils'
|
||||
|
||||
const DeploymentsNav = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -19,11 +24,24 @@ const DeploymentsNav = () => {
|
||||
const instanceId = params?.instanceId
|
||||
|
||||
const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal)
|
||||
const { data: currentInstance } = useDeploymentAppInfo(instanceId, {
|
||||
const { data: currentInstance } = useQuery(consoleQuery.deployments.overview.queryOptions({
|
||||
input: instanceId
|
||||
? { params: { appInstanceId: instanceId } }
|
||||
: skipToken,
|
||||
enabled: isActive && Boolean(instanceId),
|
||||
})
|
||||
select: data => toAppInfoFromOverview(data.instance),
|
||||
}))
|
||||
|
||||
const { apps } = useSourceApps({ enabled: isActive })
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: SOURCE_APPS_PAGE_SIZE,
|
||||
},
|
||||
},
|
||||
enabled: isActive,
|
||||
}))
|
||||
const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data])
|
||||
|
||||
const navigationItems = useMemo<NavItem[]>(() => {
|
||||
if (!isActive)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import type { AccessPermissionKind } from './types'
|
||||
import type { AccessPermissionKind, AppInfo, AppMode } from './types'
|
||||
import type {
|
||||
AppDeploymentSummary,
|
||||
AppInstanceOverview,
|
||||
ConsoleEnvironmentSummary,
|
||||
ConsoleReleaseSummary,
|
||||
EnvironmentDeploymentRow,
|
||||
EnvironmentOption,
|
||||
ListAppDeploymentsReply,
|
||||
RuntimeBindingDisplay,
|
||||
} from '@/contract/console/deployments'
|
||||
import { PUBLIC_API_PREFIX } from '@/config'
|
||||
@ -104,6 +107,75 @@ export const deployedRows = (rows?: EnvironmentDeploymentRow[]) =>
|
||||
&& (row.id || runtimeStatus || row.currentRelease || row.detail)
|
||||
}) ?? []
|
||||
|
||||
type DeploymentEnvironmentFilter = {
|
||||
id?: string
|
||||
name?: string
|
||||
kind?: string
|
||||
disabled?: boolean
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
export function toAppInfoFromSummary(summary: AppDeploymentSummary): AppInfo | undefined {
|
||||
if (!summary.id || !summary.name)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id: summary.id,
|
||||
name: summary.name,
|
||||
mode: (summary.mode || 'workflow') as AppMode,
|
||||
iconType: 'emoji',
|
||||
icon: summary.icon,
|
||||
description: summary.description ?? undefined,
|
||||
sourceAppId: summary.sourceAppId,
|
||||
sourceAppName: summary.sourceAppName,
|
||||
}
|
||||
}
|
||||
|
||||
export function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | undefined {
|
||||
if (!instance?.id)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
name: instance.name ?? instance.id,
|
||||
mode: (instance.mode || 'workflow') as AppMode,
|
||||
iconType: 'emoji',
|
||||
icon: instance.icon,
|
||||
description: instance.description ?? undefined,
|
||||
sourceAppId: instance.sourceAppId,
|
||||
sourceAppName: instance.sourceAppName,
|
||||
}
|
||||
}
|
||||
|
||||
export const sourceAppsFromList = (response?: ListAppDeploymentsReply) => {
|
||||
return (response?.data ?? [])
|
||||
.map(toAppInfoFromSummary)
|
||||
.filter((app): app is AppInfo => Boolean(app))
|
||||
}
|
||||
|
||||
export const sourceAppMapFromApps = (apps: AppInfo[]) => {
|
||||
return new Map(apps.map(app => [app.id, app]))
|
||||
}
|
||||
|
||||
export const deploymentSummariesFromList = (response?: ListAppDeploymentsReply): Record<string, AppDeploymentSummary> => {
|
||||
return Object.fromEntries(
|
||||
(response?.data ?? [])
|
||||
.filter(summary => summary.id)
|
||||
.map(summary => [summary.id!, summary]),
|
||||
)
|
||||
}
|
||||
|
||||
export const environmentOptionsFromList = (response?: ListAppDeploymentsReply): EnvironmentOption[] => {
|
||||
return ((response?.filters ?? []) as DeploymentEnvironmentFilter[])
|
||||
.filter(filter => filter.kind === 'environment' && filter.id)
|
||||
.map(filter => ({
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
disabled: filter.disabled,
|
||||
disabledReason: filter.disabledReason,
|
||||
}))
|
||||
}
|
||||
|
||||
export const accessModeToPermissionKey = (mode?: string): AccessPermissionKind => {
|
||||
const normalized = mode?.toLowerCase() ?? ''
|
||||
if (normalized === 'private')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user