diff --git a/web/app/(commonLayout)/deployments/layout.tsx b/web/app/(commonLayout)/deployments/layout.tsx
index b8088169fd5..eb522444778 100644
--- a/web/app/(commonLayout)/deployments/layout.tsx
+++ b/web/app/(commonLayout)/deployments/layout.tsx
@@ -1,13 +1,11 @@
import type { ReactNode } from 'react'
import { DeployDrawer } from '@/features/deployments/deploy-drawer'
-import { DeploymentsRouteStateHydrator } from '@/features/deployments/route-state-hydrator'
export default function DeploymentsLayout({ children }: {
children: ReactNode
}) {
return (
<>
-
{children}
>
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index 4d6bdf7984c..6092618bfb3 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -7,6 +7,7 @@ import Zendesk from '@/app/components/base/zendesk'
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
import { GotoAnything } from '@/app/components/goto-anything'
import MainNavLayout from '@/app/components/main-nav/layout'
+import { NextRouteStateBridge } from '@/app/components/next-route-state'
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import WorkflowGeneratorMount from '@/app/components/workflow/workflow-generator/mount'
@@ -30,6 +31,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
+
{children}
diff --git a/web/app/components/next-route-state/atoms.ts b/web/app/components/next-route-state/atoms.ts
new file mode 100644
index 00000000000..1997f284976
--- /dev/null
+++ b/web/app/components/next-route-state/atoms.ts
@@ -0,0 +1,49 @@
+import { atom } from 'jotai'
+
+export type NextRouteParams = Record
+
+type NextRouteState = {
+ pathname: string
+ params: NextRouteParams
+}
+
+const nextRouteStateAtom = atom({
+ pathname: '',
+ params: {},
+})
+
+function normalizedParamEntries(params: NextRouteParams) {
+ return Object.keys(params)
+ .sort()
+ .map((key) => {
+ const value = params[key]
+ return [key, Array.isArray(value) ? [...value] : value] as const
+ })
+}
+
+function normalizeNextRouteParams(params: NextRouteParams): NextRouteParams {
+ return Object.fromEntries(normalizedParamEntries(params)) as NextRouteParams
+}
+
+function routeParamsKey(params: NextRouteParams) {
+ return JSON.stringify(normalizedParamEntries(params))
+}
+
+export const nextPathnameAtom = atom(get => get(nextRouteStateAtom).pathname)
+
+export const nextParamsAtom = atom(get => get(nextRouteStateAtom).params)
+
+export const setNextRouteStateAtom = atom(null, (get, set, routeState: NextRouteState) => {
+ const nextParams = normalizeNextRouteParams(routeState.params)
+ const currentRouteState = get(nextRouteStateAtom)
+
+ if (
+ currentRouteState.pathname !== routeState.pathname
+ || routeParamsKey(currentRouteState.params) !== routeParamsKey(nextParams)
+ ) {
+ set(nextRouteStateAtom, {
+ pathname: routeState.pathname,
+ params: nextParams,
+ })
+ }
+})
diff --git a/web/app/components/next-route-state/index.tsx b/web/app/components/next-route-state/index.tsx
new file mode 100644
index 00000000000..7fb2d83c767
--- /dev/null
+++ b/web/app/components/next-route-state/index.tsx
@@ -0,0 +1,24 @@
+'use client'
+
+import type { NextRouteParams } from './atoms'
+import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect'
+import { useSetAtom } from 'jotai'
+import { useParams, usePathname } from '@/next/navigation'
+import {
+ setNextRouteStateAtom,
+} from './atoms'
+
+export function NextRouteStateBridge() {
+ const pathname = usePathname()
+ const params = useParams()
+ const setNextRouteState = useSetAtom(setNextRouteStateAtom)
+
+ useIsomorphicLayoutEffect(() => {
+ setNextRouteState({
+ pathname,
+ params,
+ })
+ }, [params, pathname, setNextRouteState])
+
+ return null
+}
diff --git a/web/features/deployments/detail/__tests__/state.spec.ts b/web/features/deployments/detail/__tests__/state.spec.ts
index 5d7b9d47948..1a53b843c4c 100644
--- a/web/features/deployments/detail/__tests__/state.spec.ts
+++ b/web/features/deployments/detail/__tests__/state.spec.ts
@@ -2,7 +2,7 @@ import type { Getter } from 'jotai'
import { skipToken } from '@tanstack/react-query'
import { atom, createStore } from 'jotai'
import { describe, expect, it, vi } from 'vitest'
-import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
+import { setNextRouteStateAtom } from '@/app/components/next-route-state/atoms'
type QueryOptions = {
enabled?: boolean
@@ -65,6 +65,13 @@ async function loadState() {
return await import('../state')
}
+function setDeploymentRoute(store: ReturnType, appInstanceId = 'app-instance-1') {
+ store.set(setNextRouteStateAtom, {
+ pathname: `/deployments/${appInstanceId}/overview`,
+ params: { appInstanceId },
+ })
+}
+
describe('deployment detail state', () => {
it('should disable detail queries with skipToken until a route app instance exists', async () => {
const state = await loadState()
@@ -92,7 +99,7 @@ describe('deployment detail state', () => {
const state = await loadState()
const store = createStore()
- store.set(deploymentRouteAppInstanceIdAtom, 'app-instance-1')
+ setDeploymentRoute(store)
store.set(state.deploymentSourceAppIdAtom, 'source-app-1')
expect(store.get(state.deploymentDetailAppInstanceQueryAtom)).toMatchObject({
diff --git a/web/features/deployments/detail/versions-tab/__tests__/state.spec.ts b/web/features/deployments/detail/versions-tab/__tests__/state.spec.ts
index 7feae92226a..2e58fb3bb80 100644
--- a/web/features/deployments/detail/versions-tab/__tests__/state.spec.ts
+++ b/web/features/deployments/detail/versions-tab/__tests__/state.spec.ts
@@ -2,7 +2,7 @@ import type { Getter } from 'jotai'
import { skipToken } from '@tanstack/react-query'
import { atom, createStore } from 'jotai'
import { describe, expect, it, vi } from 'vitest'
-import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
+import { setNextRouteStateAtom } from '@/app/components/next-route-state/atoms'
type QueryOptions = {
enabled?: boolean
@@ -57,6 +57,13 @@ async function loadState() {
return await import('../state')
}
+function setDeploymentRoute(store: ReturnType, appInstanceId = 'app-instance-1') {
+ store.set(setNextRouteStateAtom, {
+ pathname: `/deployments/${appInstanceId}/overview`,
+ params: { appInstanceId },
+ })
+}
+
describe('versions tab state', () => {
it('should gate release history and menu queries until route and menu state are ready', async () => {
const state = await loadState()
@@ -79,7 +86,7 @@ describe('versions tab state', () => {
it('should build release history input from the current page', async () => {
const state = await loadState()
const store = createStore()
- store.set(deploymentRouteAppInstanceIdAtom, 'app-instance-1')
+ setDeploymentRoute(store)
store.set(state.setReleaseHistoryCurrentPageAtom, -1)
expect(store.get(state.releaseHistoryCurrentPageAtom)).toBe(0)
@@ -99,7 +106,7 @@ describe('versions tab state', () => {
it('should scope deploy menu queries to the open release id', async () => {
const state = await loadState()
const store = createStore()
- store.set(deploymentRouteAppInstanceIdAtom, 'app-instance-1')
+ setDeploymentRoute(store)
store.set(state.setDeployReleaseMenuOpenAtom, {
releaseId: 'release-1',
diff --git a/web/features/deployments/nav/__tests__/state.spec.ts b/web/features/deployments/nav/__tests__/state.spec.ts
index 7374aa7515c..e4eb25bae01 100644
--- a/web/features/deployments/nav/__tests__/state.spec.ts
+++ b/web/features/deployments/nav/__tests__/state.spec.ts
@@ -6,10 +6,7 @@ import type {
import type { Getter } from 'jotai'
import { atom, createStore } from 'jotai'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import {
- deploymentRouteAppInstanceIdAtom,
- deploymentsRouteActiveAtom,
-} from '../../route-state'
+import { setNextRouteStateAtom } from '@/app/components/next-route-state/atoms'
type QueryOptions = {
enabled?: boolean
@@ -120,6 +117,13 @@ function setListAppInstances(appInstances: AppInstance[]) {
})
}
+function setDeploymentRoute(store: ReturnType, appInstanceId = 'app-instance-1') {
+ store.set(setNextRouteStateAtom, {
+ pathname: `/deployments/${appInstanceId}/overview`,
+ params: { appInstanceId },
+ })
+}
+
describe('deployments nav state', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -137,8 +141,7 @@ describe('deployments nav state', () => {
it('should append the current route item when it is missing from the list query', async () => {
const state = await loadState()
const store = createStore()
- store.set(deploymentsRouteActiveAtom, true)
- store.set(deploymentRouteAppInstanceIdAtom, 'app-instance-1')
+ setDeploymentRoute(store)
setListAppInstances([
appInstance({
id: 'app-instance-2',
@@ -172,8 +175,7 @@ describe('deployments nav state', () => {
it('should use the route id as a fallback current item name', async () => {
const state = await loadState()
const store = createStore()
- store.set(deploymentsRouteActiveAtom, true)
- store.set(deploymentRouteAppInstanceIdAtom, 'app-instance-1')
+ setDeploymentRoute(store)
setListAppInstances([])
expect(store.get(state.deploymentsNavItemsAtom)).toMatchObject([
diff --git a/web/features/deployments/route-state-hydrator.tsx b/web/features/deployments/route-state-hydrator.tsx
deleted file mode 100644
index e2c6fefd44f..00000000000
--- a/web/features/deployments/route-state-hydrator.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client'
-
-import { useSetAtom } from 'jotai'
-import { useHydrateAtoms } from 'jotai/react/utils'
-import { useEffect } from 'react'
-import { useParams } from '@/next/navigation'
-import {
- deploymentRouteAppInstanceIdAtom,
- deploymentsRouteActiveAtom,
-} from './route-state'
-
-function routeAppInstanceId(params?: { appInstanceId?: string | string[] }) {
- return typeof params?.appInstanceId === 'string' ? params.appInstanceId : undefined
-}
-
-export function DeploymentsRouteStateHydrator() {
- const params = useParams<{ appInstanceId?: string | string[] }>()
- const appInstanceId = routeAppInstanceId(params)
- const setDeploymentsRouteActive = useSetAtom(deploymentsRouteActiveAtom)
- const setRouteAppInstanceId = useSetAtom(deploymentRouteAppInstanceIdAtom)
-
- useHydrateAtoms([
- [deploymentsRouteActiveAtom, true],
- [deploymentRouteAppInstanceIdAtom, appInstanceId],
- ] as const, { dangerouslyForceHydrate: true })
-
- useEffect(() => {
- return () => {
- setDeploymentsRouteActive(false)
- setRouteAppInstanceId(undefined)
- }
- }, [setDeploymentsRouteActive, setRouteAppInstanceId])
-
- return null
-}
diff --git a/web/features/deployments/route-state.ts b/web/features/deployments/route-state.ts
index 8e1d7d5fead..fc2c7397188 100644
--- a/web/features/deployments/route-state.ts
+++ b/web/features/deployments/route-state.ts
@@ -1,6 +1,19 @@
'use client'
import { atom } from 'jotai'
+import {
+ nextParamsAtom,
+ nextPathnameAtom,
+} from '@/app/components/next-route-state/atoms'
-export const deploymentsRouteActiveAtom = atom(false)
-export const deploymentRouteAppInstanceIdAtom = atom(undefined)
+function isDeploymentsRoute(pathname: string) {
+ return pathname === '/deployments' || pathname.startsWith('/deployments/')
+}
+
+export const deploymentsRouteActiveAtom = atom(get => isDeploymentsRoute(get(nextPathnameAtom)))
+
+export const deploymentRouteAppInstanceIdAtom = atom((get) => {
+ const appInstanceId = get(nextParamsAtom).appInstanceId
+
+ return typeof appInstanceId === 'string' ? appInstanceId : undefined
+})