mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 04:51:11 +08:00
fix: disable deployment DSL imports (#37745)
This commit is contained in:
parent
084f122814
commit
25b90229bc
@ -0,0 +1,73 @@
|
||||
import type { Getter } from 'jotai'
|
||||
import { atom, createStore } from 'jotai'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('jotai-tanstack-query', () => ({
|
||||
atomWithInfiniteQuery: (createOptions: (get: Getter) => Record<string, unknown>) => atom((get) => {
|
||||
const options = createOptions(get)
|
||||
|
||||
return {
|
||||
...options,
|
||||
data: { pages: [{ data: [] }] },
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
}
|
||||
}),
|
||||
atomWithMutation: () => atom(() => ({
|
||||
isPending: false,
|
||||
mutateAsync: vi.fn(),
|
||||
})),
|
||||
atomWithQuery: (createOptions: (get: Getter) => Record<string, unknown>) => atom(get => ({
|
||||
...createOptions(get),
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
list: {
|
||||
infiniteOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
queryKey: ['apps', 'list'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
async function loadState() {
|
||||
return await import('../index')
|
||||
}
|
||||
|
||||
describe('create deployment guide state', () => {
|
||||
it('should keep the guide on source app mode when DSL import is disabled', async () => {
|
||||
const state = await loadState()
|
||||
const store = createStore()
|
||||
|
||||
store.set(state.selectMethodAtom, 'importDsl')
|
||||
|
||||
expect(store.get(state.methodAtom)).toBe('bindApp')
|
||||
expect(store.get(state.effectiveMethodAtom)).toBe('bindApp')
|
||||
})
|
||||
|
||||
it('should keep source app loading enabled if stale state points to DSL import', async () => {
|
||||
const state = await loadState()
|
||||
const store = createStore()
|
||||
|
||||
store.set(state.methodAtom, 'importDsl')
|
||||
|
||||
const sourceAppsQuery = store.get(state.sourceAppsQueryAtom) as unknown as { enabled?: boolean }
|
||||
|
||||
expect(store.get(state.effectiveMethodAtom)).toBe('bindApp')
|
||||
expect(sourceAppsQuery.enabled).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -27,6 +27,7 @@ import {
|
||||
isWorkflowDsl,
|
||||
} from '@/features/deployments/shared/domain/dsl'
|
||||
import { unsupportedDslNodeError } from '@/features/deployments/shared/domain/error'
|
||||
import { isDeploymentDslImportEnabled } from '@/features/deployments/shared/domain/feature-flags'
|
||||
import { createDeploymentIdempotencyKey } from '@/features/deployments/shared/domain/idempotency'
|
||||
import {
|
||||
DEPLOYMENT_PAGE_SIZE,
|
||||
@ -41,6 +42,12 @@ export type GuideMethod = 'bindApp' | 'importDsl'
|
||||
export type GuideStep = 'source' | 'release' | 'target'
|
||||
export type WorkflowSourceApp = App & { mode: Extract<AppModeEnum, 'workflow'> }
|
||||
|
||||
function deploymentGuideMethod(method: GuideMethod): GuideMethod {
|
||||
return method === 'importDsl' && !isDeploymentDslImportEnabled
|
||||
? 'bindApp'
|
||||
: method
|
||||
}
|
||||
|
||||
const RANDOM_SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const RANDOM_SUFFIX_LENGTH = 4
|
||||
const RANDOM_SUFFIX_FALLBACK_LENGTH = 6
|
||||
@ -124,6 +131,7 @@ function envVarInput(slot: EnvVarBindingSlot, selection: EnvVarValueSelection |
|
||||
// Workflow primitives
|
||||
export const stepAtom = atom<GuideStep>('source')
|
||||
export const methodAtom = atom<GuideMethod>('bindApp')
|
||||
export const effectiveMethodAtom = atom(get => deploymentGuideMethod(get(methodAtom)))
|
||||
|
||||
// Source primitives
|
||||
export const sourceSearchTextAtom = atom('')
|
||||
@ -145,7 +153,7 @@ export const dslDefaultAppNameAtom = atom((get) => {
|
||||
export const dslUnsupportedModeAtom = atom((get) => {
|
||||
const dslContent = get(dslContentAtom)
|
||||
|
||||
return get(methodAtom) === 'importDsl'
|
||||
return get(effectiveMethodAtom) === 'importDsl'
|
||||
&& Boolean(dslContent.trim())
|
||||
&& !get(isReadingDslAtom)
|
||||
&& !get(dslReadErrorAtom)
|
||||
@ -199,7 +207,7 @@ export const sourceAppsQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
initialPageParam: 1,
|
||||
placeholderData: keepPreviousData,
|
||||
}),
|
||||
enabled: get(methodAtom) === 'bindApp',
|
||||
enabled: get(effectiveMethodAtom) === 'bindApp',
|
||||
}
|
||||
})
|
||||
|
||||
@ -218,7 +226,7 @@ export const effectiveSelectedAppAtom = atom((get) => {
|
||||
})
|
||||
|
||||
function sourceReady(get: Getter) {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
|
||||
return method === 'importDsl'
|
||||
? get(importDslReadyAtom)
|
||||
@ -269,7 +277,7 @@ export const deployableEnvironmentsQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
|
||||
const precheckReleaseQueryAtom = atomWithQuery((get) => {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
const dslContent = get(dslContentAtom)
|
||||
const enabled = sourceReady(get)
|
||||
@ -310,7 +318,7 @@ function precheckReleaseReady(get: Getter) {
|
||||
}
|
||||
|
||||
export const deploymentOptionsQueryAtom = atomWithQuery((get) => {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
const dslContent = get(dslContentAtom)
|
||||
const enabled = precheckReleaseReady(get)
|
||||
@ -378,7 +386,7 @@ const deploymentOptionsContentCheckedAtom = atom((get) => {
|
||||
})
|
||||
|
||||
export const sourceCanGoNextAtom = atom((get) => {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
const importDslReady = method === 'importDsl' && get(importDslReadyAtom)
|
||||
const bindAppReady = method === 'bindApp' && Boolean(effectiveSelectedApp?.id)
|
||||
@ -416,7 +424,7 @@ export const continueFromSourceAtom = atom(null, (get, set, {
|
||||
if (!get(sourceCanGoNextAtom))
|
||||
return
|
||||
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
if (method === 'bindApp' && effectiveSelectedApp)
|
||||
set(selectSourceAppAtom, effectiveSelectedApp)
|
||||
@ -606,7 +614,7 @@ const requiredBindingsReadyAtom = atom((get) => {
|
||||
})
|
||||
|
||||
export const deploymentTargetEnvVarSlotsAtom = atom((get) => {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const deploymentOptionsQuery = get(deploymentOptionsQueryAtom)
|
||||
const slots = sourceReady(get) ? deploymentOptionsQuery.data?.options?.envVarSlots : undefined
|
||||
const dslContent = get(dslContentAtom)
|
||||
@ -702,7 +710,7 @@ export const setEnvVarAtom = atom(null, (get, set, key: string, value: EnvVarVal
|
||||
|
||||
// Workflow actions
|
||||
export const selectMethodAtom = atom(null, (_get, set, method: GuideMethod) => {
|
||||
set(methodAtom, method)
|
||||
set(methodAtom, deploymentGuideMethod(method))
|
||||
set(selectedEnvironmentIdAtom, '')
|
||||
set(manualBindingSelectionsAtom, {})
|
||||
set(envVarValuesAtom, {})
|
||||
@ -738,7 +746,7 @@ export const createDeploymentGuideSubmissionAtom = atom(null, async (get, set, {
|
||||
}: {
|
||||
deployToEnvironment: boolean
|
||||
}) => {
|
||||
const method = get(methodAtom)
|
||||
const method = get(effectiveMethodAtom)
|
||||
const dslContent = get(dslContentAtom)
|
||||
const submittedInstanceName = get(instanceNameAtom).trim()
|
||||
const submittedReleaseName = get(releaseNameAtom).trim()
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SourceStepContent } from '../source-step'
|
||||
|
||||
vi.mock('@/features/deployments/create-guide/state', async () => {
|
||||
const { atom } = await import('jotai')
|
||||
const methodAtom = atom<'bindApp' | 'importDsl'>('bindApp')
|
||||
const emptyActionAtom = atom(null, () => undefined)
|
||||
|
||||
return {
|
||||
continueFromSourceAtom: emptyActionAtom,
|
||||
dslFileAtom: atom<File | undefined>(undefined),
|
||||
dslReadErrorAtom: atom(false),
|
||||
dslUnsupportedModeAtom: atom(false),
|
||||
effectiveMethodAtom: atom(get => get(methodAtom)),
|
||||
effectiveSelectedAppAtom: atom(undefined),
|
||||
isReadingDslAtom: atom(false),
|
||||
methodAtom,
|
||||
selectDslFileAtom: emptyActionAtom,
|
||||
selectMethodAtom: atom(null, (_get, set, value: 'bindApp' | 'importDsl') => {
|
||||
set(methodAtom, value)
|
||||
}),
|
||||
selectSourceAppAtom: emptyActionAtom,
|
||||
setSourceSearchTextAtom: emptyActionAtom,
|
||||
sourceAppsQueryAtom: atom({
|
||||
data: { pages: [{ data: [] }] },
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
}),
|
||||
sourceCanGoNextAtom: atom(false),
|
||||
sourceSearchTextAtom: atom(''),
|
||||
unsupportedDslNodesAtom: atom([]),
|
||||
}
|
||||
})
|
||||
|
||||
describe('SourceStepContent', () => {
|
||||
it('should hide the import DSL option when deployment DSL import is disabled', () => {
|
||||
render(<SourceStepContent />)
|
||||
|
||||
expect(screen.getByText(/createGuide\.methods\.bindApp\.title/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/createGuide\.methods\.importDsl\.title/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/createGuide\.methods\.importDsl\.description/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: /createGuide\.source\.sourceApp/ })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -7,10 +7,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
continueFromReleaseAtom,
|
||||
dslDefaultAppNameAtom,
|
||||
effectiveMethodAtom,
|
||||
hasInstanceNameConflictAtom,
|
||||
instanceDescriptionAtom,
|
||||
instanceNameAtom,
|
||||
methodAtom,
|
||||
releaseCanGoNextAtom,
|
||||
releaseDescriptionAtom,
|
||||
releaseNameAtom,
|
||||
@ -74,7 +74,7 @@ function InstanceNameField() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const instanceName = useAtomValue(instanceNameAtom)
|
||||
const setInstanceName = useSetAtom(setInstanceNameAtom)
|
||||
const method = useAtomValue(methodAtom)
|
||||
const method = useAtomValue(effectiveMethodAtom)
|
||||
const selectedApp = useAtomValue(selectedAppAtom)
|
||||
const dslDefaultAppName = useAtomValue(dslDefaultAppNameAtom)
|
||||
const instanceNamePlaceholder = method === 'importDsl'
|
||||
|
||||
@ -19,9 +19,9 @@ import {
|
||||
dslFileAtom,
|
||||
dslReadErrorAtom,
|
||||
dslUnsupportedModeAtom,
|
||||
effectiveMethodAtom,
|
||||
effectiveSelectedAppAtom,
|
||||
isReadingDslAtom,
|
||||
methodAtom,
|
||||
selectDslFileAtom,
|
||||
selectMethodAtom,
|
||||
selectSourceAppAtom,
|
||||
@ -31,12 +31,13 @@ import {
|
||||
sourceSearchTextAtom,
|
||||
unsupportedDslNodesAtom,
|
||||
} from '@/features/deployments/create-guide/state'
|
||||
import { isDeploymentDslImportEnabled } from '@/features/deployments/shared/domain/feature-flags'
|
||||
import { StepShell } from './layout'
|
||||
|
||||
const sourceAppSkeletonKeys = ['first-source-app', 'second-source-app', 'third-source-app']
|
||||
|
||||
export function SourceStepContent() {
|
||||
const method = useAtomValue(methodAtom)
|
||||
const method = useAtomValue(effectiveMethodAtom)
|
||||
const unsupportedDslNodes = useAtomValue(unsupportedDslNodesAtom)
|
||||
|
||||
return (
|
||||
@ -55,7 +56,7 @@ export function SourceStepContent() {
|
||||
|
||||
function SourceMethodSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const method = useAtomValue(methodAtom)
|
||||
const method = useAtomValue(effectiveMethodAtom)
|
||||
const selectMethod = useSetAtom(selectMethodAtom)
|
||||
|
||||
return (
|
||||
@ -76,12 +77,14 @@ function SourceMethodSection() {
|
||||
title={t('createGuide.methods.bindApp.title')}
|
||||
description={t('createGuide.methods.bindApp.description')}
|
||||
/>
|
||||
<SourceMethodCard
|
||||
value="importDsl"
|
||||
icon="i-ri-file-code-line"
|
||||
title={t('createGuide.methods.importDsl.title')}
|
||||
description={t('createGuide.methods.importDsl.description')}
|
||||
/>
|
||||
{isDeploymentDslImportEnabled && (
|
||||
<SourceMethodCard
|
||||
value="importDsl"
|
||||
icon="i-ri-file-code-line"
|
||||
title={t('createGuide.methods.importDsl.title')}
|
||||
description={t('createGuide.methods.importDsl.description')}
|
||||
/>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</StepShell>
|
||||
)
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SourceAppPicker } from '../source-app-picker'
|
||||
|
||||
function renderSourceAppPicker(disabled: boolean) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SourceAppPicker
|
||||
value={{ id: 'app-1', name: 'Workflow 1' }}
|
||||
onChange={() => undefined}
|
||||
ariaLabel="Source app"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('SourceAppPicker', () => {
|
||||
it('should disable the switch control when disabled', () => {
|
||||
renderSourceAppPicker(true)
|
||||
|
||||
expect(screen.getByText('Workflow 1')).toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'Source app' })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,36 @@
|
||||
import type { CreateReleaseSourceSelection } from '../use-release-content-check'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canCheckReleaseSourceContent } from '../use-release-content-check'
|
||||
|
||||
function releaseSource(overrides: Partial<CreateReleaseSourceSelection> = {}): CreateReleaseSourceSelection {
|
||||
return {
|
||||
dslContent: '',
|
||||
dslReadError: false,
|
||||
encodedDslContent: '',
|
||||
hasDslContent: false,
|
||||
hasUnsupportedDslMode: false,
|
||||
isReadingDsl: false,
|
||||
isWorkflowDslContent: false,
|
||||
releaseSourceMode: 'sourceApp',
|
||||
selectedSourceAppId: undefined,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('canCheckReleaseSourceContent', () => {
|
||||
it('should allow source app releases when a source app is selected', () => {
|
||||
expect(canCheckReleaseSourceContent(releaseSource({
|
||||
selectedSourceAppId: 'app-1',
|
||||
}))).toBe(true)
|
||||
})
|
||||
|
||||
it('should block DSL release content checks when deployment DSL import is disabled', () => {
|
||||
expect(canCheckReleaseSourceContent(releaseSource({
|
||||
dslContent: 'app:\n mode: workflow',
|
||||
encodedDslContent: 'encoded-dsl',
|
||||
hasDslContent: true,
|
||||
isWorkflowDslContent: true,
|
||||
releaseSourceMode: 'dsl',
|
||||
}))).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -8,6 +8,7 @@ import { ScopeProvider } from 'jotai-scope'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags'
|
||||
import {
|
||||
closeCreateReleaseDialogAtom,
|
||||
createReleaseDialogOpenAtom,
|
||||
@ -87,13 +88,19 @@ function CreateReleaseDefaultSourceApp({ formValues }: {
|
||||
const defaultSourceApp = latestSourceAppId
|
||||
? workflowSourceAppPickerValue(defaultSourceAppQuery.data, latestSourceAppId)
|
||||
: undefined
|
||||
const sourceAppLocked = !isDeploymentDslImportEnabled
|
||||
const releaseSourceMode = formValues.releaseSourceMode === 'dsl' && !isDeploymentDslImportEnabled
|
||||
? 'sourceApp'
|
||||
: formValues.releaseSourceMode
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDialogOpen || formValues.releaseSourceMode !== 'sourceApp' || formValues.sourceApp || !defaultSourceApp)
|
||||
if (!isDialogOpen || releaseSourceMode !== 'sourceApp' || !defaultSourceApp)
|
||||
return
|
||||
if (formValues.sourceApp && (!sourceAppLocked || formValues.sourceApp.id === defaultSourceApp.id))
|
||||
return
|
||||
|
||||
form.setFieldValue('sourceApp', defaultSourceApp)
|
||||
}, [defaultSourceApp, form, formValues.releaseSourceMode, formValues.sourceApp, isDialogOpen])
|
||||
}, [defaultSourceApp, form, formValues.sourceApp, isDialogOpen, releaseSourceMode, sourceAppLocked])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -31,16 +31,20 @@ function sourceAppSearchText(app: App) {
|
||||
return `${app.name} ${app.id}`.toLowerCase()
|
||||
}
|
||||
|
||||
function SourceAppTrigger({ open, app }: {
|
||||
function SourceAppTrigger({ open, app, disabled }: {
|
||||
open: boolean
|
||||
app?: SourceAppPickerValue
|
||||
disabled: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'group flex h-10 cursor-pointer items-center gap-2 rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-left hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'group flex h-10 items-center gap-2 rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-left',
|
||||
disabled
|
||||
? 'cursor-not-allowed text-components-input-text-disabled'
|
||||
: 'cursor-pointer hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
open && 'border-components-input-border-active bg-components-input-bg-active shadow-xs',
|
||||
app && 'pl-2',
|
||||
)}
|
||||
@ -70,6 +74,7 @@ function SourceAppTrigger({ open, app }: {
|
||||
<span
|
||||
className={cn(
|
||||
'i-ri-arrow-down-s-line size-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
|
||||
disabled && 'opacity-50 group-hover:text-text-quaternary',
|
||||
open && 'text-text-secondary',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
@ -123,10 +128,11 @@ function SourceAppPickerSkeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
export function SourceAppPicker({ value, onChange, ariaLabel }: {
|
||||
export function SourceAppPicker({ value, onChange, ariaLabel, disabled = false }: {
|
||||
value?: SourceAppPickerValue
|
||||
onChange: (app: App) => void
|
||||
ariaLabel?: string
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
@ -152,6 +158,7 @@ export function SourceAppPicker({ value, onChange, ariaLabel }: {
|
||||
initialPageParam: 1,
|
||||
placeholderData: keepPreviousData,
|
||||
}),
|
||||
enabled: !disabled,
|
||||
})
|
||||
|
||||
const apps = data?.pages.flatMap(page => page.data).filter(isWorkflowApp) ?? []
|
||||
@ -159,11 +166,18 @@ export function SourceAppPicker({ value, onChange, ariaLabel }: {
|
||||
return (
|
||||
<Combobox<App>
|
||||
items={apps}
|
||||
open={isShow}
|
||||
open={!disabled && isShow}
|
||||
inputValue={searchText}
|
||||
onOpenChange={setIsShow}
|
||||
onInputValueChange={setSearchText}
|
||||
onOpenChange={(open) => {
|
||||
setIsShow(disabled ? false : open)
|
||||
}}
|
||||
onInputValueChange={(value) => {
|
||||
if (!disabled)
|
||||
setSearchText(value)
|
||||
}}
|
||||
onValueChange={(app) => {
|
||||
if (disabled)
|
||||
return
|
||||
if (!app)
|
||||
return
|
||||
onChange(app)
|
||||
@ -182,14 +196,14 @@ export function SourceAppPicker({ value, onChange, ariaLabel }: {
|
||||
return app.id
|
||||
}}
|
||||
filter={(app, query) => sourceAppSearchText(app).includes(query.toLowerCase())}
|
||||
disabled={false}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
aria-label={ariaLabel ?? t('createModal.sourceApp')}
|
||||
icon={false}
|
||||
className="block h-auto w-full border-0 bg-transparent p-0 text-left hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 data-open:bg-transparent"
|
||||
>
|
||||
<SourceAppTrigger open={isShow} app={value} />
|
||||
<SourceAppTrigger open={!disabled && isShow} app={value} disabled={disabled} />
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent
|
||||
placement="bottom-start"
|
||||
|
||||
@ -5,6 +5,7 @@ import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segm
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||
import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags'
|
||||
import {
|
||||
clearCreateReleaseSubmissionErrorAtom,
|
||||
createReleaseDslStateAtom,
|
||||
@ -32,39 +33,41 @@ export function ReleaseSourceSection() {
|
||||
<label id="release-source-mode-label" className="system-xs-medium-uppercase text-text-tertiary">
|
||||
{t('versions.releaseSourceLabel')}
|
||||
</label>
|
||||
<SegmentedControl<ReleaseSourceMode>
|
||||
aria-labelledby="release-source-mode-label"
|
||||
value={[modeField.state.value]}
|
||||
onValueChange={(value) => {
|
||||
const nextMode = selectedReleaseSourceMode(value)
|
||||
if (!nextMode || nextMode === modeField.state.value)
|
||||
return
|
||||
{isDeploymentDslImportEnabled && (
|
||||
<SegmentedControl<ReleaseSourceMode>
|
||||
aria-labelledby="release-source-mode-label"
|
||||
value={[modeField.state.value]}
|
||||
onValueChange={(value) => {
|
||||
const nextMode = selectedReleaseSourceMode(value)
|
||||
if (!nextMode || nextMode === modeField.state.value)
|
||||
return
|
||||
|
||||
clearSubmissionError()
|
||||
modeField.handleChange(nextMode)
|
||||
if (nextMode === 'sourceApp') {
|
||||
form.setFieldValue('dslFile', undefined)
|
||||
resetDslFile()
|
||||
}
|
||||
else {
|
||||
form.setFieldValue('sourceApp', undefined)
|
||||
}
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
<SegmentedControlItem value="sourceApp" className="gap-1.5">
|
||||
<span className="i-ri-apps-2-line size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{t('versions.sourceAppOption')}</span>
|
||||
</SegmentedControlItem>
|
||||
<SegmentedControlItem value="dsl" className="gap-1.5">
|
||||
<span className="i-ri-upload-cloud-2-line size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{t('versions.manualDslOption')}</span>
|
||||
</SegmentedControlItem>
|
||||
</SegmentedControl>
|
||||
clearSubmissionError()
|
||||
modeField.handleChange(nextMode)
|
||||
if (nextMode === 'sourceApp') {
|
||||
form.setFieldValue('dslFile', undefined)
|
||||
resetDslFile()
|
||||
}
|
||||
else {
|
||||
form.setFieldValue('sourceApp', undefined)
|
||||
}
|
||||
}}
|
||||
className="shrink-0"
|
||||
>
|
||||
<SegmentedControlItem value="sourceApp" className="gap-1.5">
|
||||
<span className="i-ri-apps-2-line size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{t('versions.sourceAppOption')}</span>
|
||||
</SegmentedControlItem>
|
||||
<SegmentedControlItem value="dsl" className="gap-1.5">
|
||||
<span className="i-ri-upload-cloud-2-line size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{t('versions.manualDslOption')}</span>
|
||||
</SegmentedControlItem>
|
||||
</SegmentedControl>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-h-12">
|
||||
{modeField.state.value === 'sourceApp'
|
||||
{modeField.state.value === 'sourceApp' || !isDeploymentDslImportEnabled
|
||||
? <SourceAppField />
|
||||
: <DslFileField />}
|
||||
</div>
|
||||
@ -78,6 +81,7 @@ function SourceAppField() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const form = useCreateReleaseFormApi()
|
||||
const clearSubmissionError = useSetAtom(clearCreateReleaseSubmissionErrorAtom)
|
||||
const sourceAppLocked = !isDeploymentDslImportEnabled
|
||||
|
||||
return (
|
||||
<form.Field name="sourceApp">
|
||||
@ -90,6 +94,7 @@ function SourceAppField() {
|
||||
clearSubmissionError()
|
||||
}}
|
||||
ariaLabel={t('versions.sourceAppOption')}
|
||||
disabled={sourceAppLocked}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -65,7 +65,7 @@ export function useCreateReleaseSubmission(formValues: CreateReleaseFormValues)
|
||||
if (!canCheckReleaseSourceContent(sourceSelection) || !releaseContent.releaseContentReady)
|
||||
return
|
||||
|
||||
if (value.releaseSourceMode === 'dsl') {
|
||||
if (sourceSelection.releaseSourceMode === 'dsl') {
|
||||
if (!sourceSelection.isWorkflowDslContent) {
|
||||
toast.error(t('versions.dslUnsupportedMode'))
|
||||
return
|
||||
|
||||
@ -5,6 +5,7 @@ import type { CreateReleaseFormValues, ReleaseSourceMode } from '../state/types'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags'
|
||||
import {
|
||||
createReleaseDialogOpenAtom,
|
||||
createReleaseDslStateAtom,
|
||||
@ -21,17 +22,20 @@ function createReleaseSourceSelection(
|
||||
formValues: CreateReleaseFormValues,
|
||||
dslState: CreateReleaseDslState,
|
||||
): CreateReleaseSourceSelection {
|
||||
const hasUnsupportedDslMode = formValues.releaseSourceMode === 'dsl'
|
||||
const releaseSourceMode = formValues.releaseSourceMode === 'dsl' && !isDeploymentDslImportEnabled
|
||||
? 'sourceApp'
|
||||
: formValues.releaseSourceMode
|
||||
const hasUnsupportedDslMode = releaseSourceMode === 'dsl'
|
||||
&& dslState.hasDslContent
|
||||
&& !dslState.isReadingDsl
|
||||
&& !dslState.dslReadError
|
||||
&& !dslState.isWorkflowDslContent
|
||||
const selectedSourceAppId = formValues.releaseSourceMode === 'sourceApp' ? formValues.sourceApp?.id : undefined
|
||||
const selectedSourceAppId = releaseSourceMode === 'sourceApp' ? formValues.sourceApp?.id : undefined
|
||||
|
||||
return {
|
||||
...dslState,
|
||||
hasUnsupportedDslMode,
|
||||
releaseSourceMode: formValues.releaseSourceMode,
|
||||
releaseSourceMode,
|
||||
selectedSourceAppId,
|
||||
}
|
||||
}
|
||||
@ -39,6 +43,8 @@ function createReleaseSourceSelection(
|
||||
export function canCheckReleaseSourceContent(releaseSource: CreateReleaseSourceSelection) {
|
||||
if (releaseSource.releaseSourceMode === 'sourceApp')
|
||||
return Boolean(releaseSource.selectedSourceAppId)
|
||||
if (!isDeploymentDslImportEnabled)
|
||||
return false
|
||||
|
||||
return Boolean(
|
||||
releaseSource.hasDslContent
|
||||
|
||||
2
web/features/deployments/shared/domain/feature-flags.ts
Normal file
2
web/features/deployments/shared/domain/feature-flags.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Temporary kill switch for deployment DSL import flows. Flip to true to restore them.
|
||||
export const isDeploymentDslImportEnabled = false
|
||||
Loading…
Reference in New Issue
Block a user