mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
refactor(web): reduce query atom subscriptions (#38521)
This commit is contained in:
parent
09c5c5e5ed
commit
5308b95aff
@ -54,6 +54,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
|
||||
- Treat `useParams`, route args, and `nuqs` query state as framework-owned state. When atom logic needs those values, hydrate primitive atoms at the route or surface boundary, such as with `useHydrateAtoms(..., { dangerouslyForceHydrate: true })`; keep URL updates in the route/query-state APIs instead of write atoms.
|
||||
- Within a route-owned feature, choose one source for route identity. If route params are bridged into feature atoms, use that bridge consistently for route-derived queries and actions instead of also threading the same route id through page, tab, and section props.
|
||||
- For async work tied to atom state, use `atomWithQuery` or `atomWithMutation`; write atoms should update only the inputs that drive those atoms. This applies to pure frontend async work as well as network requests, so do not hand-roll loading/error/in-flight state with `useState` or `useRef` for atom-orchestrated async behavior. For component-owned remote work, use `useQuery` or `useMutation` directly.
|
||||
- `jotai-tanstack-query` query atoms do not support TanStack Query tracked properties. A component that reads `useAtomValue(queryAtom)` subscribes to the whole query result, even if it only accesses `data`, `isLoading`, or `isError`. Export field-specific derived atoms and have components read the exact fields they render; use `selectAtom(queryAtom, result => result.field)` for query-result fields so unchanged selections do not notify subscribers. Keep direct `useAtomValue(queryAtom)` only when the component or hook genuinely needs the full observer result.
|
||||
- Row-local async state belongs to the row owner unless it participates in a shared Jotai workflow or needs atom-scoped reset semantics.
|
||||
- Leave query and mutation atoms unscoped so they keep shared QueryClient cache and invalidation behavior. Scope resettable primitives and explicit hydration tuples; scope a derived atom only when every dependency should be private to that surface.
|
||||
- For scoped primitives that are always hydrated by `ScopeProvider`, prefer `atomWithLazy<T>(() => { throw new Error(...) })` when consumers should see a non-null type.
|
||||
|
||||
@ -4,6 +4,7 @@ import type { Getter } from 'jotai/vanilla'
|
||||
import { keepPreviousData, skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { encodeDslContent } from '@/features/deployments/shared/domain/dsl'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { effectiveMethodAtom, instanceNameAtom, submissionUnsupportedDslNodesAtom } from './primitives'
|
||||
@ -53,6 +54,11 @@ export const deployableEnvironmentsQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deployableEnvironmentsDataAtom = selectAtom(deployableEnvironmentsQueryAtom, query => query.data)
|
||||
export const deployableEnvironmentsIsErrorAtom = selectAtom(deployableEnvironmentsQueryAtom, query => query.isError)
|
||||
export const deployableEnvironmentsIsLoadingAtom = selectAtom(deployableEnvironmentsQueryAtom, query => query.isLoading)
|
||||
export const deployableEnvironmentsIsFetchingAtom = selectAtom(deployableEnvironmentsQueryAtom, query => query.isFetching)
|
||||
|
||||
const precheckReleaseQueryAtom = atomWithQuery((get) => {
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
@ -88,17 +94,22 @@ const precheckReleaseQueryAtom = atomWithQuery((get) => {
|
||||
return precheckReleaseQueryOptions
|
||||
})
|
||||
|
||||
const precheckReleaseDataAtom = selectAtom(precheckReleaseQueryAtom, query => query.data)
|
||||
const precheckReleaseIsSuccessAtom = selectAtom(precheckReleaseQueryAtom, query => query.isSuccess)
|
||||
const precheckReleaseIsLoadingAtom = selectAtom(precheckReleaseQueryAtom, query => query.isLoading)
|
||||
const precheckReleaseIsFetchingAtom = selectAtom(precheckReleaseQueryAtom, query => query.isFetching)
|
||||
|
||||
function precheckReleaseReady(get: Getter) {
|
||||
const precheckReleaseQuery = get(precheckReleaseQueryAtom)
|
||||
const precheckRelease = get(precheckReleaseDataAtom)
|
||||
|
||||
return sourceReady(get)
|
||||
&& precheckReleaseQuery.isSuccess
|
||||
&& Boolean(precheckReleaseQuery.data?.canCreate)
|
||||
&& (precheckReleaseQuery.data?.unsupportedNodes.length ?? 0) === 0
|
||||
&& get(precheckReleaseIsSuccessAtom)
|
||||
&& Boolean(precheckRelease?.canCreate)
|
||||
&& (precheckRelease?.unsupportedNodes.length ?? 0) === 0
|
||||
&& get(submissionUnsupportedDslNodesAtom).length === 0
|
||||
}
|
||||
|
||||
export const deploymentOptionsQueryAtom = atomWithQuery((get) => {
|
||||
const deploymentOptionsQueryAtom = atomWithQuery((get) => {
|
||||
const method = get(effectiveMethodAtom)
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
const dslContent = get(dslContentAtom)
|
||||
@ -133,6 +144,12 @@ export const deploymentOptionsQueryAtom = atomWithQuery((get) => {
|
||||
return deploymentOptionsQueryOptions
|
||||
})
|
||||
|
||||
export const deploymentOptionsDataAtom = selectAtom(deploymentOptionsQueryAtom, query => query.data)
|
||||
export const deploymentOptionsIsErrorAtom = selectAtom(deploymentOptionsQueryAtom, query => query.isError)
|
||||
export const deploymentOptionsIsLoadingAtom = selectAtom(deploymentOptionsQueryAtom, query => query.isLoading)
|
||||
export const deploymentOptionsIsFetchingAtom = selectAtom(deploymentOptionsQueryAtom, query => query.isFetching)
|
||||
const deploymentOptionsIsSuccessAtom = selectAtom(deploymentOptionsQueryAtom, query => query.isSuccess)
|
||||
|
||||
export const unsupportedDslNodesAtom = atom((get) => {
|
||||
const submissionUnsupportedDslNodes = get(submissionUnsupportedDslNodesAtom)
|
||||
if (submissionUnsupportedDslNodes.length > 0)
|
||||
@ -141,7 +158,7 @@ export const unsupportedDslNodesAtom = atom((get) => {
|
||||
if (!sourceReady(get))
|
||||
return []
|
||||
|
||||
return get(precheckReleaseQueryAtom).data?.unsupportedNodes ?? []
|
||||
return get(precheckReleaseDataAtom)?.unsupportedNodes ?? []
|
||||
})
|
||||
|
||||
const precheckReleaseReadyAtom = atom((get) => {
|
||||
@ -149,21 +166,17 @@ const precheckReleaseReadyAtom = atom((get) => {
|
||||
})
|
||||
|
||||
export const deploymentOptionsReadyAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(deploymentOptionsQueryAtom)
|
||||
|
||||
return sourceReady(get)
|
||||
&& get(precheckReleaseReadyAtom)
|
||||
&& deploymentOptionsQuery.isSuccess
|
||||
&& get(deploymentOptionsIsSuccessAtom)
|
||||
})
|
||||
|
||||
export const deploymentOptionsContentCheckedAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(deploymentOptionsQueryAtom)
|
||||
const precheckReleaseQuery = get(precheckReleaseQueryAtom)
|
||||
const isLoadingOptions = deploymentOptionsQuery.isLoading || (deploymentOptionsQuery.isFetching && !deploymentOptionsQuery.data)
|
||||
const isCheckingReleaseContent = precheckReleaseQuery.isLoading || (precheckReleaseQuery.isFetching && !precheckReleaseQuery.data)
|
||||
const isLoadingOptions = get(deploymentOptionsIsLoadingAtom) || (get(deploymentOptionsIsFetchingAtom) && !get(deploymentOptionsDataAtom))
|
||||
const isCheckingReleaseContent = get(precheckReleaseIsLoadingAtom) || (get(precheckReleaseIsFetchingAtom) && !get(precheckReleaseDataAtom))
|
||||
|
||||
if (!sourceReady(get) || isCheckingReleaseContent || isLoadingOptions)
|
||||
return false
|
||||
|
||||
return get(precheckReleaseReadyAtom) && deploymentOptionsQuery.isSuccess
|
||||
return get(precheckReleaseReadyAtom) && get(deploymentOptionsIsSuccessAtom)
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ import type { WorkflowSourceApp } from './types'
|
||||
import { keepPreviousData, queryOptions } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { dslAppName, isWorkflowDsl } from '@/features/deployments/shared/domain/dsl'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { normalizeAppPagination } from '@/service/use-apps'
|
||||
@ -92,18 +93,28 @@ export const sourceAppsQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
const sourceAppsDataAtom = selectAtom(sourceAppsQueryAtom, query => query.data)
|
||||
export const sourceAppsErrorAtom = selectAtom(sourceAppsQueryAtom, query => query.error)
|
||||
export const sourceAppsFetchNextPageAtom = selectAtom(sourceAppsQueryAtom, query => query.fetchNextPage)
|
||||
export const sourceAppsHasNextPageAtom = selectAtom(sourceAppsQueryAtom, query => query.hasNextPage)
|
||||
export const sourceAppsIsFetchingAtom = selectAtom(sourceAppsQueryAtom, query => query.isFetching)
|
||||
export const sourceAppsIsFetchingNextPageAtom = selectAtom(sourceAppsQueryAtom, query => query.isFetchingNextPage)
|
||||
export const sourceAppsIsLoadingAtom = selectAtom(sourceAppsQueryAtom, query => query.isLoading)
|
||||
export const sourceAppsIsPlaceholderDataAtom = selectAtom(sourceAppsQueryAtom, query => query.isPlaceholderData)
|
||||
|
||||
export const sourceAppsAtom = atom((get) => {
|
||||
return (get(sourceAppsDataAtom)?.pages.flatMap(page => page.data) ?? []) as WorkflowSourceApp[]
|
||||
})
|
||||
|
||||
export const effectiveSelectedAppAtom = atom((get) => {
|
||||
const selectedApp = get(selectedAppAtom)
|
||||
if (selectedApp)
|
||||
return selectedApp
|
||||
|
||||
const sourceAppsQuery = get(sourceAppsQueryAtom)
|
||||
if (sourceAppsQuery.isPlaceholderData)
|
||||
if (get(sourceAppsIsPlaceholderDataAtom))
|
||||
return undefined
|
||||
|
||||
const sourceApps = (sourceAppsQuery.data?.pages.flatMap(page => page.data) ?? []) as WorkflowSourceApp[]
|
||||
|
||||
return sourceApps[0]
|
||||
return get(sourceAppsAtom)[0]
|
||||
})
|
||||
|
||||
export function sourceReady(get: Getter) {
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
selectedEnvironmentIdAtom,
|
||||
submissionUnsupportedDslNodesAtom,
|
||||
} from './primitives'
|
||||
import { deployableEnvironmentsQueryAtom, deploymentOptionsQueryAtom } from './queries'
|
||||
import { deployableEnvironmentsQueryAtom, deploymentOptionsDataAtom } from './queries'
|
||||
import { submittedReleaseReadyAtom } from './release'
|
||||
import { dslContentAtom, effectiveSelectedAppAtom } from './source'
|
||||
import {
|
||||
@ -75,7 +75,7 @@ export const createDeploymentGuideSubmissionAtom = atom(null, async (get, set, {
|
||||
|
||||
const effectiveSelectedApp = get(effectiveSelectedAppAtom)
|
||||
const deployableEnvironmentsQuery = get(deployableEnvironmentsQueryAtom)
|
||||
const deploymentOptions = get(deploymentOptionsQueryAtom).data?.options
|
||||
const deploymentOptions = get(deploymentOptionsDataAtom)?.options
|
||||
const envVarSlots = get(deploymentTargetEnvVarSlotsAtom)
|
||||
const envVarValues = get(envVarValuesAtom)
|
||||
const bindingSlots = get(deploymentTargetBindingSlotsAtom)
|
||||
|
||||
@ -11,23 +11,21 @@ import {
|
||||
import { dslEnvVarSlots } from '@/features/deployments/shared/domain/dsl'
|
||||
import { environmentMatchesIdentifier } from './environment'
|
||||
import { effectiveMethodAtom, envVarValuesAtom, manualBindingSelectionsAtom, selectedEnvironmentIdAtom } from './primitives'
|
||||
import { deployableEnvironmentsQueryAtom, deploymentOptionsQueryAtom, deploymentOptionsReadyAtom } from './queries'
|
||||
import { deployableEnvironmentsDataAtom, deploymentOptionsDataAtom, deploymentOptionsReadyAtom } from './queries'
|
||||
import { submittedReleaseReadyAtom } from './release'
|
||||
import { dslContentAtom, sourceReady } from './source'
|
||||
import { envVarSelectionReady } from './utils'
|
||||
|
||||
export const deployableEnvironmentsAtom = atom((get) => {
|
||||
const deployableEnvironmentsQuery = get(deployableEnvironmentsQueryAtom)
|
||||
const deployableEnvironments = get(deployableEnvironmentsDataAtom)
|
||||
|
||||
return sourceReady(get)
|
||||
? deployableEnvironmentsQuery.data?.environments ?? []
|
||||
? deployableEnvironments?.environments ?? []
|
||||
: []
|
||||
})
|
||||
|
||||
const deployableEnvironmentsReadyAtom = atom((get) => {
|
||||
const deployableEnvironmentsQuery = get(deployableEnvironmentsQueryAtom)
|
||||
|
||||
return sourceReady(get) && deployableEnvironmentsQuery.isSuccess
|
||||
return sourceReady(get) && Boolean(get(deployableEnvironmentsDataAtom))
|
||||
})
|
||||
|
||||
export const effectiveSelectedEnvironmentIdAtom = atom((get) => {
|
||||
@ -35,10 +33,10 @@ export const effectiveSelectedEnvironmentIdAtom = atom((get) => {
|
||||
})
|
||||
|
||||
export const deploymentTargetBindingSlotsAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(deploymentOptionsQueryAtom)
|
||||
const deploymentOptions = get(deploymentOptionsDataAtom)
|
||||
|
||||
return sourceReady(get)
|
||||
? deploymentOptionsQuery.data?.options?.credentialSlots?.filter(slot => runtimeCredentialSlotKey(slot)) ?? []
|
||||
? deploymentOptions?.options?.credentialSlots?.filter(slot => runtimeCredentialSlotKey(slot)) ?? []
|
||||
: []
|
||||
})
|
||||
|
||||
@ -59,8 +57,8 @@ export const requiredBindingsReadyAtom = atom((get) => {
|
||||
|
||||
export const deploymentTargetEnvVarSlotsAtom = atom((get) => {
|
||||
const method = get(effectiveMethodAtom)
|
||||
const deploymentOptionsQuery = get(deploymentOptionsQueryAtom)
|
||||
const slots = sourceReady(get) ? deploymentOptionsQuery.data?.options?.envVarSlots : undefined
|
||||
const deploymentOptions = get(deploymentOptionsDataAtom)
|
||||
const slots = sourceReady(get) ? deploymentOptions?.options?.envVarSlots : undefined
|
||||
const dslContent = get(dslContentAtom)
|
||||
|
||||
// Deployment options own the canonical slot list; DSL metadata only enriches import-DSL defaults.
|
||||
|
||||
@ -5,12 +5,13 @@ import { SourceStepContent } from '../source-step'
|
||||
const mocks = vi.hoisted(() => {
|
||||
const sourceAppsQuery = {
|
||||
data: { pages: [{ data: [] }] },
|
||||
error: null,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
}
|
||||
|
||||
return {
|
||||
@ -46,6 +47,14 @@ vi.mock('@/features/deployments/create-guide/state/source', async () => {
|
||||
dslUnsupportedModeAtom: atom(false),
|
||||
effectiveSelectedAppAtom: atom(undefined),
|
||||
isReadingDslAtom: atom(false),
|
||||
sourceAppsAtom: atom(() => mocks.sourceAppsQuery.data.pages.flatMap(page => page.data)),
|
||||
sourceAppsErrorAtom: atom(() => mocks.sourceAppsQuery.error),
|
||||
sourceAppsFetchNextPageAtom: atom(() => mocks.sourceAppsQuery.fetchNextPage),
|
||||
sourceAppsHasNextPageAtom: atom(() => mocks.sourceAppsQuery.hasNextPage),
|
||||
sourceAppsIsFetchingAtom: atom(() => mocks.sourceAppsQuery.isFetching),
|
||||
sourceAppsIsFetchingNextPageAtom: atom(() => mocks.sourceAppsQuery.isFetchingNextPage),
|
||||
sourceAppsIsLoadingAtom: atom(() => mocks.sourceAppsQuery.isLoading),
|
||||
sourceAppsIsPlaceholderDataAtom: atom(() => mocks.sourceAppsQuery.isPlaceholderData),
|
||||
sourceAppsQueryAtom: atom(mocks.sourceAppsQuery),
|
||||
}
|
||||
})
|
||||
@ -80,12 +89,13 @@ describe('SourceStepContent', () => {
|
||||
vi.clearAllMocks()
|
||||
Object.assign(mocks.sourceAppsQuery, {
|
||||
data: { pages: [{ data: [] }] },
|
||||
error: null,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
isPlaceholderData: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
@ -114,7 +124,13 @@ describe('SourceStepContent', () => {
|
||||
render(<SourceStepContent />)
|
||||
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
fetchNextPage: expect.any(Function),
|
||||
hasNextPage: expect.any(Boolean),
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
|
||||
@ -21,7 +21,14 @@ import {
|
||||
dslUnsupportedModeAtom,
|
||||
effectiveSelectedAppAtom,
|
||||
isReadingDslAtom,
|
||||
sourceAppsQueryAtom,
|
||||
sourceAppsAtom,
|
||||
sourceAppsErrorAtom,
|
||||
sourceAppsFetchNextPageAtom,
|
||||
sourceAppsHasNextPageAtom,
|
||||
sourceAppsIsFetchingAtom,
|
||||
sourceAppsIsFetchingNextPageAtom,
|
||||
sourceAppsIsLoadingAtom,
|
||||
sourceAppsIsPlaceholderDataAtom,
|
||||
} from '@/features/deployments/create-guide/state/source'
|
||||
import {
|
||||
continueFromSourceAtom,
|
||||
@ -189,10 +196,23 @@ function SourceAppList() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const selectSourceApp = useSetAtom(selectSourceAppAtom)
|
||||
const effectiveSelectedApp = useAtomValue(effectiveSelectedAppAtom)
|
||||
const sourceAppsQuery = useAtomValue(sourceAppsQueryAtom)
|
||||
const sourceApps = (sourceAppsQuery.data?.pages.flatMap(page => page.data) ?? []) as WorkflowSourceApp[]
|
||||
const sourceAppsLoading = sourceAppsQuery.isLoading || sourceAppsQuery.isPlaceholderData || (sourceAppsQuery.isFetching && sourceApps.length === 0)
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(sourceAppsQuery, {
|
||||
const sourceApps = useAtomValue(sourceAppsAtom)
|
||||
const sourceAppsError = useAtomValue(sourceAppsErrorAtom)
|
||||
const sourceAppsFetchNextPage = useAtomValue(sourceAppsFetchNextPageAtom)
|
||||
const sourceAppsHasNextPage = useAtomValue(sourceAppsHasNextPageAtom)
|
||||
const sourceAppsIsFetching = useAtomValue(sourceAppsIsFetchingAtom)
|
||||
const sourceAppsIsFetchingNextPage = useAtomValue(sourceAppsIsFetchingNextPageAtom)
|
||||
const sourceAppsIsLoading = useAtomValue(sourceAppsIsLoadingAtom)
|
||||
const sourceAppsIsPlaceholderData = useAtomValue(sourceAppsIsPlaceholderDataAtom)
|
||||
const sourceAppsLoading = sourceAppsIsLoading || sourceAppsIsPlaceholderData || (sourceAppsIsFetching && sourceApps.length === 0)
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>({
|
||||
error: sourceAppsError,
|
||||
fetchNextPage: sourceAppsFetchNextPage,
|
||||
hasNextPage: sourceAppsHasNextPage,
|
||||
isFetching: sourceAppsIsFetching,
|
||||
isFetchingNextPage: sourceAppsIsFetchingNextPage,
|
||||
isLoading: sourceAppsIsLoading,
|
||||
}, {
|
||||
enabled: !sourceAppsLoading,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
@ -218,12 +238,12 @@ function SourceAppList() {
|
||||
onSelect={() => selectSourceApp(app)}
|
||||
/>
|
||||
))}
|
||||
{sourceAppsQuery.isFetchingNextPage && (
|
||||
{sourceAppsIsFetchingNextPage && (
|
||||
<div className="border-t border-divider-subtle px-3 py-2 text-center system-xs-regular text-text-tertiary">
|
||||
{t('createModal.loadingApps')}
|
||||
</div>
|
||||
)}
|
||||
{sourceAppsQuery.hasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
{sourceAppsHasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -16,8 +16,13 @@ import {
|
||||
stepAtom,
|
||||
} from '@/features/deployments/create-guide/state/primitives'
|
||||
import {
|
||||
deployableEnvironmentsQueryAtom,
|
||||
deploymentOptionsQueryAtom,
|
||||
deployableEnvironmentsIsErrorAtom,
|
||||
deployableEnvironmentsIsFetchingAtom,
|
||||
deployableEnvironmentsIsLoadingAtom,
|
||||
deploymentOptionsDataAtom,
|
||||
deploymentOptionsIsErrorAtom,
|
||||
deploymentOptionsIsFetchingAtom,
|
||||
deploymentOptionsIsLoadingAtom,
|
||||
unsupportedDslNodesAtom,
|
||||
} from '@/features/deployments/create-guide/state/queries'
|
||||
import {
|
||||
@ -72,11 +77,12 @@ export function TargetStepContent() {
|
||||
|
||||
function TargetEnvironmentSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const environmentsQuery = useAtomValue(deployableEnvironmentsQueryAtom)
|
||||
const environmentsIsError = useAtomValue(deployableEnvironmentsIsErrorAtom)
|
||||
const environmentsIsFetching = useAtomValue(deployableEnvironmentsIsFetchingAtom)
|
||||
const environmentsIsLoading = useAtomValue(deployableEnvironmentsIsLoadingAtom)
|
||||
const environments = useAtomValue(deployableEnvironmentsAtom)
|
||||
const effectiveSelectedEnvironmentId = useAtomValue(effectiveSelectedEnvironmentIdAtom)
|
||||
const isEnvironmentError = environmentsQuery.isError
|
||||
const isEnvironmentLoading = environmentsQuery.isLoading || (environmentsQuery.isFetching && !environmentsQuery.data)
|
||||
const isEnvironmentLoading = environmentsIsLoading || (environmentsIsFetching && environments.length === 0)
|
||||
const selectEnvironment = useSetAtom(selectedEnvironmentIdAtom)
|
||||
const hasEnvironmentOptions = environments.length > 0
|
||||
|
||||
@ -102,7 +108,7 @@ function TargetEnvironmentSection() {
|
||||
? <TargetEnvironmentSkeleton />
|
||||
: (
|
||||
<div className="rounded-lg border border-divider-subtle bg-background-default-subtle px-3 py-3 system-sm-regular text-text-quaternary">
|
||||
{isEnvironmentError
|
||||
{environmentsIsError
|
||||
? t('createGuide.target.loadEnvironmentsFailed')
|
||||
: t('createGuide.target.noEnvironmentOptions')}
|
||||
</div>
|
||||
@ -144,11 +150,14 @@ function EnvironmentOptionRow({ environment }: {
|
||||
|
||||
function TargetBindingSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const deploymentOptionsQuery = useAtomValue(deploymentOptionsQueryAtom)
|
||||
const deploymentOptions = useAtomValue(deploymentOptionsDataAtom)
|
||||
const deploymentOptionsIsError = useAtomValue(deploymentOptionsIsErrorAtom)
|
||||
const deploymentOptionsIsFetching = useAtomValue(deploymentOptionsIsFetchingAtom)
|
||||
const deploymentOptionsIsLoading = useAtomValue(deploymentOptionsIsLoadingAtom)
|
||||
const bindingSlots = useAtomValue(deploymentTargetBindingSlotsAtom)
|
||||
const bindingSelections = useAtomValue(deploymentTargetBindingSelectionsAtom)
|
||||
const isBindingError = deploymentOptionsQuery.isError
|
||||
const isBindingLoading = deploymentOptionsQuery.isLoading || (deploymentOptionsQuery.isFetching && !deploymentOptionsQuery.data)
|
||||
const isBindingError = deploymentOptionsIsError
|
||||
const isBindingLoading = deploymentOptionsIsLoading || (deploymentOptionsIsFetching && !deploymentOptions)
|
||||
const selectBinding = useSetAtom(selectBindingAtom)
|
||||
const unsupportedDslNodes = useAtomValue(unsupportedDslNodesAtom)
|
||||
const shouldRender = !(isBindingError && unsupportedDslNodes.length > 0)
|
||||
@ -196,10 +205,13 @@ function TargetEnvVarSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const setEnvVar = useSetAtom(setEnvVarAtom)
|
||||
const envVarValues = useAtomValue(envVarValuesAtom)
|
||||
const deploymentOptionsQuery = useAtomValue(deploymentOptionsQueryAtom)
|
||||
const deploymentOptions = useAtomValue(deploymentOptionsDataAtom)
|
||||
const deploymentOptionsIsError = useAtomValue(deploymentOptionsIsErrorAtom)
|
||||
const deploymentOptionsIsFetching = useAtomValue(deploymentOptionsIsFetchingAtom)
|
||||
const deploymentOptionsIsLoading = useAtomValue(deploymentOptionsIsLoadingAtom)
|
||||
const envVarSlots = useAtomValue(deploymentTargetEnvVarSlotsAtom)
|
||||
const isBindingError = deploymentOptionsQuery.isError
|
||||
const isBindingLoading = deploymentOptionsQuery.isLoading || (deploymentOptionsQuery.isFetching && !deploymentOptionsQuery.data)
|
||||
const isBindingError = deploymentOptionsIsError
|
||||
const isBindingLoading = deploymentOptionsIsLoading || (deploymentOptionsIsFetching && !deploymentOptions)
|
||||
|
||||
if (isBindingLoading || isBindingError)
|
||||
return null
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
atomWithQuery,
|
||||
queryClientAtom,
|
||||
} from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import * as z from 'zod'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { normalizeAppPagination } from '@/service/use-apps'
|
||||
@ -270,6 +271,18 @@ export const createReleaseSourceAppsQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
const createReleaseSourceAppsDataAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.data)
|
||||
export const createReleaseSourceAppsErrorAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.error)
|
||||
export const createReleaseSourceAppsFetchNextPageAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.fetchNextPage)
|
||||
export const createReleaseSourceAppsHasNextPageAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.hasNextPage)
|
||||
export const createReleaseSourceAppsIsFetchingAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.isFetching)
|
||||
export const createReleaseSourceAppsIsFetchingNextPageAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.isFetchingNextPage)
|
||||
export const createReleaseSourceAppsIsLoadingAtom = selectAtom(createReleaseSourceAppsQueryAtom, query => query.isLoading)
|
||||
|
||||
export const createReleaseSourceAppsAtom = atom((get) => {
|
||||
return get(createReleaseSourceAppsDataAtom)?.pages.flatMap(page => page.data) ?? []
|
||||
})
|
||||
|
||||
export const createReleaseDslContentAtom = atom((get) => {
|
||||
return get(createReleaseDslFileContentQueryAtom).data ?? ''
|
||||
})
|
||||
|
||||
@ -35,6 +35,13 @@ vi.mock('@/features/deployments/create-release/state', async () => {
|
||||
const { atom } = await import('jotai')
|
||||
|
||||
return {
|
||||
createReleaseSourceAppsAtom: atom(() => mocks.sourceAppsQuery.data.pages.flatMap(page => page.data)),
|
||||
createReleaseSourceAppsErrorAtom: atom(() => mocks.sourceAppsQuery.error),
|
||||
createReleaseSourceAppsFetchNextPageAtom: atom(() => mocks.sourceAppsQuery.fetchNextPage),
|
||||
createReleaseSourceAppsHasNextPageAtom: atom(() => mocks.sourceAppsQuery.hasNextPage),
|
||||
createReleaseSourceAppsIsFetchingAtom: atom(() => mocks.sourceAppsQuery.isFetching),
|
||||
createReleaseSourceAppsIsFetchingNextPageAtom: atom(() => mocks.sourceAppsQuery.isFetchingNextPage),
|
||||
createReleaseSourceAppsIsLoadingAtom: atom(() => mocks.sourceAppsQuery.isLoading),
|
||||
createReleaseSourceAppSearchTextAtom: atom(''),
|
||||
createReleaseSourceAppsQueryAtom: atom(mocks.sourceAppsQuery),
|
||||
}
|
||||
@ -98,7 +105,13 @@ describe('SourceAppPicker', () => {
|
||||
renderSourceAppPicker(false)
|
||||
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
fetchNextPage: expect.any(Function),
|
||||
hasNextPage: true,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
@ -110,7 +123,13 @@ describe('SourceAppPicker', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.useInfiniteScroll).toHaveBeenLastCalledWith(
|
||||
mocks.sourceAppsQuery,
|
||||
expect.objectContaining({
|
||||
fetchNextPage: expect.any(Function),
|
||||
hasNextPage: true,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
|
||||
@ -21,8 +21,14 @@ import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import { useInfiniteScroll } from '@/features/deployments/shared/hooks/use-infinite-scroll'
|
||||
import { TitleTooltip } from '../../shared/components/title-tooltip'
|
||||
import {
|
||||
createReleaseSourceAppsAtom,
|
||||
createReleaseSourceAppSearchTextAtom,
|
||||
createReleaseSourceAppsQueryAtom,
|
||||
createReleaseSourceAppsErrorAtom,
|
||||
createReleaseSourceAppsFetchNextPageAtom,
|
||||
createReleaseSourceAppsHasNextPageAtom,
|
||||
createReleaseSourceAppsIsFetchingAtom,
|
||||
createReleaseSourceAppsIsFetchingNextPageAtom,
|
||||
createReleaseSourceAppsIsLoadingAtom,
|
||||
} from '../state'
|
||||
|
||||
const SOURCE_APP_PICKER_SKELETON_KEYS = ['first-source-app', 'second-source-app', 'third-source-app']
|
||||
@ -134,21 +140,26 @@ export function SourceAppPicker({ value, onChange, disabled = false }: {
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
const searchText = useAtomValue(createReleaseSourceAppSearchTextAtom)
|
||||
const setSearchText = useSetAtom(createReleaseSourceAppSearchTextAtom)
|
||||
const sourceAppsQuery = useAtomValue(createReleaseSourceAppsQueryAtom)
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
} = sourceAppsQuery
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(sourceAppsQuery, {
|
||||
const apps = useAtomValue(createReleaseSourceAppsAtom)
|
||||
const sourceAppsError = useAtomValue(createReleaseSourceAppsErrorAtom)
|
||||
const sourceAppsFetchNextPage = useAtomValue(createReleaseSourceAppsFetchNextPageAtom)
|
||||
const sourceAppsHasNextPage = useAtomValue(createReleaseSourceAppsHasNextPageAtom)
|
||||
const sourceAppsIsFetching = useAtomValue(createReleaseSourceAppsIsFetchingAtom)
|
||||
const sourceAppsIsFetchingNextPage = useAtomValue(createReleaseSourceAppsIsFetchingNextPageAtom)
|
||||
const sourceAppsIsLoading = useAtomValue(createReleaseSourceAppsIsLoadingAtom)
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>({
|
||||
error: sourceAppsError,
|
||||
fetchNextPage: sourceAppsFetchNextPage,
|
||||
hasNextPage: sourceAppsHasNextPage,
|
||||
isFetching: sourceAppsIsFetching,
|
||||
isFetchingNextPage: sourceAppsIsFetchingNextPage,
|
||||
isLoading: sourceAppsIsLoading,
|
||||
}, {
|
||||
enabled: isShow && !disabled,
|
||||
rootMargin: '0px 0px 160px 0px',
|
||||
threshold: 0.1,
|
||||
})
|
||||
|
||||
const apps = data?.pages.flatMap(page => page.data) ?? []
|
||||
|
||||
return (
|
||||
<Combobox<App>
|
||||
items={apps}
|
||||
@ -208,23 +219,23 @@ export function SourceAppPicker({ value, onChange, disabled = false }: {
|
||||
</ComboboxInputGroup>
|
||||
</div>
|
||||
<div ref={rootRef} className="min-h-0 flex-1 overflow-y-auto p-1">
|
||||
{(isLoading || isFetchingNextPage) && apps.length === 0 && <SourceAppPickerSkeleton />}
|
||||
{(sourceAppsIsLoading || sourceAppsIsFetchingNextPage) && apps.length === 0 && <SourceAppPickerSkeleton />}
|
||||
<ComboboxList className="max-h-none p-0">
|
||||
{(app: App) => (
|
||||
<SourceAppOption key={app.id} app={app} />
|
||||
)}
|
||||
</ComboboxList>
|
||||
{!(isLoading || isFetchingNextPage) && (
|
||||
{!(sourceAppsIsLoading || sourceAppsIsFetchingNextPage) && (
|
||||
<ComboboxEmpty>
|
||||
{t('createModal.appSearchEmpty')}
|
||||
</ComboboxEmpty>
|
||||
)}
|
||||
{isFetchingNextPage && apps.length > 0 && (
|
||||
{sourceAppsIsFetchingNextPage && apps.length > 0 && (
|
||||
<div className="px-3 py-2 text-center system-xs-regular text-text-tertiary">
|
||||
{t('createModal.loadingApps')}
|
||||
</div>
|
||||
)}
|
||||
{hasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
{sourceAppsHasNextPage && <div ref={sentinelRef} aria-hidden="true" className="h-px" />}
|
||||
</div>
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
|
||||
@ -18,6 +18,7 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { envVarBindingSlotFromContract } from '../../shared/components/env-var-bindings-utils'
|
||||
import {
|
||||
@ -81,6 +82,10 @@ export const releaseDeploymentViewQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const releaseDeploymentViewAtom = selectAtom(releaseDeploymentViewQueryAtom, query => query.data)
|
||||
export const releaseDeploymentViewIsLoadingAtom = selectAtom(releaseDeploymentViewQueryAtom, query => query.isLoading)
|
||||
export const releaseDeploymentViewIsErrorAtom = selectAtom(releaseDeploymentViewQueryAtom, query => query.isError)
|
||||
|
||||
const selectedEnvIdAtom = atom<string | undefined>(undefined)
|
||||
const selectedReleaseIdAtom = atom<string | undefined>(undefined)
|
||||
const manualBindingsAtom = atom<RuntimeCredentialBindingSelections>({})
|
||||
@ -223,44 +228,47 @@ const releaseDeploymentOptionsQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deployBindingSlotsAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(releaseDeploymentOptionsQueryAtom)
|
||||
const releaseDeploymentOptionsAtom = selectAtom(releaseDeploymentOptionsQueryAtom, query => query.data)
|
||||
const releaseDeploymentOptionsIsLoadingAtom = selectAtom(releaseDeploymentOptionsQueryAtom, query => query.isLoading)
|
||||
const releaseDeploymentOptionsIsFetchingAtom = selectAtom(releaseDeploymentOptionsQueryAtom, query => query.isFetching)
|
||||
const releaseDeploymentOptionsIsErrorAtom = selectAtom(releaseDeploymentOptionsQueryAtom, query => query.isError)
|
||||
|
||||
return deploymentOptionsQuery.data?.options.credentialSlots.filter(slot => runtimeCredentialSlotKey(slot)) ?? []
|
||||
export const deployBindingSlotsAtom = atom((get) => {
|
||||
const deploymentOptions = get(releaseDeploymentOptionsAtom)
|
||||
|
||||
return deploymentOptions?.options.credentialSlots.filter(slot => runtimeCredentialSlotKey(slot)) ?? []
|
||||
})
|
||||
|
||||
export const deployEnvVarSlotsAtom = atom((get): EnvVarBindingSlot[] => {
|
||||
const deploymentOptionsQuery = get(releaseDeploymentOptionsQueryAtom)
|
||||
const deploymentOptions = get(releaseDeploymentOptionsAtom)
|
||||
|
||||
return deploymentOptionsQuery.data?.options.envVarSlots.flatMap((slot): EnvVarBindingSlot[] => {
|
||||
return deploymentOptions?.options.envVarSlots.flatMap((slot): EnvVarBindingSlot[] => {
|
||||
const bindingSlot = envVarBindingSlotFromContract(slot)
|
||||
return bindingSlot ? [bindingSlot] : []
|
||||
}) ?? []
|
||||
})
|
||||
|
||||
export const deployIsBindingOptionsLoadingAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(releaseDeploymentOptionsQueryAtom)
|
||||
const releaseId = get(deployTargetReleaseIdAtom)
|
||||
|
||||
return Boolean(
|
||||
releaseId
|
||||
&& get(deployHasSelectedEnvironmentAtom)
|
||||
&& (deploymentOptionsQuery.isLoading || deploymentOptionsQuery.isFetching),
|
||||
&& (get(releaseDeploymentOptionsIsLoadingAtom) || get(releaseDeploymentOptionsIsFetchingAtom)),
|
||||
)
|
||||
})
|
||||
|
||||
export const deployHasBindingOptionsErrorAtom = atom((get) => {
|
||||
return get(releaseDeploymentOptionsQueryAtom).isError
|
||||
return get(releaseDeploymentOptionsIsErrorAtom)
|
||||
})
|
||||
|
||||
const deployIsBindingOptionsReadyAtom = atom((get) => {
|
||||
const deploymentOptionsQuery = get(releaseDeploymentOptionsQueryAtom)
|
||||
const releaseId = get(deployTargetReleaseIdAtom)
|
||||
|
||||
return Boolean(
|
||||
releaseId
|
||||
&& get(deployHasSelectedEnvironmentAtom)
|
||||
&& deploymentOptionsQuery.data
|
||||
&& get(releaseDeploymentOptionsAtom)
|
||||
&& !get(deployIsBindingOptionsLoadingAtom)
|
||||
&& !get(deployHasBindingOptionsErrorAtom),
|
||||
)
|
||||
|
||||
@ -11,7 +11,7 @@ import { ScopeProvider } from 'jotai-scope'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EnvVarBindingsPanel } from '../../shared/components/env-var-bindings'
|
||||
import { isAvailableDeploymentTarget } from '../../shared/domain/runtime-status'
|
||||
import { canAttemptDeployAtom, canSubmitDeployAtom, closeDeployDrawerAtom, deployBindingSlotsAtom, deployEnvVarSlotsAtom, deployEnvVarValuesAtom, deployFormAppInstanceIdAtom, deployHasBindingOptionsErrorAtom, deployHasSelectedEnvironmentAtom, deployIsBindingOptionsLoadingAtom, deployReadyFormConfigAtom, deployReadyFormLocalAtoms, deployReleaseSubmissionAtom, deploySelectedBindingsAtom, deployShowValidationErrorsAtom, deployTargetReleaseIdAtom, isDeployReleaseSubmittingAtom, releaseDeploymentViewQueryAtom, selectDeployBindingAtom, setDeployEnvVarAtom, showDeployValidationErrorsAtom } from '../state'
|
||||
import { canAttemptDeployAtom, canSubmitDeployAtom, closeDeployDrawerAtom, deployBindingSlotsAtom, deployEnvVarSlotsAtom, deployEnvVarValuesAtom, deployFormAppInstanceIdAtom, deployHasBindingOptionsErrorAtom, deployHasSelectedEnvironmentAtom, deployIsBindingOptionsLoadingAtom, deployReadyFormConfigAtom, deployReadyFormLocalAtoms, deployReleaseSubmissionAtom, deploySelectedBindingsAtom, deployShowValidationErrorsAtom, deployTargetReleaseIdAtom, isDeployReleaseSubmittingAtom, releaseDeploymentViewAtom, releaseDeploymentViewIsErrorAtom, releaseDeploymentViewIsLoadingAtom, selectDeployBindingAtom, setDeployEnvVarAtom, showDeployValidationErrorsAtom } from '../state'
|
||||
import {
|
||||
currentReleaseIdForEnvironment,
|
||||
selectableDeployReleases,
|
||||
@ -203,13 +203,15 @@ function DeployFormContent({
|
||||
presetReleaseId,
|
||||
}: DeployFormProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const releaseDeploymentViewQuery = useAtomValue(releaseDeploymentViewQueryAtom)
|
||||
const deploymentView = useAtomValue(releaseDeploymentViewAtom)
|
||||
const isLoading = useAtomValue(releaseDeploymentViewIsLoadingAtom)
|
||||
const isError = useAtomValue(releaseDeploymentViewIsErrorAtom)
|
||||
|
||||
if (releaseDeploymentViewQuery.isLoading) {
|
||||
if (isLoading) {
|
||||
return <DeployFormSkeleton />
|
||||
}
|
||||
|
||||
if (releaseDeploymentViewQuery.isError) {
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-4 system-sm-regular text-text-destructive">
|
||||
{t('common.loadFailed')}
|
||||
@ -217,7 +219,6 @@ function DeployFormContent({
|
||||
)
|
||||
}
|
||||
|
||||
const deploymentView = releaseDeploymentViewQuery.data
|
||||
if (!deploymentView) {
|
||||
return (
|
||||
<div className="p-4 system-sm-regular text-text-destructive">
|
||||
|
||||
@ -2,7 +2,11 @@ import type { AccessChannels, AccessEndpoint } from '@dify/contracts/enterprise/
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../../../route-state'
|
||||
import { accessSettingsQueryAtom } from '../../state'
|
||||
import {
|
||||
accessSettingsAtom,
|
||||
accessSettingsIsErrorAtom,
|
||||
accessSettingsIsLoadingAtom,
|
||||
} from '../../state'
|
||||
import { AccessChannelsSection } from '../section'
|
||||
|
||||
const mockToggleAccessChannel = vi.hoisted(() => vi.fn())
|
||||
@ -68,17 +72,17 @@ describe('AccessChannelsSection', () => {
|
||||
mockUseAtomValue.mockImplementation((atom) => {
|
||||
if (atom === deploymentRouteAppInstanceIdAtom)
|
||||
return 'app-instance-1'
|
||||
if (atom === accessSettingsQueryAtom) {
|
||||
if (atom === accessSettingsAtom) {
|
||||
return {
|
||||
data: {
|
||||
accessChannels: createAccessChannels(),
|
||||
webAppEndpoints: [createEndpoint('https://app.example.com/webapp')],
|
||||
cliEndpoint: createEndpoint('https://cli.example.com/entry'),
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
accessChannels: createAccessChannels(),
|
||||
webAppEndpoints: [createEndpoint('https://app.example.com/webapp')],
|
||||
cliEndpoint: createEndpoint('https://cli.example.com/entry'),
|
||||
}
|
||||
}
|
||||
if (atom === accessSettingsIsLoadingAtom)
|
||||
return false
|
||||
if (atom === accessSettingsIsErrorAtom)
|
||||
return false
|
||||
return undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -12,7 +12,11 @@ import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
|
||||
import { DeploymentEmptyState, DeploymentNoticeState, DeploymentStateMessage } from '../../../shared/components/empty-state'
|
||||
import { CopyPill, EndpointRow } from '../../../shared/components/endpoint'
|
||||
import { Section } from '../../../shared/components/section'
|
||||
import { accessSettingsQueryAtom } from '../state'
|
||||
import {
|
||||
accessSettingsAtom,
|
||||
accessSettingsIsErrorAtom,
|
||||
accessSettingsIsLoadingAtom,
|
||||
} from '../state'
|
||||
import { getUrlOrigin } from './url'
|
||||
|
||||
const ACCESS_CHANNEL_SKELETON_SECTIONS = [
|
||||
@ -115,12 +119,12 @@ function ChannelRow({ info, children }: {
|
||||
export function AccessChannelsSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const accessSettingsQuery = useAtomValue(accessSettingsQueryAtom)
|
||||
const accessChannels = accessSettingsQuery.data?.accessChannels
|
||||
const webAppEndpoints: AccessEndpoint[] | undefined = accessSettingsQuery.data?.webAppEndpoints
|
||||
const cliEndpoint: AccessEndpoint | undefined = accessSettingsQuery.data?.cliEndpoint
|
||||
const isLoading = accessSettingsQuery.isLoading
|
||||
const isError = accessSettingsQuery.isError
|
||||
const accessSettings = useAtomValue(accessSettingsAtom)
|
||||
const isLoading = useAtomValue(accessSettingsIsLoadingAtom)
|
||||
const isError = useAtomValue(accessSettingsIsErrorAtom)
|
||||
const accessChannels = accessSettings?.accessChannels
|
||||
const webAppEndpoints: AccessEndpoint[] | undefined = accessSettings?.webAppEndpoints
|
||||
const cliEndpoint: AccessEndpoint | undefined = accessSettings?.cliEndpoint
|
||||
const runEnabled = accessChannels?.webAppEnabled ?? false
|
||||
const webappRows = webAppEndpoints?.flatMap((endpoint) => {
|
||||
const endpointUrl = endpoint.endpointUrl
|
||||
|
||||
@ -5,7 +5,11 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../../../route-state'
|
||||
import { accessSettingsQueryAtom } from '../../state'
|
||||
import {
|
||||
accessSettingsAtom,
|
||||
accessSettingsIsErrorAtom,
|
||||
accessSettingsIsLoadingAtom,
|
||||
} from '../../state'
|
||||
import { EnvironmentPermissionRow } from '../environment-permission-row'
|
||||
import { AccessPermissionsSection } from '../section'
|
||||
|
||||
@ -236,15 +240,15 @@ describe('AccessPermissionsSection', () => {
|
||||
mockUseAtomValue.mockImplementation((atom) => {
|
||||
if (atom === deploymentRouteAppInstanceIdAtom)
|
||||
return 'app-instance-1'
|
||||
if (atom === accessSettingsQueryAtom) {
|
||||
if (atom === accessSettingsAtom) {
|
||||
return {
|
||||
data: {
|
||||
environmentPolicies: [createEnvironmentAccessPolicy()],
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
environmentPolicies: [createEnvironmentAccessPolicy()],
|
||||
}
|
||||
}
|
||||
if (atom === accessSettingsIsLoadingAtom)
|
||||
return false
|
||||
if (atom === accessSettingsIsErrorAtom)
|
||||
return false
|
||||
return undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,7 +7,11 @@ import { SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
|
||||
import { DeploymentEmptyState, DeploymentStateMessage } from '../../../shared/components/empty-state'
|
||||
import { Section } from '../../../shared/components/section'
|
||||
import { accessSettingsQueryAtom } from '../state'
|
||||
import {
|
||||
accessSettingsAtom,
|
||||
accessSettingsIsErrorAtom,
|
||||
accessSettingsIsLoadingAtom,
|
||||
} from '../state'
|
||||
import { EnvironmentPermissionRow } from './environment-permission-row'
|
||||
|
||||
const ACCESS_PERMISSIONS_SKELETON_KEYS = ['production', 'staging', 'development']
|
||||
@ -28,10 +32,10 @@ function AccessPermissionsSkeleton() {
|
||||
export function AccessPermissionsSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const accessSettingsQuery = useAtomValue(accessSettingsQueryAtom)
|
||||
const environmentPolicies: EnvironmentAccessPolicy[] | undefined = accessSettingsQuery.data?.environmentPolicies
|
||||
const isLoading = accessSettingsQuery.isLoading
|
||||
const isError = accessSettingsQuery.isError
|
||||
const accessSettings = useAtomValue(accessSettingsAtom)
|
||||
const isLoading = useAtomValue(accessSettingsIsLoadingAtom)
|
||||
const isError = useAtomValue(accessSettingsIsErrorAtom)
|
||||
const environmentPolicies: EnvironmentAccessPolicy[] | undefined = accessSettings?.environmentPolicies
|
||||
const policyRows = environmentPolicies ?? []
|
||||
|
||||
return (
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
|
||||
@ -17,3 +18,7 @@ export const accessSettingsQueryAtom = atomWithQuery((get) => {
|
||||
enabled: Boolean(appInstanceId),
|
||||
})
|
||||
})
|
||||
|
||||
export const accessSettingsAtom = selectAtom(accessSettingsQueryAtom, query => query.data)
|
||||
export const accessSettingsIsLoadingAtom = selectAtom(accessSettingsQueryAtom, query => query.isLoading)
|
||||
export const accessSettingsIsErrorAtom = selectAtom(accessSettingsQueryAtom, query => query.isError)
|
||||
|
||||
@ -16,7 +16,11 @@ import { ApiKeyGenerateMenu } from '../api-keys/api-key-generate-menu'
|
||||
import { ApiKeyList } from '../api-keys/api-key-list'
|
||||
import { CreatedApiTokenDialog } from '../api-keys/created-token-dialog'
|
||||
import { DeveloperApiDocsDrawer } from '../docs/docs-drawer'
|
||||
import { developerApiSettingsQueryAtom } from '../state'
|
||||
import {
|
||||
developerApiSettingsAtom,
|
||||
developerApiSettingsIsErrorAtom,
|
||||
developerApiSettingsIsLoadingAtom,
|
||||
} from '../state'
|
||||
import { DeveloperApiSkeleton } from './skeleton'
|
||||
|
||||
type CreatedApiToken = {
|
||||
@ -86,21 +90,23 @@ export function DeveloperApiSection() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const [createdApiToken, setCreatedApiToken] = useState<CreatedApiToken>()
|
||||
const developerApiSettingsQuery = useAtomValue(developerApiSettingsQueryAtom)
|
||||
const accessChannels = developerApiSettingsQuery.data?.accessChannels
|
||||
const developerApiSettings = useAtomValue(developerApiSettingsAtom)
|
||||
const isLoading = useAtomValue(developerApiSettingsIsLoadingAtom)
|
||||
const isError = useAtomValue(developerApiSettingsIsErrorAtom)
|
||||
const accessChannels = developerApiSettings?.accessChannels
|
||||
const apiEnabled = accessChannels?.developerApiEnabled ?? false
|
||||
const apiUrl = developerApiSettingsQuery.data?.developerApiUrl.apiUrl
|
||||
const apiKeys: ApiKey[] = developerApiSettingsQuery.data?.apiKeys ?? []
|
||||
const environments = developerApiSettingsQuery.data?.environments ?? []
|
||||
const apiUrl = developerApiSettings?.developerApiUrl.apiUrl
|
||||
const apiKeys: ApiKey[] = developerApiSettings?.apiKeys ?? []
|
||||
const environments = developerApiSettings?.environments ?? []
|
||||
const visibleCreatedApiToken = createdApiToken && createdApiToken.appInstanceId === appInstanceId
|
||||
? createdApiToken.token
|
||||
: undefined
|
||||
const hasSelectableEnvironment = environments.some(environment => Boolean(environment.id))
|
||||
|
||||
if (developerApiSettingsQuery.isLoading)
|
||||
if (isLoading)
|
||||
return <DeveloperApiSkeleton />
|
||||
|
||||
if (developerApiSettingsQuery.isError || !appInstanceId)
|
||||
if (isError || !appInstanceId)
|
||||
return <DeploymentStateMessage variant="section">{t('common.loadFailed')}</DeploymentStateMessage>
|
||||
|
||||
if (!apiEnabled) {
|
||||
|
||||
@ -7,7 +7,11 @@ import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { developerApiSettingsQueryAtom } from './state'
|
||||
import {
|
||||
developerApiSettingsAtom,
|
||||
developerApiSettingsIsErrorAtom,
|
||||
developerApiSettingsIsLoadingAtom,
|
||||
} from './state'
|
||||
|
||||
function DeveloperApiSwitch({ checked, accessChannels, disabled }: {
|
||||
checked: boolean
|
||||
@ -43,11 +47,13 @@ function DeveloperApiSwitch({ checked, accessChannels, disabled }: {
|
||||
|
||||
export function DeveloperApiHeaderSwitch() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const developerApiSettingsQuery = useAtomValue(developerApiSettingsQueryAtom)
|
||||
const accessChannels = developerApiSettingsQuery.data?.accessChannels
|
||||
const developerApiSettings = useAtomValue(developerApiSettingsAtom)
|
||||
const isLoading = useAtomValue(developerApiSettingsIsLoadingAtom)
|
||||
const isError = useAtomValue(developerApiSettingsIsErrorAtom)
|
||||
const accessChannels = developerApiSettings?.accessChannels
|
||||
const apiEnabled = accessChannels?.developerApiEnabled ?? false
|
||||
|
||||
if (developerApiSettingsQuery.isLoading)
|
||||
if (isLoading)
|
||||
return <SwitchSkeleton />
|
||||
|
||||
return (
|
||||
@ -58,7 +64,7 @@ export function DeveloperApiHeaderSwitch() {
|
||||
<DeveloperApiSwitch
|
||||
checked={apiEnabled}
|
||||
accessChannels={accessChannels}
|
||||
disabled={developerApiSettingsQuery.isError}
|
||||
disabled={isError}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
|
||||
@ -17,3 +18,7 @@ export const developerApiSettingsQueryAtom = atomWithQuery((get) => {
|
||||
enabled: Boolean(appInstanceId),
|
||||
})
|
||||
})
|
||||
|
||||
export const developerApiSettingsAtom = selectAtom(developerApiSettingsQueryAtom, query => query.data)
|
||||
export const developerApiSettingsIsLoadingAtom = selectAtom(developerApiSettingsQueryAtom, query => query.isLoading)
|
||||
export const developerApiSettingsIsErrorAtom = selectAtom(developerApiSettingsQueryAtom, query => query.isError)
|
||||
|
||||
@ -20,7 +20,11 @@ import { usePathname } from '@/next/navigation'
|
||||
import { DeploymentActionsMenu } from '../deployment-actions'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../route-state'
|
||||
import { TitleTooltip } from '../shared/components/title-tooltip'
|
||||
import { deploymentDetailAppInstanceQueryAtom } from './state'
|
||||
import {
|
||||
deploymentDetailAppInstanceAtom,
|
||||
deploymentDetailAppInstanceIsErrorAtom,
|
||||
deploymentDetailAppInstanceIsLoadingAtom,
|
||||
} from './state'
|
||||
|
||||
type TabDef = {
|
||||
key: InstanceDetailTabKey
|
||||
@ -95,10 +99,12 @@ function DeploymentDetailInstanceInfo({ appInstanceId, expand }: {
|
||||
expand: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const overviewQuery = useAtomValue(deploymentDetailAppInstanceQueryAtom)
|
||||
const app = overviewQuery.data?.appInstance
|
||||
const isLoading = !app && overviewQuery.isLoading
|
||||
const isUnavailable = !app || overviewQuery.isError
|
||||
const overview = useAtomValue(deploymentDetailAppInstanceAtom)
|
||||
const isOverviewLoading = useAtomValue(deploymentDetailAppInstanceIsLoadingAtom)
|
||||
const isOverviewError = useAtomValue(deploymentDetailAppInstanceIsErrorAtom)
|
||||
const app = overview?.appInstance
|
||||
const isLoading = !app && isOverviewLoading
|
||||
const isUnavailable = !app || isOverviewError
|
||||
const instanceName = app ? app.displayName : appInstanceId
|
||||
|
||||
return (
|
||||
|
||||
@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { openDeployDrawerAtom } from '../../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
|
||||
import {
|
||||
deploymentEnvironmentDeploymentsQueryAtom,
|
||||
deploymentEnvironmentDeploymentsIsErrorAtom,
|
||||
deploymentEnvironmentDeploymentsIsLoadingAtom,
|
||||
deploymentRuntimeInstanceRowsAtom,
|
||||
} from '../../state'
|
||||
|
||||
@ -34,10 +35,11 @@ export function NewDeploymentButton() {
|
||||
}
|
||||
|
||||
export function NewDeploymentHeaderAction() {
|
||||
const environmentDeploymentsQuery = useAtomValue(deploymentEnvironmentDeploymentsQueryAtom)
|
||||
const isLoading = useAtomValue(deploymentEnvironmentDeploymentsIsLoadingAtom)
|
||||
const hasError = useAtomValue(deploymentEnvironmentDeploymentsIsErrorAtom)
|
||||
const rows = useAtomValue(deploymentRuntimeInstanceRowsAtom)
|
||||
|
||||
if (environmentDeploymentsQuery.isLoading || environmentDeploymentsQuery.isError || rows.length === 0)
|
||||
if (isLoading || hasError || rows.length === 0)
|
||||
return null
|
||||
|
||||
return <NewDeploymentButton />
|
||||
|
||||
@ -14,7 +14,8 @@ import {
|
||||
} from '../../shared/components/detail-table'
|
||||
import { DeploymentEmptyState, DeploymentStateMessage } from '../../shared/components/empty-state'
|
||||
import {
|
||||
deploymentEnvironmentDeploymentsQueryAtom,
|
||||
deploymentEnvironmentDeploymentsIsErrorAtom,
|
||||
deploymentEnvironmentDeploymentsIsLoadingAtom,
|
||||
deploymentRuntimeInstanceRowsAtom,
|
||||
} from '../state'
|
||||
import { DeploymentEnvironmentList } from './environment-list/deployment-environment-list'
|
||||
@ -89,10 +90,9 @@ function DeploymentEnvironmentListSkeleton() {
|
||||
|
||||
export function DeploymentInstances() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const environmentDeploymentsQuery = useAtomValue(deploymentEnvironmentDeploymentsQueryAtom)
|
||||
const isLoading = useAtomValue(deploymentEnvironmentDeploymentsIsLoadingAtom)
|
||||
const hasError = useAtomValue(deploymentEnvironmentDeploymentsIsErrorAtom)
|
||||
const rows = useAtomValue(deploymentRuntimeInstanceRowsAtom)
|
||||
const isLoading = environmentDeploymentsQuery.isLoading
|
||||
const hasError = environmentDeploymentsQuery.isError
|
||||
|
||||
return (
|
||||
<div className="flex w-full min-w-0 flex-col gap-4 px-6 py-6">
|
||||
|
||||
@ -9,7 +9,11 @@ import { hasRuntimeInstanceDeployment } from '../../shared/domain/runtime-status
|
||||
import { AccessStatusSection, AccessStatusSectionSkeleton, ApiTokenSummarySection, ApiTokenSummarySectionSkeleton } from './access-summary/access-status-section'
|
||||
import { EnvironmentStrip, EnvironmentStripSkeleton } from './environment-status/environment-strip'
|
||||
import { ReleaseHero, ReleaseHeroSkeleton } from './release-summary/release-hero'
|
||||
import { deploymentOverviewQueryAtom } from './state'
|
||||
import {
|
||||
deploymentOverviewAtom,
|
||||
deploymentOverviewIsErrorAtom,
|
||||
deploymentOverviewIsLoadingAtom,
|
||||
} from './state'
|
||||
|
||||
function OverviewLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -63,13 +67,14 @@ function OverviewLoadingSkeleton() {
|
||||
|
||||
export function DeploymentOverview() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const overviewQuery = useAtomValue(deploymentOverviewQueryAtom)
|
||||
const overview = overviewQuery.data
|
||||
const overview = useAtomValue(deploymentOverviewAtom)
|
||||
const isLoading = useAtomValue(deploymentOverviewIsLoadingAtom)
|
||||
const isError = useAtomValue(deploymentOverviewIsErrorAtom)
|
||||
|
||||
if (overviewQuery.isLoading)
|
||||
if (isLoading)
|
||||
return <OverviewLoadingSkeleton />
|
||||
|
||||
if (overviewQuery.isError) {
|
||||
if (isError) {
|
||||
return (
|
||||
<OverviewLayout>
|
||||
<DeploymentStateMessage variant="section">{t('common.loadFailed')}</DeploymentStateMessage>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
|
||||
@ -17,3 +18,7 @@ export const deploymentOverviewQueryAtom = atomWithQuery((get) => {
|
||||
enabled: Boolean(appInstanceId),
|
||||
})
|
||||
})
|
||||
|
||||
export const deploymentOverviewAtom = selectAtom(deploymentOverviewQueryAtom, query => query.data)
|
||||
export const deploymentOverviewIsLoadingAtom = selectAtom(deploymentOverviewQueryAtom, query => query.isLoading)
|
||||
export const deploymentOverviewIsErrorAtom = selectAtom(deploymentOverviewQueryAtom, query => query.isError)
|
||||
|
||||
@ -41,8 +41,10 @@ vi.mock('../state', async (importOriginal) => {
|
||||
|
||||
return {
|
||||
...actual,
|
||||
deployReleaseMenuEnvironmentDeploymentsQueryAtom: atom(environmentDeploymentsErrorResult()),
|
||||
deployReleaseMenuAppInstanceQueryAtom: atom(appInstanceResult()),
|
||||
deployReleaseMenuEnvironmentDeploymentsAtom: atom(undefined),
|
||||
deployReleaseMenuEnvironmentDeploymentsIsErrorAtom: atom(true),
|
||||
deployReleaseMenuEnvironmentDeploymentsIsLoadingAtom: atom(false),
|
||||
deployReleaseMenuAppInstanceNameAtom: atom('Deployment 1'),
|
||||
}
|
||||
})
|
||||
|
||||
@ -82,24 +84,6 @@ function createRelease(): Release {
|
||||
}
|
||||
}
|
||||
|
||||
function environmentDeploymentsErrorResult() {
|
||||
return {
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function appInstanceResult() {
|
||||
return {
|
||||
data: {
|
||||
appInstance: {
|
||||
displayName: 'Deployment 1',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('DeployReleaseMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@ -28,8 +28,10 @@ import { EditReleaseDialog } from './edit-release-dialog'
|
||||
import { exportReleaseDsl } from './release-dsl-export'
|
||||
import {
|
||||
deleteReleaseDialogOpenAtom,
|
||||
deployReleaseMenuAppInstanceQueryAtom,
|
||||
deployReleaseMenuEnvironmentDeploymentsQueryAtom,
|
||||
deployReleaseMenuAppInstanceNameAtom,
|
||||
deployReleaseMenuEnvironmentDeploymentsAtom,
|
||||
deployReleaseMenuEnvironmentDeploymentsIsErrorAtom,
|
||||
deployReleaseMenuEnvironmentDeploymentsIsLoadingAtom,
|
||||
deployReleaseMenuOpenAtom,
|
||||
openDeleteReleaseDialogAtom,
|
||||
openEditReleaseDialogAtom,
|
||||
@ -54,19 +56,21 @@ function DeployReleaseMenuContent({ onDeleted }: {
|
||||
const setDeleteReleaseDialogOpen = useSetAtom(deleteReleaseDialogOpenAtom)
|
||||
const openEditReleaseDialog = useSetAtom(openEditReleaseDialogAtom)
|
||||
const openDeleteReleaseDialog = useSetAtom(openDeleteReleaseDialogAtom)
|
||||
const environmentDeploymentsQuery = useAtomValue(deployReleaseMenuEnvironmentDeploymentsQueryAtom)
|
||||
const appInstanceQuery = useAtomValue(deployReleaseMenuAppInstanceQueryAtom)
|
||||
const environmentDeployments = useAtomValue(deployReleaseMenuEnvironmentDeploymentsAtom)
|
||||
const environmentDeploymentsIsLoading = useAtomValue(deployReleaseMenuEnvironmentDeploymentsIsLoadingAtom)
|
||||
const environmentDeploymentsIsError = useAtomValue(deployReleaseMenuEnvironmentDeploymentsIsErrorAtom)
|
||||
const appInstanceName = useAtomValue(deployReleaseMenuAppInstanceNameAtom)
|
||||
const deleteRelease = useMutation(consoleQuery.enterprise.releaseService.deleteRelease.mutationOptions())
|
||||
const exportReleaseDslMutation = useMutation(mutationOptions({
|
||||
mutationKey: ['deployments', 'release-dsl-export'],
|
||||
mutationFn: (input: ExportReleaseDslInput) => exportReleaseDsl(input),
|
||||
}))
|
||||
|
||||
const environments = (environmentDeploymentsQuery.data?.environmentDeployments ?? [])
|
||||
const deploymentEnvironmentRows = environmentDeployments?.environmentDeployments ?? []
|
||||
const environments = deploymentEnvironmentRows
|
||||
.map(row => row.environment)
|
||||
const deploymentRows = environmentDeploymentsQuery.data?.environmentDeployments.filter(row => !isUndeployedDeploymentRow(row)) ?? []
|
||||
const deploymentRows = deploymentEnvironmentRows.filter(row => !isUndeployedDeploymentRow(row))
|
||||
const targetRelease = releaseRows.find(release => release.id === releaseId)
|
||||
const appInstanceName = appInstanceQuery.data?.appInstance.displayName
|
||||
|
||||
if (!targetRelease)
|
||||
return null
|
||||
@ -74,8 +78,8 @@ function DeployReleaseMenuContent({ onDeleted }: {
|
||||
const release = targetRelease
|
||||
const targetReleaseName = release.displayName
|
||||
const deleteUsageCount = releaseUsageCount(releaseId, deploymentRows)
|
||||
const isCheckingDeleteUsage = open && environmentDeploymentsQuery.isLoading
|
||||
const hasDeleteUsageCheckFailed = open && environmentDeploymentsQuery.isError
|
||||
const isCheckingDeleteUsage = open && environmentDeploymentsIsLoading
|
||||
const hasDeleteUsageCheckFailed = open && environmentDeploymentsIsError
|
||||
const isReleaseInUse = deleteUsageCount > 0
|
||||
const isDeletingRelease = deleteRelease.isPending
|
||||
const isExportingDsl = exportReleaseDslMutation.isPending
|
||||
@ -130,7 +134,7 @@ function DeployReleaseMenuContent({ onDeleted }: {
|
||||
|
||||
const groupedRows = buildDeployMenuSections({
|
||||
environments,
|
||||
environmentDeployments: environmentDeploymentsQuery.data?.environmentDeployments ?? [],
|
||||
environmentDeployments: deploymentEnvironmentRows,
|
||||
releaseRows,
|
||||
releaseId,
|
||||
targetRelease: release,
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Release } from '@dify/contracts/enterprise/types.gen'
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { atomWithLazy } from 'jotai/utils'
|
||||
import { atomWithLazy, selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
|
||||
|
||||
@ -35,6 +35,19 @@ export const deployReleaseMenuEnvironmentDeploymentsQueryAtom = atomWithQuery((g
|
||||
})
|
||||
})
|
||||
|
||||
export const deployReleaseMenuEnvironmentDeploymentsAtom = selectAtom(
|
||||
deployReleaseMenuEnvironmentDeploymentsQueryAtom,
|
||||
query => query.data,
|
||||
)
|
||||
export const deployReleaseMenuEnvironmentDeploymentsIsLoadingAtom = selectAtom(
|
||||
deployReleaseMenuEnvironmentDeploymentsQueryAtom,
|
||||
query => query.isLoading,
|
||||
)
|
||||
export const deployReleaseMenuEnvironmentDeploymentsIsErrorAtom = selectAtom(
|
||||
deployReleaseMenuEnvironmentDeploymentsQueryAtom,
|
||||
query => query.isError,
|
||||
)
|
||||
|
||||
export const deployReleaseMenuAppInstanceQueryAtom = atomWithQuery((get) => {
|
||||
const appInstanceId = get(deploymentRouteAppInstanceIdAtom)
|
||||
const menuOpen = get(deployReleaseMenuOpenAtom)
|
||||
@ -49,6 +62,11 @@ export const deployReleaseMenuAppInstanceQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deployReleaseMenuAppInstanceNameAtom = selectAtom(
|
||||
deployReleaseMenuAppInstanceQueryAtom,
|
||||
query => query.data?.appInstance.displayName,
|
||||
)
|
||||
|
||||
export const openEditReleaseDialogAtom = atom(null, (_get, set) => {
|
||||
set(deployReleaseMenuOpenAtom, false)
|
||||
set(deleteReleaseDialogOpenAtom, false)
|
||||
|
||||
@ -7,8 +7,10 @@ import { DeploymentEmptyState, DeploymentStateMessage } from '../../../shared/co
|
||||
import {
|
||||
adjustReleaseHistoryPageAfterDeleteAtom,
|
||||
RELEASE_HISTORY_PAGE_SIZE,
|
||||
releaseHistoryAtom,
|
||||
releaseHistoryCurrentPageAtom,
|
||||
releaseHistoryQueryAtom,
|
||||
releaseHistoryIsErrorAtom,
|
||||
releaseHistoryIsLoadingAtom,
|
||||
setReleaseHistoryCurrentPageAtom,
|
||||
} from '../state'
|
||||
import { ReleaseHistoryRows } from './release-history-rows'
|
||||
@ -20,9 +22,9 @@ export function ReleaseHistoryTable() {
|
||||
const currentPage = useAtomValue(releaseHistoryCurrentPageAtom)
|
||||
const setCurrentPage = useSetAtom(setReleaseHistoryCurrentPageAtom)
|
||||
const adjustPageAfterDelete = useSetAtom(adjustReleaseHistoryPageAfterDeleteAtom)
|
||||
const releaseHistoryQuery = useAtomValue(releaseHistoryQueryAtom)
|
||||
const isLoading = releaseHistoryQuery.isLoading
|
||||
const hasError = releaseHistoryQuery.isError
|
||||
const releaseHistory = useAtomValue(releaseHistoryAtom)
|
||||
const isLoading = useAtomValue(releaseHistoryIsLoadingAtom)
|
||||
const hasError = useAtomValue(releaseHistoryIsErrorAtom)
|
||||
|
||||
if (isLoading)
|
||||
return <ReleaseHistoryTableSkeleton />
|
||||
@ -35,7 +37,6 @@ export function ReleaseHistoryTable() {
|
||||
)
|
||||
}
|
||||
|
||||
const releaseHistory = releaseHistoryQuery.data
|
||||
if (!releaseHistory) {
|
||||
return (
|
||||
<DeploymentStateMessage variant="list">
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { keepPreviousData, skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
|
||||
@ -29,6 +30,10 @@ export const releaseHistoryQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const releaseHistoryAtom = selectAtom(releaseHistoryQueryAtom, query => query.data)
|
||||
export const releaseHistoryIsLoadingAtom = selectAtom(releaseHistoryQueryAtom, query => query.isLoading)
|
||||
export const releaseHistoryIsErrorAtom = selectAtom(releaseHistoryQueryAtom, query => query.isError)
|
||||
|
||||
export const setReleaseHistoryCurrentPageAtom = atom(null, (_get, set, page: number) => {
|
||||
set(releaseHistoryCurrentPageAtom, Math.max(page, 0))
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { nextPathnameAtom } from '@/app/components/next-route-state/atoms'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../route-state'
|
||||
@ -35,6 +36,10 @@ export const deploymentDetailAppInstanceQueryAtom = atomWithQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deploymentDetailAppInstanceAtom = selectAtom(deploymentDetailAppInstanceQueryAtom, query => query.data)
|
||||
export const deploymentDetailAppInstanceIsLoadingAtom = selectAtom(deploymentDetailAppInstanceQueryAtom, query => query.isLoading)
|
||||
export const deploymentDetailAppInstanceIsErrorAtom = selectAtom(deploymentDetailAppInstanceQueryAtom, query => query.isError)
|
||||
|
||||
export const deploymentEnvironmentDeploymentsQueryAtom = atomWithQuery((get) => {
|
||||
const appInstanceId = get(deploymentRouteAppInstanceIdAtom)
|
||||
|
||||
@ -49,6 +54,10 @@ export const deploymentEnvironmentDeploymentsQueryAtom = atomWithQuery((get) =>
|
||||
})
|
||||
})
|
||||
|
||||
export const deploymentEnvironmentDeploymentsAtom = selectAtom(deploymentEnvironmentDeploymentsQueryAtom, query => query.data)
|
||||
export const deploymentEnvironmentDeploymentsIsLoadingAtom = selectAtom(deploymentEnvironmentDeploymentsQueryAtom, query => query.isLoading)
|
||||
export const deploymentEnvironmentDeploymentsIsErrorAtom = selectAtom(deploymentEnvironmentDeploymentsQueryAtom, query => query.isError)
|
||||
|
||||
export const deploymentRuntimeInstanceRowsAtom = atom((get) => {
|
||||
return get(deploymentEnvironmentDeploymentsQueryAtom).data?.environmentDeployments.filter(hasRuntimeInstanceDeployment) ?? []
|
||||
return get(deploymentEnvironmentDeploymentsAtom)?.environmentDeployments.filter(hasRuntimeInstanceDeployment) ?? []
|
||||
})
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ReactNode } from 'react'
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { useHydrateAtoms } from 'jotai/utils'
|
||||
import { selectAtom, useHydrateAtoms } from 'jotai/utils'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentStatusPollingInterval } from '../../shared/domain/runtime-status'
|
||||
@ -52,8 +52,10 @@ const deploymentsListEnvironmentsQueryAtom = atomWithQuery(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const deploymentsListEnvironmentsDataAtom = selectAtom(deploymentsListEnvironmentsQueryAtom, query => query.data)
|
||||
|
||||
export const deploymentsListEnvironmentFilterOptionsAtom = atom((get): DeploymentsListEnvironmentFilterOption[] => {
|
||||
const environments = get(deploymentsListEnvironmentsQueryAtom).data?.environments ?? []
|
||||
const environments = get(deploymentsListEnvironmentsDataAtom)?.environments ?? []
|
||||
|
||||
return [
|
||||
{
|
||||
@ -83,7 +85,7 @@ export const deploymentsListSelectedEnvironmentFilterOptionAtom = atom((get): De
|
||||
: allOption)
|
||||
})
|
||||
|
||||
export const deploymentsListQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
const deploymentsListQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
const queryKeywords = get(deploymentsListKeywordsAtom).trim()
|
||||
const queryEnvironmentId = get(deploymentsListEnvironmentIdAtom) ?? undefined
|
||||
|
||||
@ -114,26 +116,34 @@ export const deploymentsListQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
})
|
||||
})
|
||||
|
||||
const deploymentsListDataAtom = selectAtom(deploymentsListQueryAtom, query => query.data)
|
||||
export const deploymentsListErrorAtom = selectAtom(deploymentsListQueryAtom, query => query.error)
|
||||
export const deploymentsListFetchNextPageAtom = selectAtom(deploymentsListQueryAtom, query => query.fetchNextPage)
|
||||
export const deploymentsListHasNextPageAtom = selectAtom(deploymentsListQueryAtom, query => query.hasNextPage)
|
||||
export const deploymentsListIsFetchingAtom = selectAtom(deploymentsListQueryAtom, query => query.isFetching)
|
||||
export const deploymentsListIsFetchingNextPageAtom = selectAtom(deploymentsListQueryAtom, query => query.isFetchingNextPage)
|
||||
export const deploymentsListIsLoadingAtom = selectAtom(deploymentsListQueryAtom, query => query.isLoading)
|
||||
const deploymentsListIsErrorAtom = selectAtom(deploymentsListQueryAtom, query => query.isError)
|
||||
|
||||
export const deploymentsListRowsAtom = atom((get) => {
|
||||
return get(deploymentsListQueryAtom).data?.pages.flatMap(page => page.appInstanceSummaries) ?? []
|
||||
return get(deploymentsListDataAtom)?.pages.flatMap(page => page.appInstanceSummaries) ?? []
|
||||
})
|
||||
|
||||
export const deploymentsListShowSkeletonAtom = atom((get) => {
|
||||
const deploymentsListQuery = get(deploymentsListQueryAtom)
|
||||
const pages = deploymentsListQuery.data?.pages ?? []
|
||||
const pages = get(deploymentsListDataAtom)?.pages ?? []
|
||||
|
||||
return deploymentsListQuery.isLoading || (deploymentsListQuery.isFetching && pages.length === 0)
|
||||
return get(deploymentsListIsLoadingAtom) || (get(deploymentsListIsFetchingAtom) && pages.length === 0)
|
||||
})
|
||||
|
||||
export const deploymentsListShowEmptyStateAtom = atom((get) => {
|
||||
return !get(deploymentsListShowSkeletonAtom)
|
||||
&& !get(deploymentsListQueryAtom).isError
|
||||
&& !get(deploymentsListIsErrorAtom)
|
||||
&& get(deploymentsListRowsAtom).length === 0
|
||||
})
|
||||
|
||||
export const deploymentsListShowErrorStateAtom = atom((get) => {
|
||||
return !get(deploymentsListShowSkeletonAtom)
|
||||
&& get(deploymentsListQueryAtom).isError
|
||||
&& get(deploymentsListIsErrorAtom)
|
||||
})
|
||||
|
||||
export const deploymentsListHasFilterAtom = atom((get) => {
|
||||
|
||||
@ -12,8 +12,13 @@ import { SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import { DeploymentEmptyState, DeploymentStateMessage } from '../../shared/components/empty-state'
|
||||
import { useInfiniteScroll } from '../../shared/hooks/use-infinite-scroll'
|
||||
import {
|
||||
deploymentsListErrorAtom,
|
||||
deploymentsListFetchNextPageAtom,
|
||||
deploymentsListHasFilterAtom,
|
||||
deploymentsListQueryAtom,
|
||||
deploymentsListHasNextPageAtom,
|
||||
deploymentsListIsFetchingAtom,
|
||||
deploymentsListIsFetchingNextPageAtom,
|
||||
deploymentsListIsLoadingAtom,
|
||||
deploymentsListRowsAtom,
|
||||
deploymentsListShowEmptyStateAtom,
|
||||
deploymentsListShowErrorStateAtom,
|
||||
@ -157,13 +162,25 @@ function DeploymentsListControls() {
|
||||
|
||||
export function DeploymentsListShell() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const deploymentsListQuery = useAtomValue(deploymentsListQueryAtom)
|
||||
const deploymentsListError = useAtomValue(deploymentsListErrorAtom)
|
||||
const deploymentsListFetchNextPage = useAtomValue(deploymentsListFetchNextPageAtom)
|
||||
const deploymentsListHasNextPage = useAtomValue(deploymentsListHasNextPageAtom)
|
||||
const deploymentsListIsFetching = useAtomValue(deploymentsListIsFetchingAtom)
|
||||
const deploymentsListIsFetchingNextPage = useAtomValue(deploymentsListIsFetchingNextPageAtom)
|
||||
const deploymentsListIsLoading = useAtomValue(deploymentsListIsLoadingAtom)
|
||||
const appInstanceSummaries = useAtomValue(deploymentsListRowsAtom)
|
||||
const showSkeleton = useAtomValue(deploymentsListShowSkeletonAtom)
|
||||
const showErrorState = useAtomValue(deploymentsListShowErrorStateAtom)
|
||||
const showEmptyState = useAtomValue(deploymentsListShowEmptyStateAtom)
|
||||
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(deploymentsListQuery)
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>({
|
||||
error: deploymentsListError,
|
||||
fetchNextPage: deploymentsListFetchNextPage,
|
||||
hasNextPage: deploymentsListHasNextPage,
|
||||
isFetching: deploymentsListIsFetching,
|
||||
isFetchingNextPage: deploymentsListIsFetchingNextPage,
|
||||
isLoading: deploymentsListIsLoading,
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
@ -185,7 +202,7 @@ export function DeploymentsListShell() {
|
||||
summary={summary}
|
||||
/>
|
||||
))}
|
||||
{deploymentsListQuery.isFetchingNextPage && <DeploymentsListSkeleton />}
|
||||
{deploymentsListIsFetchingNextPage && <DeploymentsListSkeleton />}
|
||||
<div ref={sentinelRef} aria-hidden="true" className="col-span-full h-px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user