mirror of
https://github.com/langgenius/dify.git
synced 2026-05-11 23:18:39 +08:00
tweaks
This commit is contained in:
parent
86fc60debf
commit
6d0d0763b1
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: how-to-write-component
|
||||
description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling.
|
||||
description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around abstraction choices, props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling.
|
||||
---
|
||||
|
||||
# How To Write A Component
|
||||
@ -12,6 +12,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
|
||||
- Search before adding UI, hooks, helpers, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit.
|
||||
- Group code by feature workflow, route, or ownership area: components, hooks, local types, query helpers, atoms, constants, and small utilities should live near the code that changes with them.
|
||||
- Promote code to shared only when multiple verticals need the same stable primitive. Otherwise keep it local and compose shared primitives inside the owning feature.
|
||||
- Prefer local code and purpose-named helpers over catch-all utility modules; inline cheap derived values when that is clearer.
|
||||
- Use Tailwind CSS v4.1+ rules via the `tailwind-css-rules` skill. Prefer v4 utilities, `gap`, `text-size/line-height`, `min-h-dvh`, and avoid deprecated utilities and `@apply`.
|
||||
|
||||
## Ownership
|
||||
@ -30,9 +31,9 @@ Use this as the decision guide for React/TypeScript component structure. Existin
|
||||
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
|
||||
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
|
||||
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
|
||||
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them.
|
||||
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers and one-off UI extensions beside the component that needs them.
|
||||
- Name values by their domain role and backend API contract, and keep that name stable across the call chain, especially persistent IDs and route params. Normalize framework or route params at the boundary.
|
||||
- Keep fallback and invariant checks at the lowest component that already handles that state; callers should pass raw values through instead of duplicating checks.
|
||||
- Keep fallback and invariant checks at the lowest component that already handles that state; avoid defensive fallbacks that mask impossible states.
|
||||
|
||||
## Queries And Mutations
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ConsoleRelease, ReleaseRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { releaseDeploymentAction } from '../utils'
|
||||
import { releaseDeploymentAction } from '../release-action'
|
||||
|
||||
function release(overrides: ReleaseRow): ReleaseRow {
|
||||
return overrides
|
||||
7
web/features/deployments/app-mode.ts
Normal file
7
web/features/deployments/app-mode.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const appModeValues = new Set<string>(Object.values(AppModeEnum))
|
||||
|
||||
export function toAppMode(mode?: string): AppModeEnum {
|
||||
return appModeValues.has(mode ?? '') ? (mode as AppModeEnum) : AppModeEnum.WORKFLOW
|
||||
}
|
||||
@ -13,7 +13,6 @@ import {
|
||||
deployDrawerOpenAtom,
|
||||
deployDrawerReleaseIdAtom,
|
||||
} from '../store'
|
||||
import { environmentOptionsFromOptionsReply } from '../utils'
|
||||
import { DeployForm } from './deploy-drawer/form'
|
||||
|
||||
export function DeployDrawer() {
|
||||
@ -39,7 +38,12 @@ export function DeployDrawer() {
|
||||
enabled: open,
|
||||
}))
|
||||
|
||||
const environments = environmentOptionsFromOptionsReply(environmentOptionsReply)
|
||||
const environments = environmentOptionsReply?.environments
|
||||
?.filter(environment => environment.id)
|
||||
.map(environment => ({
|
||||
...environment,
|
||||
disabled: environment.deployable === false,
|
||||
})) ?? []
|
||||
const releases = releaseHistory?.data?.filter(release => release.id) ?? []
|
||||
const defaultReleaseId = releases[0]?.id
|
||||
const formKey = `${drawerAppInstanceId ?? 'none'}-${drawerEnvironmentId ?? 'any'}-${drawerReleaseId ?? 'new'}-${open ? '1' : '0'}`
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { DeploymentBindingOptionSlot, DeploymentRuntimeBinding, ReleaseRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import type { EnvironmentOption } from '@/features/deployments/types'
|
||||
import type { DeploymentBindingOptionSlot, DeploymentEnvironmentOption, DeploymentRuntimeBinding, ReleaseRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -10,17 +9,11 @@ import { useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { environmentId, environmentMode, environmentName } from '../../environment'
|
||||
import { releaseCommit, releaseLabel } from '../../release'
|
||||
import { releaseDeploymentAction } from '../../release-action'
|
||||
import { isUndeployedDeploymentRow } from '../../runtime-status'
|
||||
import { closeDeployDrawerAtom } from '../../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
environmentId,
|
||||
environmentMode,
|
||||
environmentName,
|
||||
releaseCommit,
|
||||
releaseDeploymentAction,
|
||||
releaseLabel,
|
||||
} from '../../utils'
|
||||
import {
|
||||
DeploymentSelect,
|
||||
EnvironmentRow,
|
||||
@ -36,6 +29,10 @@ type DeployFormProps = {
|
||||
presetReleaseId?: string
|
||||
}
|
||||
|
||||
type EnvironmentOption = DeploymentEnvironmentOption & {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type BindingSelections = Record<string, string>
|
||||
|
||||
type BindingSelectOption = {
|
||||
@ -235,11 +232,11 @@ export function DeployForm({
|
||||
const selectedRelease = releases.find(release => release.id === selectedReleaseId)
|
||||
const targetReleaseId = displayedRelease?.id ?? selectedRelease?.id ?? selectedReleaseId
|
||||
const targetRelease = displayedRelease ?? selectedRelease ?? (targetReleaseId ? { id: targetReleaseId } : undefined)
|
||||
const deploymentRows = deployedRows(environmentDeployments?.data)
|
||||
const deploymentRows = environmentDeployments?.data?.filter(row => Boolean(row.environment?.id) && !isUndeployedDeploymentRow(row)) ?? []
|
||||
const selectedDeploymentRow = deploymentRows.find(row => environmentId(row.environment) === selectedEnvironmentId)
|
||||
const action = releaseDeploymentAction({
|
||||
targetRelease,
|
||||
currentRelease: activeRelease(selectedDeploymentRow),
|
||||
currentRelease: selectedDeploymentRow?.currentRelease,
|
||||
releaseRows: releases,
|
||||
isExistingRelease,
|
||||
})
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import type { EnvironmentOption } from '@/features/deployments/types'
|
||||
import type { DeploymentEnvironmentOption } from '@dify/contracts/enterprise/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { environmentHealth, environmentMode, environmentName } from '../../utils'
|
||||
import { environmentHealth, environmentMode, environmentName } from '../../environment'
|
||||
import { HealthBadge, ModeBadge } from '../status-badge'
|
||||
|
||||
type EnvironmentOption = DeploymentEnvironmentOption & {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function Field({ label, hint, children }: {
|
||||
label: string
|
||||
hint?: string
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
'use client'
|
||||
import type { DeployStatus, EnvironmentHealth, EnvironmentMode } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type DeployStatus = 'ready' | 'deploying' | 'deploy_failed'
|
||||
type EnvironmentMode = 'shared' | 'isolated'
|
||||
type EnvironmentHealth = 'ready' | 'degraded'
|
||||
|
||||
const statusStyles: Record<DeployStatus, string> = {
|
||||
ready: 'border-util-colors-green-green-200 bg-util-colors-green-green-50 text-util-colors-green-green-700',
|
||||
deploying: 'border-util-colors-warning-warning-200 bg-util-colors-warning-warning-50 text-util-colors-warning-warning-700',
|
||||
|
||||
@ -12,7 +12,7 @@ import { useMutation } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { environmentName } from '../../utils'
|
||||
import { environmentName } from '../../environment'
|
||||
|
||||
function ApiKeyRow({ appInstanceId, apiKey }: {
|
||||
appInstanceId: string
|
||||
|
||||
@ -4,7 +4,8 @@ import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { environmentName, webappUrl } from '../../utils'
|
||||
import { environmentName } from '../../environment'
|
||||
import { webappUrl } from '../../webapp-url'
|
||||
import { Section } from '../common'
|
||||
import { CopyPill, EndpointRow } from './common'
|
||||
import { getUrlOrigin } from './url'
|
||||
|
||||
@ -7,7 +7,6 @@ import type {
|
||||
ConsoleEnvironment,
|
||||
EnvironmentAccessRow,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type { AccessPermissionKind } from '../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -22,11 +21,26 @@ import { useDebounce } from 'ahooks'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
accessModeToPermissionKey,
|
||||
environmentName,
|
||||
permissionKeyToAccessMode,
|
||||
} from '../../utils'
|
||||
import { environmentName } from '../../environment'
|
||||
|
||||
type AccessPermissionKind = 'organization' | 'specific' | 'anyone'
|
||||
|
||||
function accessModeToPermissionKey(mode?: string): AccessPermissionKind {
|
||||
const normalized = mode?.toLowerCase() ?? ''
|
||||
if (normalized === 'private')
|
||||
return 'specific'
|
||||
if (normalized === 'public')
|
||||
return 'anyone'
|
||||
return 'organization'
|
||||
}
|
||||
|
||||
function permissionKeyToAccessMode(key: AccessPermissionKind) {
|
||||
if (key === 'organization')
|
||||
return 'private_all'
|
||||
if (key === 'specific')
|
||||
return 'private'
|
||||
return 'public'
|
||||
}
|
||||
|
||||
const permissionIcon: Record<AccessPermissionKind, string> = {
|
||||
organization: 'i-ri-team-line',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { EnvironmentOption } from '../types'
|
||||
import type { DeploymentEnvironmentOption } from '@dify/contracts/enterprise/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -12,15 +12,15 @@ import { useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { environmentId, environmentName } from '../environment'
|
||||
import { isUndeployedDeploymentRow } from '../runtime-status'
|
||||
import { openDeployDrawerAtom } from '../store'
|
||||
import {
|
||||
deployedRows,
|
||||
environmentId,
|
||||
environmentName,
|
||||
environmentOptionsFromOptionsReply,
|
||||
} from '../utils'
|
||||
import { DeploymentEnvironmentList } from './deploy-tab/deployment-environment-list'
|
||||
|
||||
type EnvironmentOption = DeploymentEnvironmentOption & {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function NewDeploymentMenu({ appInstanceId, availableEnvs }: {
|
||||
appInstanceId: string
|
||||
availableEnvs: EnvironmentOption[]
|
||||
@ -91,9 +91,14 @@ export function DeployTab({ appInstanceId }: {
|
||||
},
|
||||
}))
|
||||
const { data: environmentOptionsReply } = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions())
|
||||
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
|
||||
const environmentOptions = environmentOptionsReply?.environments
|
||||
?.filter(environment => environment.id)
|
||||
.map(environment => ({
|
||||
...environment,
|
||||
disabled: environment.deployable === false,
|
||||
})) ?? []
|
||||
const rows = environmentDeployments?.data?.filter(row => row.environment?.id) ?? []
|
||||
const deployedRuntimeRows = deployedRows(environmentDeployments?.data)
|
||||
const deployedRuntimeRows = rows.filter(row => !isUndeployedDeploymentRow(row))
|
||||
|
||||
const deployedEnvIds = new Set(deployedRuntimeRows.map(row => environmentId(row.environment)))
|
||||
const availableEnvs = environmentOptions.filter(env => env.id && !deployedEnvIds.has(env.id))
|
||||
|
||||
@ -15,19 +15,15 @@ import { useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { openDeployDrawerAtom } from '../../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deploymentId,
|
||||
deploymentStatus,
|
||||
environmentBackend,
|
||||
environmentId,
|
||||
environmentMode,
|
||||
environmentName,
|
||||
isUndeployedDeploymentRow,
|
||||
releaseCommit,
|
||||
releaseLabel,
|
||||
} from '../../utils'
|
||||
} from '../../environment'
|
||||
import { releaseCommit, releaseLabel } from '../../release'
|
||||
import { deploymentStatus, isUndeployedDeploymentRow } from '../../runtime-status'
|
||||
import { openDeployDrawerAtom } from '../../store'
|
||||
import { DeploymentPanel } from './deployment-panel'
|
||||
import { DeploymentStatusSummary } from './deployment-status-summary'
|
||||
|
||||
@ -47,7 +43,7 @@ function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
const status = deploymentStatus(row)
|
||||
|
||||
function handleRuntimeAction() {
|
||||
const runtimeInstanceId = deploymentId(row)
|
||||
const runtimeInstanceId = row.id ?? ''
|
||||
setMenuOpen(false)
|
||||
|
||||
if (status === 'deploying') {
|
||||
@ -126,7 +122,7 @@ function DeploymentEnvironmentRow({ appInstanceId, row, isExpanded, onToggle }:
|
||||
const { t } = useTranslation('deployments')
|
||||
const envId = environmentId(row.environment)
|
||||
const isUndeployed = isUndeployedDeploymentRow(row)
|
||||
const release = activeRelease(row)
|
||||
const release = row.currentRelease
|
||||
const chevron = !isUndeployed && (
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@ -4,19 +4,14 @@ import type { ReleaseRuntimeBinding, RuntimeInstanceRow } from '@dify/contracts/
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { environmentBackend, environmentMode } from '../../environment'
|
||||
import { formatDate, releaseCommit, releaseLabel } from '../../release'
|
||||
import {
|
||||
activeRelease,
|
||||
deploymentId,
|
||||
environmentBackend,
|
||||
environmentMode,
|
||||
formatDate,
|
||||
isRuntimeEnvVarBinding,
|
||||
isRuntimeModelBinding,
|
||||
isRuntimePluginBinding,
|
||||
releaseCommit,
|
||||
releaseLabel,
|
||||
runtimeBindingSummary,
|
||||
} from '../../utils'
|
||||
} from '../../runtime-bindings'
|
||||
|
||||
function InfoBlock({ title, children }: {
|
||||
title: string
|
||||
@ -67,7 +62,7 @@ export function DeploymentPanel({ row }: {
|
||||
row: RuntimeInstanceRow
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const observed = activeRelease(row)
|
||||
const observed = row.currentRelease
|
||||
const env = row.environment
|
||||
const endpoints = row.detail?.endpoints
|
||||
const detailBindings = row.detail?.bindings ?? []
|
||||
@ -79,7 +74,7 @@ export function DeploymentPanel({ row }: {
|
||||
<div className="border-t border-divider-subtle bg-background-default-subtle px-4 py-3">
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<InfoBlock title={t('deployTab.panel.instanceInfo')}>
|
||||
<InfoRow label={t('deployTab.panel.deploymentId')} value={deploymentId(row) || '—'} mono />
|
||||
<InfoRow label={t('deployTab.panel.deploymentId')} value={row.id || '—'} mono />
|
||||
<InfoRow label={t('deployTab.panel.replicas')} value={row.detail?.replicas != null ? String(row.detail.replicas) : '—'} />
|
||||
<InfoRow label={t('deployTab.panel.runtimeMode')} value={row.detail?.runtimeMode ?? t(environmentMode(env) === 'isolated' ? 'mode.isolated' : 'mode.shared')} suffix={` / ${environmentBackend(env).toUpperCase()}`} />
|
||||
<InfoRow label={t('deployTab.panel.runtimeNote')} value={row.detail?.runtimeNote ?? row.status ?? '—'} />
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
|
||||
import type { RuntimeInstanceRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { releaseLabel } from '../../release'
|
||||
import {
|
||||
activeRelease,
|
||||
deploymentStatus,
|
||||
isUndeployedDeploymentRow,
|
||||
releaseLabel,
|
||||
} from '../../utils'
|
||||
} from '../../runtime-status'
|
||||
|
||||
export function DeploymentStatusSummary({ row }: {
|
||||
row: RuntimeInstanceRow
|
||||
@ -28,13 +27,13 @@ export function DeploymentStatusSummary({ row }: {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 system-sm-medium text-util-colors-blue-blue-700">
|
||||
<span className="i-ri-loader-4-line size-3.5 animate-spin" />
|
||||
{t('deployTab.status.deployingRelease', { release: releaseLabel(activeRelease(row)) })}
|
||||
{t('deployTab.status.deployingRelease', { release: releaseLabel(row.currentRelease) })}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'deploy_failed') {
|
||||
const hasRunningRelease = !!activeRelease(row)?.id
|
||||
const hasRunningRelease = !!row.currentRelease?.id
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 system-sm-medium text-util-colors-warning-warning-700">
|
||||
<span className="i-ri-alert-line size-3.5" />
|
||||
|
||||
@ -15,7 +15,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { toAppMode } from '../utils'
|
||||
import { toAppMode } from '../app-mode'
|
||||
|
||||
type TabDef = {
|
||||
key: InstanceDetailTabKey
|
||||
|
||||
@ -8,14 +8,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { toAppMode } from '../app-mode'
|
||||
import { StatusBadge } from '../components/status-badge'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../data'
|
||||
import { releaseLabel } from '../release'
|
||||
import { openDeployDrawerAtom } from '../store'
|
||||
import {
|
||||
releaseLabel,
|
||||
toAppMode,
|
||||
webappUrl,
|
||||
} from '../utils'
|
||||
import { webappUrl } from '../webapp-url'
|
||||
import { Section } from './common'
|
||||
|
||||
function InfoRow({ label, value, mono }: {
|
||||
|
||||
@ -16,7 +16,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deployedRows } from '../utils'
|
||||
import { isUndeployedDeploymentRow } from '../runtime-status'
|
||||
import { AccessChannelsSection } from './access-tab/channels-section'
|
||||
import { DeveloperApiSection } from './access-tab/developer-api-section'
|
||||
import { AccessPermissionsSection } from './access-tab/permissions-section'
|
||||
@ -264,7 +264,7 @@ function DeleteInstanceControlSection({ appInstanceId }: {
|
||||
if (!app?.id)
|
||||
return null
|
||||
|
||||
const hasDeployments = deployedRows(environmentDeployments?.data).length > 0
|
||||
const hasDeployments = environmentDeployments?.data?.some(row => Boolean(row.environment?.id) && !isUndeployedDeploymentRow(row)) ?? false
|
||||
|
||||
return (
|
||||
<DeleteInstanceControl
|
||||
|
||||
@ -13,16 +13,10 @@ import { useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { environmentId, environmentName } from '../../environment'
|
||||
import { releaseDeploymentAction } from '../../release-action'
|
||||
import { deploymentStatus, isUndeployedDeploymentRow } from '../../runtime-status'
|
||||
import { openDeployDrawerAtom } from '../../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
deploymentStatus,
|
||||
environmentId,
|
||||
environmentName,
|
||||
environmentOptionsFromOptionsReply,
|
||||
releaseDeploymentAction,
|
||||
} from '../../utils'
|
||||
|
||||
export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows }: {
|
||||
appInstanceId: string
|
||||
@ -42,9 +36,14 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows }: {
|
||||
enabled: open,
|
||||
}))
|
||||
|
||||
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
|
||||
const environmentOptions = environmentOptionsReply?.environments
|
||||
?.filter(environment => environment.id)
|
||||
.map(environment => ({
|
||||
...environment,
|
||||
disabled: environment.deployable === false,
|
||||
})) ?? []
|
||||
const environments = environmentOptions.filter(env => env.id)
|
||||
const deploymentRows = deployedRows(environmentDeployments?.data)
|
||||
const deploymentRows = environmentDeployments?.data?.filter(row => Boolean(row.environment?.id) && !isUndeployedDeploymentRow(row)) ?? []
|
||||
const targetRelease = releaseRows.find(release => release.id === releaseId) ?? { id: releaseId }
|
||||
|
||||
return (
|
||||
@ -64,12 +63,13 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows }: {
|
||||
{environments.map((env) => {
|
||||
const envId = env.id!
|
||||
const row = deploymentRows.find(item => environmentId(item.environment) === envId)
|
||||
const isCurrent = activeRelease(row)?.id === releaseId
|
||||
const currentRelease = row?.currentRelease
|
||||
const isCurrent = currentRelease?.id === releaseId
|
||||
const isEnvironmentDeploying = row ? deploymentStatus(row) === 'deploying' : false
|
||||
const disabled = Boolean(env.disabled || isCurrent || isEnvironmentDeploying)
|
||||
const action = releaseDeploymentAction({
|
||||
targetRelease,
|
||||
currentRelease: activeRelease(row),
|
||||
currentRelease,
|
||||
releaseRows,
|
||||
isExistingRelease: true,
|
||||
})
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import type { DeployedEnvironment, ReleaseRow, RuntimeInstanceRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import {
|
||||
activeRelease,
|
||||
deploymentStatus,
|
||||
environmentId,
|
||||
environmentName,
|
||||
} from '../../utils'
|
||||
import { environmentId, environmentName } from '../../environment'
|
||||
import { deploymentStatus } from '../../runtime-status'
|
||||
|
||||
export type ReleaseDeploymentState = 'active' | 'deploying' | 'failed'
|
||||
|
||||
@ -52,7 +48,7 @@ export function getReleaseDeployments(row: ReleaseRow, deploymentRows: RuntimeIn
|
||||
return []
|
||||
|
||||
const items: ReleaseDeployment[] = []
|
||||
if (activeRelease(deployment)?.id === releaseId) {
|
||||
if (deployment.currentRelease?.id === releaseId) {
|
||||
items.push({
|
||||
environmentId: envId,
|
||||
environmentName: environmentName(deployment.environment),
|
||||
|
||||
@ -9,11 +9,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../../data'
|
||||
import {
|
||||
deployedRows,
|
||||
formatDate,
|
||||
releaseCommit,
|
||||
releaseLabel,
|
||||
} from '../../utils'
|
||||
} from '../../release'
|
||||
import { isUndeployedDeploymentRow } from '../../runtime-status'
|
||||
import { DeployReleaseMenu } from './deploy-release-menu'
|
||||
import { DeployedToBadge } from './deployed-to-badge'
|
||||
import { getReleaseDeployments } from './release-deployments'
|
||||
@ -174,7 +174,7 @@ export function ReleaseHistoryTable({ appInstanceId }: {
|
||||
|| (releaseRows.length === 0 && overviewQuery.isLoading)
|
||||
|| (shouldLoadRuntimeInstances && environmentDeploymentsQuery.isLoading)
|
||||
const sourceAppUnavailable = overviewQuery.data?.instance?.canCreateRelease === false
|
||||
const deploymentRows = deployedRows(environmentDeploymentsQuery.data?.data)
|
||||
const deploymentRows = environmentDeploymentsQuery.data?.data?.filter(row => Boolean(row.environment?.id) && !isUndeployedDeploymentRow(row)) ?? []
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
34
web/features/deployments/environment.ts
Normal file
34
web/features/deployments/environment.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { ConsoleEnvironment, DeploymentEnvironmentOption } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
export function environmentId(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
return environment?.id ?? ''
|
||||
}
|
||||
|
||||
export function environmentName(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
return environment?.name || environment?.id || '—'
|
||||
}
|
||||
|
||||
export function environmentMode(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
const type = environment?.type?.toLowerCase() ?? ''
|
||||
return type.includes('isolated') ? 'isolated' : 'shared'
|
||||
}
|
||||
|
||||
function environmentRuntimeName(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
if (!environment)
|
||||
return ''
|
||||
if ('backend' in environment && environment.backend)
|
||||
return environment.backend
|
||||
if ('runtime' in environment && environment.runtime)
|
||||
return environment.runtime
|
||||
return ''
|
||||
}
|
||||
|
||||
export function environmentBackend(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
const runtime = environmentRuntimeName(environment).toLowerCase()
|
||||
return runtime.includes('host') ? 'host' : 'k8s'
|
||||
}
|
||||
|
||||
export function environmentHealth(environment?: ConsoleEnvironment | DeploymentEnvironmentOption) {
|
||||
const status = environment?.status?.toLowerCase() ?? ''
|
||||
return status.includes('ready') ? 'ready' : 'degraded'
|
||||
}
|
||||
@ -15,7 +15,7 @@ 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 Link from '@/next/link'
|
||||
import { toAppMode } from '../utils'
|
||||
import { toAppMode } from '../app-mode'
|
||||
|
||||
const INSTANCE_CARD_MENU_TAB_KEYS = ['deploy', 'versions', 'settings'] satisfies InstanceDetailTabKey[]
|
||||
|
||||
|
||||
@ -8,9 +8,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import Nav from '@/app/components/header/nav'
|
||||
import { useParams, useRouter, useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { toAppMode } from '../app-mode'
|
||||
import { SOURCE_APPS_PAGE_SIZE } from '../data'
|
||||
import { openCreateInstanceModalAtom } from '../store'
|
||||
import { toAppMode } from '../utils'
|
||||
|
||||
function navItemFromListApp(app: AppInstanceCard): NavItem[] {
|
||||
if (!app.id || !app.name)
|
||||
|
||||
69
web/features/deployments/release-action.ts
Normal file
69
web/features/deployments/release-action.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { ConsoleRelease, ReleaseRow } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
export type ReleaseDeploymentAction = 'deploy' | 'deployExistingRelease' | 'promote' | 'rollback'
|
||||
|
||||
function releaseCreatedAt(release?: ConsoleRelease | ReleaseRow) {
|
||||
const value = release?.createdAt
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
const time = Date.parse(value)
|
||||
return Number.isFinite(time) ? time : undefined
|
||||
}
|
||||
|
||||
function releaseById(releaseRows: ReleaseRow[], releaseId?: string) {
|
||||
return releaseRows.find(release => release.id === releaseId)
|
||||
}
|
||||
|
||||
function releaseOrderIndex(releaseRows: ReleaseRow[], releaseId?: string) {
|
||||
return releaseRows.findIndex(release => release.id === releaseId)
|
||||
}
|
||||
|
||||
function compareReleaseOrder(targetRelease: ConsoleRelease | ReleaseRow | undefined, currentRelease: ConsoleRelease, releaseRows: ReleaseRow[]) {
|
||||
if (!targetRelease?.id || !currentRelease.id)
|
||||
return undefined
|
||||
if (targetRelease.id === currentRelease.id)
|
||||
return 0
|
||||
|
||||
const normalizedTargetRelease = releaseById(releaseRows, targetRelease.id) ?? targetRelease
|
||||
const normalizedCurrentRelease = releaseById(releaseRows, currentRelease.id) ?? currentRelease
|
||||
const targetCreatedAt = releaseCreatedAt(normalizedTargetRelease)
|
||||
const currentCreatedAt = releaseCreatedAt(normalizedCurrentRelease)
|
||||
|
||||
if (targetCreatedAt !== undefined && currentCreatedAt !== undefined && targetCreatedAt !== currentCreatedAt)
|
||||
return targetCreatedAt > currentCreatedAt ? 1 : -1
|
||||
|
||||
const targetIndex = releaseOrderIndex(releaseRows, targetRelease.id)
|
||||
const currentIndex = releaseOrderIndex(releaseRows, currentRelease.id)
|
||||
if (targetIndex >= 0 && currentIndex >= 0 && targetIndex !== currentIndex)
|
||||
return targetIndex < currentIndex ? 1 : -1
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function releaseDeploymentAction({
|
||||
targetRelease,
|
||||
currentRelease,
|
||||
releaseRows,
|
||||
isExistingRelease,
|
||||
}: {
|
||||
targetRelease?: ConsoleRelease | ReleaseRow
|
||||
currentRelease?: ConsoleRelease
|
||||
releaseRows: ReleaseRow[]
|
||||
isExistingRelease?: boolean
|
||||
}): ReleaseDeploymentAction {
|
||||
if (!currentRelease?.id)
|
||||
return isExistingRelease ? 'deployExistingRelease' : 'deploy'
|
||||
|
||||
const order = compareReleaseOrder(targetRelease, currentRelease, releaseRows)
|
||||
if (order === -1)
|
||||
return 'rollback'
|
||||
if (order === 1)
|
||||
return 'promote'
|
||||
|
||||
return targetRelease?.id && targetRelease.id !== currentRelease.id
|
||||
? 'promote'
|
||||
: isExistingRelease
|
||||
? 'deployExistingRelease'
|
||||
: 'deploy'
|
||||
}
|
||||
15
web/features/deployments/release.ts
Normal file
15
web/features/deployments/release.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { ConsoleRelease, ReleaseRow } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
export function formatDate(value?: string) {
|
||||
if (!value)
|
||||
return '—'
|
||||
return value.replace('T', ' ').replace(/\.\d+Z?$/, '').replace(/Z$/, '').slice(0, 16)
|
||||
}
|
||||
|
||||
export function releaseLabel(release?: ConsoleRelease | ReleaseRow) {
|
||||
return release?.name || release?.id || '—'
|
||||
}
|
||||
|
||||
export function releaseCommit(release?: ConsoleRelease | ReleaseRow) {
|
||||
return release && 'shortCommitId' in release ? release.shortCommitId || '—' : '—'
|
||||
}
|
||||
17
web/features/deployments/runtime-bindings.ts
Normal file
17
web/features/deployments/runtime-bindings.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { ReleaseRuntimeBinding } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
export function runtimeBindingSummary(binding?: ReleaseRuntimeBinding) {
|
||||
return binding?.label || binding?.displayValue || binding?.kind || '—'
|
||||
}
|
||||
|
||||
export function isRuntimeEnvVarBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return (binding?.kind?.toLowerCase() ?? '').includes('env')
|
||||
}
|
||||
|
||||
export function isRuntimeModelBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return (binding?.kind?.toLowerCase() ?? '').includes('model')
|
||||
}
|
||||
|
||||
export function isRuntimePluginBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return !isRuntimeEnvVarBinding(binding) && !isRuntimeModelBinding(binding)
|
||||
}
|
||||
16
web/features/deployments/runtime-status.ts
Normal file
16
web/features/deployments/runtime-status.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { RuntimeInstanceRow } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
type DeploymentUiStatus = 'ready' | 'deploying' | 'deploy_failed'
|
||||
|
||||
export function isUndeployedDeploymentRow(row?: RuntimeInstanceRow) {
|
||||
return (row?.status?.toLowerCase() ?? '').includes('undeployed') || (!row?.id && !row?.currentRelease && !row?.detail)
|
||||
}
|
||||
|
||||
export function deploymentStatus(row: RuntimeInstanceRow): DeploymentUiStatus {
|
||||
const runtimeStatus = row.status?.toLowerCase() ?? ''
|
||||
if (runtimeStatus.includes('deploying') || runtimeStatus.includes('pending'))
|
||||
return 'deploying'
|
||||
if (runtimeStatus.includes('fail') || runtimeStatus.includes('error'))
|
||||
return 'deploy_failed'
|
||||
return 'ready'
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import type { DeploymentEnvironmentOption } from '@dify/contracts/enterprise/types.gen'
|
||||
|
||||
export type EnvironmentMode = 'shared' | 'isolated'
|
||||
export type EnvironmentHealth = 'ready' | 'degraded'
|
||||
|
||||
export type DeployStatus = 'ready' | 'deploying' | 'deploy_failed'
|
||||
|
||||
export type AccessPermissionKind = 'organization' | 'specific' | 'anyone'
|
||||
|
||||
export type EnvironmentOption = DeploymentEnvironmentOption & {
|
||||
disabled?: boolean
|
||||
}
|
||||
@ -1,233 +0,0 @@
|
||||
import type {
|
||||
ConsoleEnvironment,
|
||||
ConsoleRelease,
|
||||
ListDeploymentEnvironmentOptionsReply,
|
||||
ReleaseRow,
|
||||
ReleaseRuntimeBinding,
|
||||
RuntimeInstanceRow,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type {
|
||||
AccessPermissionKind,
|
||||
EnvironmentOption,
|
||||
} from './types'
|
||||
import { PUBLIC_API_PREFIX } from '@/config'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type DeploymentUiStatus = 'ready' | 'deploying' | 'deploy_failed'
|
||||
export type ReleaseDeploymentAction = 'deploy' | 'deployExistingRelease' | 'promote' | 'rollback'
|
||||
|
||||
const appModeValues = new Set<string>(Object.values(AppModeEnum))
|
||||
|
||||
export function toAppMode(mode?: string): AppModeEnum {
|
||||
return appModeValues.has(mode ?? '') ? (mode as AppModeEnum) : AppModeEnum.WORKFLOW
|
||||
}
|
||||
|
||||
export function formatDate(value?: string) {
|
||||
if (!value)
|
||||
return '—'
|
||||
return value.replace('T', ' ').replace(/\.\d+Z?$/, '').replace(/Z$/, '').slice(0, 16)
|
||||
}
|
||||
|
||||
export function environmentId(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
return environment?.id ?? ''
|
||||
}
|
||||
|
||||
export function environmentName(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
return environment?.name || environment?.id || '—'
|
||||
}
|
||||
|
||||
export function environmentMode(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
const type = environment?.type?.toLowerCase() ?? ''
|
||||
return type.includes('isolated') ? 'isolated' : 'shared'
|
||||
}
|
||||
|
||||
function environmentRuntimeName(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
if (!environment)
|
||||
return ''
|
||||
if ('backend' in environment && environment.backend)
|
||||
return environment.backend
|
||||
if ('runtime' in environment && environment.runtime)
|
||||
return environment.runtime
|
||||
return ''
|
||||
}
|
||||
|
||||
export function environmentBackend(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
const runtime = environmentRuntimeName(environment).toLowerCase()
|
||||
return runtime.includes('host') ? 'host' : 'k8s'
|
||||
}
|
||||
|
||||
export function environmentHealth(environment?: ConsoleEnvironment | EnvironmentOption) {
|
||||
const status = environment?.status?.toLowerCase() ?? ''
|
||||
return status.includes('ready') ? 'ready' : 'degraded'
|
||||
}
|
||||
|
||||
export function releaseLabel(release?: ConsoleRelease | ReleaseRow) {
|
||||
return release?.name || release?.id || '—'
|
||||
}
|
||||
|
||||
export function releaseCommit(release?: ConsoleRelease | ReleaseRow) {
|
||||
return release && 'shortCommitId' in release ? release.shortCommitId || '—' : '—'
|
||||
}
|
||||
|
||||
function releaseCreatedAt(release?: ConsoleRelease | ReleaseRow) {
|
||||
const value = release?.createdAt
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
const time = Date.parse(value)
|
||||
return Number.isFinite(time) ? time : undefined
|
||||
}
|
||||
|
||||
function releaseById(releaseRows: ReleaseRow[], releaseId?: string) {
|
||||
return releaseRows.find(release => release.id === releaseId)
|
||||
}
|
||||
|
||||
function releaseOrderIndex(releaseRows: ReleaseRow[], releaseId?: string) {
|
||||
return releaseRows.findIndex(release => release.id === releaseId)
|
||||
}
|
||||
|
||||
function compareReleaseOrder(targetRelease: ConsoleRelease | ReleaseRow | undefined, currentRelease: ConsoleRelease, releaseRows: ReleaseRow[]) {
|
||||
if (!targetRelease?.id || !currentRelease.id)
|
||||
return undefined
|
||||
if (targetRelease.id === currentRelease.id)
|
||||
return 0
|
||||
|
||||
const normalizedTargetRelease = releaseById(releaseRows, targetRelease.id) ?? targetRelease
|
||||
const normalizedCurrentRelease = releaseById(releaseRows, currentRelease.id) ?? currentRelease
|
||||
const targetCreatedAt = releaseCreatedAt(normalizedTargetRelease)
|
||||
const currentCreatedAt = releaseCreatedAt(normalizedCurrentRelease)
|
||||
|
||||
if (targetCreatedAt !== undefined && currentCreatedAt !== undefined && targetCreatedAt !== currentCreatedAt)
|
||||
return targetCreatedAt > currentCreatedAt ? 1 : -1
|
||||
|
||||
const targetIndex = releaseOrderIndex(releaseRows, targetRelease.id)
|
||||
const currentIndex = releaseOrderIndex(releaseRows, currentRelease.id)
|
||||
if (targetIndex >= 0 && currentIndex >= 0 && targetIndex !== currentIndex)
|
||||
return targetIndex < currentIndex ? 1 : -1
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function releaseDeploymentAction({
|
||||
targetRelease,
|
||||
currentRelease,
|
||||
releaseRows,
|
||||
isExistingRelease,
|
||||
}: {
|
||||
targetRelease?: ConsoleRelease | ReleaseRow
|
||||
currentRelease?: ConsoleRelease
|
||||
releaseRows: ReleaseRow[]
|
||||
isExistingRelease?: boolean
|
||||
}): ReleaseDeploymentAction {
|
||||
if (!currentRelease?.id)
|
||||
return isExistingRelease ? 'deployExistingRelease' : 'deploy'
|
||||
|
||||
const order = compareReleaseOrder(targetRelease, currentRelease, releaseRows)
|
||||
if (order === -1)
|
||||
return 'rollback'
|
||||
if (order === 1)
|
||||
return 'promote'
|
||||
|
||||
return targetRelease?.id && targetRelease.id !== currentRelease.id
|
||||
? 'promote'
|
||||
: isExistingRelease
|
||||
? 'deployExistingRelease'
|
||||
: 'deploy'
|
||||
}
|
||||
|
||||
export function runtimeBindingSummary(binding?: ReleaseRuntimeBinding) {
|
||||
return binding?.label || binding?.displayValue || binding?.kind || '—'
|
||||
}
|
||||
|
||||
export function isRuntimeEnvVarBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return (binding?.kind?.toLowerCase() ?? '').includes('env')
|
||||
}
|
||||
|
||||
export function isRuntimeModelBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return (binding?.kind?.toLowerCase() ?? '').includes('model')
|
||||
}
|
||||
|
||||
export function isRuntimePluginBinding(binding?: ReleaseRuntimeBinding) {
|
||||
return !isRuntimeEnvVarBinding(binding) && !isRuntimeModelBinding(binding)
|
||||
}
|
||||
|
||||
const absoluteUrlRegExp = /^[a-z][a-z\d+.-]*:\/\//i
|
||||
|
||||
function withLeadingSlash(path: string) {
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
function publicWebappOrigin() {
|
||||
try {
|
||||
return new URL(PUBLIC_API_PREFIX).origin
|
||||
}
|
||||
catch {
|
||||
return PUBLIC_API_PREFIX.replace(/\/api\/?$/, '').replace(/\/+$/, '')
|
||||
}
|
||||
}
|
||||
|
||||
export function webappUrl(url?: string) {
|
||||
if (!url)
|
||||
return ''
|
||||
if (absoluteUrlRegExp.test(url))
|
||||
return url
|
||||
|
||||
const origin = publicWebappOrigin()
|
||||
return `${origin}${withLeadingSlash(url)}`
|
||||
}
|
||||
|
||||
export function deploymentId(row?: RuntimeInstanceRow) {
|
||||
return row?.id || ''
|
||||
}
|
||||
|
||||
export function activeRelease(row?: RuntimeInstanceRow) {
|
||||
return row?.currentRelease
|
||||
}
|
||||
|
||||
export function isUndeployedDeploymentRow(row?: RuntimeInstanceRow) {
|
||||
return (row?.status?.toLowerCase() ?? '').includes('undeployed') || (!row?.id && !row?.currentRelease && !row?.detail)
|
||||
}
|
||||
|
||||
export function deploymentStatus(row: RuntimeInstanceRow): DeploymentUiStatus {
|
||||
const runtimeStatus = row.status?.toLowerCase() ?? ''
|
||||
if (runtimeStatus.includes('deploying') || runtimeStatus.includes('pending'))
|
||||
return 'deploying'
|
||||
if (runtimeStatus.includes('fail') || runtimeStatus.includes('error'))
|
||||
return 'deploy_failed'
|
||||
return 'ready'
|
||||
}
|
||||
|
||||
export function deployedRows(rows?: RuntimeInstanceRow[]) {
|
||||
return rows?.filter((row) => {
|
||||
const runtimeStatus = row.status?.toLowerCase() ?? ''
|
||||
return row.environment?.id
|
||||
&& !isUndeployedDeploymentRow(row)
|
||||
&& (row.id || runtimeStatus || row.currentRelease || row.detail)
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
export function environmentOptionsFromOptionsReply(response?: ListDeploymentEnvironmentOptionsReply): EnvironmentOption[] {
|
||||
return response?.environments
|
||||
?.filter(environment => environment.id)
|
||||
.map(environment => ({
|
||||
...environment,
|
||||
disabled: environment.deployable === false,
|
||||
})) ?? []
|
||||
}
|
||||
|
||||
export function accessModeToPermissionKey(mode?: string): AccessPermissionKind {
|
||||
const normalized = mode?.toLowerCase() ?? ''
|
||||
if (normalized === 'private')
|
||||
return 'specific'
|
||||
if (normalized === 'public')
|
||||
return 'anyone'
|
||||
return 'organization'
|
||||
}
|
||||
|
||||
export function permissionKeyToAccessMode(key: AccessPermissionKind) {
|
||||
if (key === 'organization')
|
||||
return 'private_all'
|
||||
if (key === 'specific')
|
||||
return 'private'
|
||||
return 'public'
|
||||
}
|
||||
26
web/features/deployments/webapp-url.ts
Normal file
26
web/features/deployments/webapp-url.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { PUBLIC_API_PREFIX } from '@/config'
|
||||
|
||||
const absoluteUrlRegExp = /^[a-z][a-z\d+.-]*:\/\//i
|
||||
|
||||
function withLeadingSlash(path: string) {
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
function publicWebappOrigin() {
|
||||
try {
|
||||
return new URL(PUBLIC_API_PREFIX).origin
|
||||
}
|
||||
catch {
|
||||
return PUBLIC_API_PREFIX.replace(/\/api\/?$/, '').replace(/\/+$/, '')
|
||||
}
|
||||
}
|
||||
|
||||
export function webappUrl(url?: string) {
|
||||
if (!url)
|
||||
return ''
|
||||
if (absoluteUrlRegExp.test(url))
|
||||
return url
|
||||
|
||||
const origin = publicWebappOrigin()
|
||||
return `${origin}${withLeadingSlash(url)}`
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user