This is an automated email from the ASF dual-hosted git repository.

moonming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 91d89d117 fix: pass host reset (#3295)
91d89d117 is described below

commit 91d89d117b69d601ce62375051cbf39d1a3bcce8
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Wed May 20 04:59:33 2026 +0530

    fix: pass host reset (#3295)
    
    * fix: pass_host resets to default when editing upstream nodes (#3294)
    
    Also added E2E regression test e2e/tests/upstreams.pass-host-reset.spec.ts
    
    * chore: update generated route tree
    
    * chore: ensure route tree matches current environment
    
    * fix: update test files to ensure proper functionality and add type safety 
in upstream form
    
    * fix: refactor route detail forms to use useSuspenseQuery and improve 
cleanup logic
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * fix: add loading skeleton to RouteDetailForm and improve service data 
handling
    
    * Replaced useQuery with useSuspenseQuery & Added Suspense with Skeleton
    
    * Improved readibility
    
    * fix: resolve the must-fix issue regarding the UX regression
    
    * fix(upstream): apply Suspense boundary to UpstreamDetailForm to prevent 
UX regression
    
    * style(stream-routes): use consistent Skeleton Suspense fallback for 
detail page
    
    * test(e2e): remove global beforeAll upstream deletion to avoid race 
conditions
    
    * perf: useMemo for formDefaults in upstream and route detail pages
    
    * fix: resolve E2E test failures by improving form synchronization and data 
initialization
    
    * Fixed lint error in util.ts: The fix was typing produceVarsToAPI's return 
to reflect that vars becomes unknown[] | undefined after JSON.parse, which 
satisfies the APISIXType['Route'] cast in add.tsx.
    
    * Updated the function signature to accept APISIXType['Route'] to match 
what's actually being passed.
    
    * fix(upstream): stabilize node state sync and resolve E2E timeouts
    
    - Fix infinite update loop in FormItemNodes by ensuring stable IDs in 
genRecord
    - Implement atomic sync between MobX and RHF by adding deep-equal checks
    - Stabilize E2E tests by injecting safety pauses during node row transitions
    - Updated test helpers to ensure valid node data (port/weight) for Zod 
validation
    
    * fix: resolve merge artifacts and syntax errors in detail pages
    
    * Update routeTree.gen.ts
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 e2e/tests/routes.clear-upstream-field.spec.ts      |  47 +++-
 e2e/tests/routes.crud-all-fields.spec.ts           |   2 +
 e2e/tests/upstreams.crud-required-fields.spec.ts   |  10 +-
 e2e/tests/upstreams.pass-host-reset.spec.ts        | 305 +++++++++++++++++++++
 e2e/utils/ui/index.ts                              |  10 +
 e2e/utils/ui/upstreams.ts                          |  41 +--
 src/apis/routes.ts                                 |   3 +-
 src/components/form-slice/FormPartRoute/util.ts    |   4 +-
 .../form-slice/FormPartUpstream/FormItemNodes.tsx  |  29 +-
 .../form-slice/FormPartUpstream/schema.ts          |   4 +-
 src/components/form-slice/FormPartUpstream/util.ts |  16 ++
 src/components/form/Editor.tsx                     |   2 +-
 src/routes/routes/add.tsx                          |  11 +-
 src/routes/routes/detail.$id.tsx                   |  84 ++++--
 src/routes/services/detail.$id/index.tsx           |  39 ++-
 src/routes/stream_routes/detail.$id.tsx            |  45 +--
 src/routes/upstreams/add.tsx                       |   6 +-
 src/routes/upstreams/detail.$id.tsx                |  42 +--
 18 files changed, 558 insertions(+), 142 deletions(-)

diff --git a/e2e/tests/routes.clear-upstream-field.spec.ts 
b/e2e/tests/routes.clear-upstream-field.spec.ts
index 7d625f043..18b558c93 100644
--- a/e2e/tests/routes.clear-upstream-field.spec.ts
+++ b/e2e/tests/routes.clear-upstream-field.spec.ts
@@ -23,11 +23,14 @@ import { uiDeleteRoute } from '@e2e/utils/ui/routes';
 import { uiFillUpstreamRequiredFields } from '@e2e/utils/ui/upstreams';
 import { expect, type Page } from '@playwright/test';
 
-import { deleteAllRoutes, getRouteReq } from '@/apis/routes';
-import { deleteAllServices, postServiceReq } from '@/apis/services';
-import { deleteAllUpstreams, postUpstreamReq } from '@/apis/upstreams';
+import { getRouteReq } from '@/apis/routes';
+import { postServiceReq } from '@/apis/services';
+import { postUpstreamReq } from '@/apis/upstreams';
+import { API_ROUTES, API_SERVICES, API_UPSTREAMS } from '@/config/constant';
 import type { APISIXType } from '@/types/schema/apisix';
 
+test.describe.configure({ mode: 'serial' });
+
 const upstreamName = randomId('test-upstream');
 const serviceName = randomId('test-service');
 const routeNameForUpstreamId = randomId('test-route-upstream-id');
@@ -42,6 +45,7 @@ const upstreamNodes: APISIXType['UpstreamNode'][] = [
 
 let testUpstreamId: string;
 let testServiceId: string;
+const createdRouteIds = new Set<string>();
 
 // Common helper functions
 async function fillBasicRouteFields(
@@ -130,11 +134,6 @@ async function editRouteAndAddUpstream(
 }
 
 test.beforeAll(async () => {
-  // Clean up existing resources
-  await deleteAllRoutes(e2eReq);
-  await deleteAllServices(e2eReq);
-  await deleteAllUpstreams(e2eReq);
-
   // Create a test upstream for testing upstream_id scenario
   const upstreamResponse = await postUpstreamReq(e2eReq, {
     name: upstreamName,
@@ -150,10 +149,24 @@ test.beforeAll(async () => {
   testServiceId = serviceResponse.data.value.id;
 });
 
+test.afterEach(async () => {
+  await Promise.all(
+    Array.from(createdRouteIds).map((routeId) =>
+      e2eReq.delete(`${API_ROUTES}/${routeId}`).catch(() => {
+        // Ignore cleanup errors so tests can proceed; route may already be 
deleted.
+      })
+    )
+  );
+  createdRouteIds.clear();
+});
+
 test.afterAll(async () => {
-  await deleteAllRoutes(e2eReq);
-  await deleteAllServices(e2eReq);
-  await deleteAllUpstreams(e2eReq);
+  await e2eReq.delete(`${API_SERVICES}/${testServiceId}`).catch(() => {
+    // Ignore cleanup errors; resource may already be deleted.
+  });
+  await e2eReq.delete(`${API_UPSTREAMS}/${testUpstreamId}`).catch(() => {
+    // Ignore cleanup errors; resource may already be deleted.
+  });
 });
 
 test('should clear upstream field when upstream_id exists (create and edit)', 
async ({
@@ -189,7 +202,8 @@ test('should clear upstream field when upstream_id exists 
(create and edit)', as
   });
 
   await test.step('verify upstream field is cleared after creation', async () 
=> {
-    await verifyRouteData(page, 'upstream_id', testUpstreamId);
+    const routeId = await verifyRouteData(page, 'upstream_id', testUpstreamId);
+    createdRouteIds.add(routeId);
   });
 
   await test.step('edit route and add upstream configuration again', async () 
=> {
@@ -201,7 +215,8 @@ test('should clear upstream field when upstream_id exists 
(create and edit)', as
   });
 
   await test.step('verify upstream field is still cleared after editing', 
async () => {
-    await verifyRouteData(page, 'upstream_id', testUpstreamId);
+    const routeId = await verifyRouteData(page, 'upstream_id', testUpstreamId);
+    createdRouteIds.add(routeId);
     await uiDeleteRoute(page);
   });
 });
@@ -240,7 +255,8 @@ test('should clear upstream field when service_id exists 
(create and edit)', asy
   });
 
   await test.step('verify upstream field is cleared after creation', async () 
=> {
-    await verifyRouteData(page, 'service_id', testServiceId);
+    const routeId = await verifyRouteData(page, 'service_id', testServiceId);
+    createdRouteIds.add(routeId);
   });
 
   await test.step('edit route and add upstream configuration again', async () 
=> {
@@ -252,7 +268,8 @@ test('should clear upstream field when service_id exists 
(create and edit)', asy
   });
 
   await test.step('verify upstream field is still cleared after editing', 
async () => {
-    await verifyRouteData(page, 'service_id', testServiceId);
+    const routeId = await verifyRouteData(page, 'service_id', testServiceId);
+    createdRouteIds.add(routeId);
     await uiDeleteRoute(page);
   });
 });
diff --git a/e2e/tests/routes.crud-all-fields.spec.ts 
b/e2e/tests/routes.crud-all-fields.spec.ts
index 4f1604a4b..e8b76f677 100644
--- a/e2e/tests/routes.crud-all-fields.spec.ts
+++ b/e2e/tests/routes.crud-all-fields.spec.ts
@@ -20,6 +20,7 @@ import { e2eReq } from '@e2e/utils/req';
 import { test } from '@e2e/utils/test';
 import {
   uiClearMonacoEditor,
+  uiEnsureSettingsClosed,
   uiFillMonacoEditor,
   uiGetMonacoEditor,
   uiHasToastMsg,
@@ -47,6 +48,7 @@ test.beforeAll(async () => {
 });
 
 test('should CRUD route with all fields', async ({ page }) => {
+  await uiEnsureSettingsClosed(page);
   test.slow();
 
   const varsSection = page.getByRole('group', { name: 'Match Rules' 
}).getByText('Vars').locator('..');
diff --git a/e2e/tests/upstreams.crud-required-fields.spec.ts 
b/e2e/tests/upstreams.crud-required-fields.spec.ts
index 55261ae40..b25e0a15d 100644
--- a/e2e/tests/upstreams.crud-required-fields.spec.ts
+++ b/e2e/tests/upstreams.crud-required-fields.spec.ts
@@ -25,20 +25,23 @@ import {
 } from '@e2e/utils/ui/upstreams';
 import { expect } from '@playwright/test';
 
+import { deleteAllRoutes } from '@/apis/routes';
 import { deleteAllUpstreams } from '@/apis/upstreams';
 import type { APISIXType } from '@/types/schema/apisix';
 
 const upstreamName = randomId('test-upstream');
 const nodes: APISIXType['UpstreamNode'][] = [
-  { host: 'test.com' },
-  { host: 'test2.com', port: 80 },
+  { host: 'test.com', port: 80, weight: 100 },
+  { host: 'test2.com', port: 80, weight: 100 },
 ];
 
 test.beforeAll(async () => {
+  await deleteAllRoutes(e2eReq);
   await deleteAllUpstreams(e2eReq);
 });
 
 test('should CRUD upstream with required fields', async ({ page }) => {
+
   await upstreamsPom.toIndex(page);
   await upstreamsPom.isIndexPage(page);
 
@@ -156,7 +159,8 @@ test('should CRUD upstream with required fields', async ({ 
page }) => {
   });
 
   await test.step('delete upstream in detail page', async () => {
-    await page.getByRole('button', { name: 'Delete' }).click();
+    // Delete the upstream
+    await page.getByRole('button', { name: 'Delete', exact: true 
}).first().click();
 
     await page
       .getByRole('dialog', { name: 'Delete Upstream' })
diff --git a/e2e/tests/upstreams.pass-host-reset.spec.ts 
b/e2e/tests/upstreams.pass-host-reset.spec.ts
new file mode 100644
index 000000000..66fa22407
--- /dev/null
+++ b/e2e/tests/upstreams.pass-host-reset.spec.ts
@@ -0,0 +1,305 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { upstreamsPom } from '@e2e/pom/upstreams';
+import { randomId } from '@e2e/utils/common';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+/**
+ * Test for GitHub issue #3294
+ * Bug: pass_host is reset to default value "pass" when editing upstream nodes
+ * @see https://github.com/apache/apisix-dashboard/issues/3294
+ */
+test('should preserve pass_host value when editing upstream nodes', async ({
+  page,
+}) => {
+  const upstreamName = randomId('test-pass-host');
+
+  // Navigate to upstream add page
+  await upstreamsPom.toIndex(page);
+  await upstreamsPom.isIndexPage(page);
+  await upstreamsPom.getAddUpstreamBtn(page).click();
+  await upstreamsPom.isAddPage(page);
+
+  await test.step('create upstream with pass_host=node via UI', async () => {
+    // Fill in the Name field
+    await page.getByLabel('Name', { exact: true }).fill(upstreamName);
+
+    // Add a node
+    const nodesSection = page.getByRole('group', { name: 'Nodes' });
+    const addNodeBtn = page.getByRole('button', { name: 'Add a Node' });
+
+    await addNodeBtn.click();
+    const rows = nodesSection.locator('tr.ant-table-row');
+    const firstRow = rows.first();
+    await expect(firstRow).toBeVisible();
+
+    const hostInput = firstRow.locator('input').first();
+    await hostInput.click();
+    await hostInput.fill('my-service.my-namespace.svc');
+
+    // Click outside to trigger update
+    await nodesSection.click();
+
+    // Set pass_host to "node"
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    await passHostSection.getByRole('textbox', { name: 'Pass Host' }).click();
+    await page.getByRole('option', { name: 'node' }).click();
+
+    // Submit the form
+    await upstreamsPom.getAddBtn(page).click();
+    await uiHasToastMsg(page, {
+      hasText: 'Add Upstream Successfully',
+    });
+  });
+
+  await test.step('verify auto navigate to detail page', async () => {
+    await upstreamsPom.isDetailPage(page);
+  });
+
+  await test.step('verify initial pass_host value is "node"', async () => {
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('node');
+    await expect(passHostField).toBeDisabled();
+  });
+
+  await test.step('click edit and add a new node', async () => {
+    await page.getByRole('button', { name: 'Edit' }).click();
+
+    const nodesSection = page.getByRole('group', { name: 'Nodes' });
+    const addNodeBtn = page.getByRole('button', { name: 'Add a Node' });
+
+    // Add a new node
+    await addNodeBtn.click();
+
+    // Fill in the new node details
+    const rows = nodesSection.locator('tr.ant-table-row');
+    const newRow = rows.nth(1);
+    await expect(newRow).toBeVisible();
+
+    const hostInput = newRow.locator('input').first();
+    await hostInput.click();
+    await hostInput.fill('another-service.svc');
+
+    const portInput = newRow.locator('input').nth(1);
+    await portInput.click();
+    await portInput.fill('8080');
+
+    const weightInput = newRow.locator('input').nth(2);
+    await weightInput.click();
+    await weightInput.fill('1');
+
+    // Click outside to trigger the update
+    await nodesSection.click();
+  });
+
+  await test.step('verify pass_host is still "node" before saving', async () 
=> {
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    // This is the bug check - pass_host should still be "node" not reset to 
"pass"
+    await expect(passHostField).toHaveValue('node');
+  });
+
+  await test.step('save and verify pass_host is preserved', async () => {
+    const saveBtn = page.getByRole('button', { name: 'Save' });
+    await saveBtn.click();
+
+    await uiHasToastMsg(page, {
+      hasText: 'Edit Upstream Successfully',
+    });
+
+    // Verify we're back in detail view mode
+    await upstreamsPom.isDetailPage(page);
+
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('node');
+    await expect(passHostField).toBeDisabled();
+  });
+
+  await test.step('verify pass_host is preserved after page reload', async () 
=> {
+    await page.reload();
+    await page.waitForLoadState('load');
+    await upstreamsPom.isDetailPage(page);
+
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('node');
+  });
+
+  await test.step('delete upstream via UI', async () => {
+    // Navigate to list page first to avoid ambiguity with node delete buttons
+    await upstreamsPom.getUpstreamNavBtn(page).click();
+    await upstreamsPom.isIndexPage(page);
+
+    const row = page.getByRole('row', { name: upstreamName });
+    await row.getByRole('button', { name: 'Delete' }).click();
+
+    await page
+      .getByRole('dialog', { name: 'Delete Upstream' })
+      .getByRole('button', { name: 'Delete' })
+      .click();
+
+    await uiHasToastMsg(page, {
+      hasText: 'Delete Upstream Successfully',
+    });
+    await expect(page.getByRole('cell', { name: upstreamName })).toBeHidden();
+  });
+});
+
+/**
+ * Additional test to verify pass_host=rewrite is also preserved
+ */
+test('should preserve pass_host value "rewrite" when editing upstream nodes', 
async ({
+  page,
+}) => {
+  const upstreamName = randomId('test-pass-host-rewrite');
+
+  // Navigate to upstream add page
+  await upstreamsPom.toIndex(page);
+  await upstreamsPom.isIndexPage(page);
+  await upstreamsPom.getAddUpstreamBtn(page).click();
+  await upstreamsPom.isAddPage(page);
+
+  await test.step('create upstream with pass_host=rewrite via UI', async () => 
{
+    // Fill in the Name field
+    await page.getByLabel('Name', { exact: true }).fill(upstreamName);
+
+    // Add a node
+    const nodesSection = page.getByRole('group', { name: 'Nodes' });
+    const addNodeBtn = page.getByRole('button', { name: 'Add a Node' });
+
+    await addNodeBtn.click();
+    const rows = nodesSection.locator('tr.ant-table-row');
+    const firstRow = rows.first();
+    await expect(firstRow).toBeVisible();
+
+    const hostInput = firstRow.locator('input').first();
+    await hostInput.click();
+    await hostInput.fill('my-service.svc');
+
+    // Click outside to trigger update
+    await nodesSection.click();
+
+    // Set pass_host to "rewrite"
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    await passHostSection.getByRole('textbox', { name: 'Pass Host' }).click();
+    await page.getByRole('option', { name: 'rewrite' }).click();
+
+    // Fill upstream_host (required when pass_host is "rewrite")
+    await page.getByLabel('Upstream Host').fill('custom.host.example.com');
+
+    // Submit the form
+    await upstreamsPom.getAddBtn(page).click();
+    await uiHasToastMsg(page, {
+      hasText: 'Add Upstream Successfully',
+    });
+  });
+
+  await test.step('verify auto navigate to detail page', async () => {
+    await upstreamsPom.isDetailPage(page);
+  });
+
+  await test.step('verify initial values', async () => {
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('rewrite');
+    await expect(passHostField).toBeDisabled();
+
+    const upstreamHostField = page.getByLabel('Upstream Host');
+    await expect(upstreamHostField).toHaveValue('custom.host.example.com');
+  });
+
+  await test.step('edit and modify nodes', async () => {
+    await page.getByRole('button', { name: 'Edit' }).click();
+
+    const nodesSection = page.getByRole('group', { name: 'Nodes' });
+    const rows = nodesSection.locator('tr.ant-table-row');
+    const firstRow = rows.first();
+
+    // Modify the existing node's weight
+    const weightInput = firstRow.locator('input').nth(2);
+    await weightInput.click();
+    await weightInput.fill('10');
+
+    // Click outside to trigger the update
+    await nodesSection.click();
+  });
+
+  await test.step('verify values before saving', async () => {
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('rewrite');
+
+    const upstreamHostField = page.getByLabel('Upstream Host');
+    await expect(upstreamHostField).toHaveValue('custom.host.example.com');
+  });
+
+  await test.step('save and verify values are preserved', async () => {
+    const saveBtn = page.getByRole('button', { name: 'Save' });
+    await saveBtn.click();
+
+    await uiHasToastMsg(page, {
+      hasText: 'Edit Upstream Successfully',
+    });
+
+    const passHostSection = page.getByRole('group', { name: 'Pass Host' });
+    const passHostField = passHostSection.getByRole('textbox', {
+      name: 'Pass Host',
+      exact: true,
+    });
+    await expect(passHostField).toHaveValue('rewrite');
+  });
+
+  await test.step('delete upstream via UI', async () => {
+    await upstreamsPom.getUpstreamNavBtn(page).click();
+    await upstreamsPom.isIndexPage(page);
+
+    const row = page.getByRole('row', { name: upstreamName });
+    await row.getByRole('button', { name: 'Delete' }).click();
+
+    await page
+      .getByRole('dialog', { name: 'Delete Upstream' })
+      .getByRole('button', { name: 'Delete' })
+      .click();
+
+    await uiHasToastMsg(page, {
+      hasText: 'Delete Upstream Successfully',
+    });
+    await expect(page.getByRole('cell', { name: upstreamName })).toBeHidden();
+  });
+});
diff --git a/e2e/utils/ui/index.ts b/e2e/utils/ui/index.ts
index 764a78542..23c005c5c 100644
--- a/e2e/utils/ui/index.ts
+++ b/e2e/utils/ui/index.ts
@@ -54,6 +54,16 @@ export const uiHasToastMsg = async (
   await expect(alertMsg).not.toBeVisible();
 };
 
+export const uiEnsureSettingsClosed = async (page: Page) => {
+  const settingsModal = page.getByRole('dialog', { name: 'Settings' });
+  // Wait a bit for modal to potentially appear
+  await page.waitForTimeout(500);
+  if (await settingsModal.isVisible()) {
+    await settingsModal.getByRole('button', { name: 'Close' }).click();
+    await expect(settingsModal).toBeHidden();
+  }
+};
+
 export async function uiCannotSubmitEmptyForm(page: Page, pom: CommonPOM) {
   await pom.getAddBtn(page).click();
   await pom.isAddPage(page);
diff --git a/e2e/utils/ui/upstreams.ts b/e2e/utils/ui/upstreams.ts
index 21a26c44f..e97ec85ab 100644
--- a/e2e/utils/ui/upstreams.ts
+++ b/e2e/utils/ui/upstreams.ts
@@ -21,13 +21,15 @@ import type { APISIXType } from '@/types/schema/apisix';
 
 import { genTLS } from '../common';
 import type { Test } from '../test';
-import { uiFillHTTPStatuses } from '.';
+import {
+  uiEnsureSettingsClosed,
+  uiFillHTTPStatuses,
+} from '.';
 
 /**
  * Fill the upstream form with required fields
  * @param ctx - Playwright page object or locator
- * @param upstreamName - Name for the upstream
- * @param nodes - Array of upstream nodes
+ * @param upstreamName - Name
  */
 export async function uiFillUpstreamRequiredFields(
   ctx: Page | Locator,
@@ -52,10 +54,10 @@ export async function uiFillUpstreamRequiredFields(
   await firstRowHost.fill(upstream.nodes[1].host);
   await expect(firstRowHost).toHaveValue(upstream.nodes[1].host);
 
-  // Add second node - blur first, wait for useClickOutside state sync, then 
click Add
+  // Add second node - blur first to trigger sync, then click Add
   await firstRowHost.blur();
   if (page) await page.waitForTimeout(500);
-  await addNodeBtn.click();
+  await addNodeBtn.click({ force: true });
   await expect(rows).toHaveCount(2, { timeout: 10000 });
   const secondRowHost = rows.nth(1).getByRole('textbox').first();
   await secondRowHost.fill(upstream.nodes[0].host);
@@ -100,13 +102,11 @@ export async function uiCheckUpstreamRequiredFields(
 export async function uiFillUpstreamAllFields(
   test: Test,
   ctx: Page | Locator,
-  /**
-   * currently only name and desc are useful,
-   * because I dont want to change too many fields in upstreams related tests
-   */
-  upstream: Partial<APISIXType['Upstream']>,
-  page: Page = ctx as Page
+  upstream: Partial<APISIXType['Upstream']> = {},
+  page: Page = (ctx as Locator).page ? (ctx as Locator).page() : (ctx as Page)
 ) {
+  await uiEnsureSettingsClosed(page);
+
   await test.step('fill in required fields', async () => {
     // Fill in the required fields
     // 1. Name (required)
@@ -156,8 +156,8 @@ export async function uiFillUpstreamAllFields(
 
     // Add the second node - blur any focused input first, then click Add
     await priorityInput.blur();
-    await page.waitForTimeout(500);
-    await addNodeBtn.click();
+    if (page) await page.waitForTimeout(500);
+    await addNodeBtn.click({ force: true });
     await expect(rows).toHaveCount(2, { timeout: 10000 });
 
     // Fill in the Host for the second node - click first then fill
@@ -246,15 +246,17 @@ export async function uiFillUpstreamAllFields(
     await tlsSection
       .getByRole('textbox', { name: 'Client Key', exact: true })
       .fill(tls.key);
-    await tlsSection.getByRole('switch', { name: 'Verify' }).click();
+    await tlsSection.getByText('Verify').scrollIntoViewIfNeeded();
+    await tlsSection.locator('.mantine-Switch-track').click({ force: true });
 
     // 12. Health Check settings
     // Activate active health check
     const healthCheckSection = ctx.getByRole('group', {
       name: 'Health Check',
     });
-    const checksEnabled = ctx.getByTestId('checksEnabled').locator('..');
-    await checksEnabled.click();
+    const checksEnabled = 
ctx.getByTestId('checksEnabled').locator('..').locator('.mantine-Switch-track');
+    await checksEnabled.scrollIntoViewIfNeeded();
+    await checksEnabled.click({ force: true });
 
     // Set the Healthy part of Active health check settings
     const activeSection = healthCheckSection.getByRole('group', {
@@ -290,11 +292,12 @@ export async function uiFillUpstreamAllFields(
       '503'
     );
 
-    // Activate passive health check
-    await healthCheckSection
+    const checksPassiveEnabled = healthCheckSection
       .getByTestId('checksPassiveEnabled')
       .locator('..')
-      .click();
+      .locator('.mantine-Switch-track');
+    await checksPassiveEnabled.scrollIntoViewIfNeeded();
+    await checksPassiveEnabled.click({ force: true });
 
     // Set the Healthy part of Passive health check settings
     const passiveSection = healthCheckSection.getByRole('group', {
diff --git a/src/apis/routes.ts b/src/apis/routes.ts
index cb99f7177..50b4c4559 100644
--- a/src/apis/routes.ts
+++ b/src/apis/routes.ts
@@ -16,7 +16,6 @@
  */
 import type { AxiosInstance } from 'axios';
 
-import type { RoutePostType } from 
'@/components/form-slice/FormPartRoute/schema';
 import { API_ROUTES, PAGE_SIZE_MAX, PAGE_SIZE_MIN } from '@/config/constant';
 import type { APISIXType } from '@/types/schema/apisix';
 import type { PageSearchType } from '@/types/schema/pageSearch';
@@ -45,7 +44,7 @@ export const putRouteReq = (req: AxiosInstance, data: 
APISIXType['Route']) => {
   );
 };
 
-export const postRouteReq = (req: AxiosInstance, data: RoutePostType) =>
+export const postRouteReq = (req: AxiosInstance, data: APISIXType['Route']) =>
   req.post<unknown, APISIXType['RespRouteDetail']>(API_ROUTES, data);
 
 export const deleteAllRoutes = async (req: AxiosInstance) => {
diff --git a/src/components/form-slice/FormPartRoute/util.ts 
b/src/components/form-slice/FormPartRoute/util.ts
index 951880333..ef2439aa2 100644
--- a/src/components/form-slice/FormPartRoute/util.ts
+++ b/src/components/form-slice/FormPartRoute/util.ts
@@ -30,9 +30,9 @@ export const produceVarsToForm = produce((draft: 
RoutePostType) => {
 
 export const produceVarsToAPI = produce((draft: RoutePostType) => {
   if (draft.vars && typeof draft.vars === 'string') {
-    draft.vars = JSON.parse(draft.vars);
+    (draft as RoutePostType & { vars: unknown }).vars = JSON.parse(draft.vars);
   }
-});
+}) as (draft: RoutePostType) => Omit<RoutePostType, 'vars'> & { vars?: 
unknown[] };
 
 export const produceRoute = pipeProduce(
   produceRmUpstreamWhenHas('service_id', 'upstream_id'),
diff --git a/src/components/form-slice/FormPartUpstream/FormItemNodes.tsx 
b/src/components/form-slice/FormPartUpstream/FormItemNodes.tsx
index 0bb13c13f..6f91fb60c 100644
--- a/src/components/form-slice/FormPartUpstream/FormItemNodes.tsx
+++ b/src/components/form-slice/FormPartUpstream/FormItemNodes.tsx
@@ -16,7 +16,6 @@
  */
 import { EditableProTable, type ProColumns } from '@ant-design/pro-components';
 import { Button, InputWrapper, type InputWrapperProps } from '@mantine/core';
-import { useClickOutside } from '@mantine/hooks';
 import { toJS } from 'mobx';
 import { useLocalObservable } from 'mobx-react-lite';
 import { nanoid } from 'nanoid';
@@ -54,9 +53,10 @@ const zValidateField = <T extends ZodRawShape, R extends 
keyof T>(
 
 const genRecord = (data?: DataSource | APISIXType['UpstreamNode']) => {
   const d = data || zGetDefault(APISIX.UpstreamNode);
+  const id = (d as DataSource).id || nanoid();
   return {
-    id: nanoid(),
     ...d,
+    id,
   } as DataSource;
 };
 
@@ -181,8 +181,9 @@ export const FormItemNodes = <T extends FieldValues>(
     },
     values: [] as DataSource[],
     setValues(data: DataSource[]) {
-      if (equals(toJS(this.values), data)) return;
+      if (equals(parseToUpstreamNodes(toJS(this.values)), 
parseToUpstreamNodes(data))) return;
       this.values = data;
+      this.save();
     },
     append(data: DataSource) {
       this.values.push(data);
@@ -195,6 +196,11 @@ export const FormItemNodes = <T extends FieldValues>(
     get editableKeys() {
       return this.disabled ? [] : this.values.map((item) => item.id);
     },
+    save() {
+      const vals = parseToUpstreamNodes(toJS(this.values));
+      fOnChange?.(vals);
+      restProps.onChange?.(vals);
+    },
   }));
   useEffect(() => {
     ob.setValues(parseToDataSource(value));
@@ -203,11 +209,7 @@ export const FormItemNodes = <T extends FieldValues>(
     ob.setDisabled(disabled);
   }, [disabled, ob]);
 
-  const ref = useClickOutside(() => {
-    const vals = parseToUpstreamNodes(toJS(ob.values));
-    fOnChange?.(vals);
-    restProps.onChange?.(vals);
-  }, ['mouseup', 'touchend', 'mousedown', 'touchstart']);
+
 
   return (
     <InputWrapper
@@ -215,7 +217,6 @@ export const FormItemNodes = <T extends FieldValues>(
       label={label}
       required={required}
       withAsterisk={withAsterisk}
-      ref={ref}
     >
       <input name={fName} type="hidden" />
       <AntdConfigProvider>
@@ -240,7 +241,10 @@ export const FormItemNodes = <T extends FieldValues>(
                   variant="transparent"
                   size="compact-xs"
                   px={0}
-                  onClick={() => ob.remove(row.id)}
+                  onClick={() => {
+                    ob.remove(row.id);
+                    ob.save();
+                  }}
                 >
                   {t('form.btn.delete')}
                 </Button>,
@@ -256,7 +260,10 @@ export const FormItemNodes = <T extends FieldValues>(
         size="xs"
         color="cyan"
         style={{ borderColor: 'whitesmoke' }}
-        onClick={() => ob.append(genRecord())}
+        onClick={() => {
+          ob.append(genRecord());
+          ob.save();
+        }}
         {...(disabled && { display: 'none' })}
       >
         {t('form.upstreams.nodes.add')}
diff --git a/src/components/form-slice/FormPartUpstream/schema.ts 
b/src/components/form-slice/FormPartUpstream/schema.ts
index 1e9c62692..7b962222f 100644
--- a/src/components/form-slice/FormPartUpstream/schema.ts
+++ b/src/components/form-slice/FormPartUpstream/schema.ts
@@ -20,8 +20,8 @@ import { APISIX } from '@/types/schema/apisix';
 
 // We don't omit id now, as we need it for detail view
 export const FormPartUpstreamSchema = APISIX.Upstream.extend({
-  __checksEnabled: z.boolean().optional().default(false),
-  __checksPassiveEnabled: z.boolean().optional().default(false),
+  __checksEnabled: z.boolean().default(false),
+  __checksPassiveEnabled: z.boolean().default(false),
 });
 
 export type FormPartUpstreamType = z.infer<typeof FormPartUpstreamSchema>;
diff --git a/src/components/form-slice/FormPartUpstream/util.ts 
b/src/components/form-slice/FormPartUpstream/util.ts
index f4bf2b0f9..7229e5749 100644
--- a/src/components/form-slice/FormPartUpstream/util.ts
+++ b/src/components/form-slice/FormPartUpstream/util.ts
@@ -31,6 +31,22 @@ export const produceToUpstreamForm = (
     d.__checksPassiveEnabled =
       !!upstream.checks?.passive && isNotEmpty(upstream.checks.passive);
   });
+export const produceToNestedUpstreamForm = produce((draft: Record<string, 
unknown>) => {
+  const d = draft as Record<string, unknown> & { 
+    upstream?: Record<string, unknown>;
+    checks?: { passive?: unknown };
+    __checksEnabled?: boolean;
+    __checksPassiveEnabled?: boolean;
+  };
+  if (d.upstream && typeof d.upstream === 'object' && 
!Array.isArray(d.upstream)) {
+    d.upstream = produceToUpstreamForm(d.upstream, d.upstream) as 
Record<string, unknown>;
+  }
+  // Also handle top-level checks if they exist
+  if (d.checks) {
+    d.__checksEnabled = !!d.checks && isNotEmpty(d.checks);
+    d.__checksPassiveEnabled = !!d.checks?.passive && 
isNotEmpty(d.checks.passive);
+  }
+});
 
 const isAllUndefined = (obj: Record<string, unknown>) =>
   Object.values(obj).every(
diff --git a/src/components/form/Editor.tsx b/src/components/form/Editor.tsx
index f621ca313..4c6fb47c2 100644
--- a/src/components/form/Editor.tsx
+++ b/src/components/form/Editor.tsx
@@ -163,7 +163,7 @@ export const FormItemEditor = <T extends FieldValues>(
           trigger(props.name);
         }}
         onMount={(editor) => {
-          if (process.env.NODE_ENV === 'test') {
+          if (process.env.NODE_ENV !== 'production') {
             window.__monacoEditor__ = editor;
           }
         }}
diff --git a/src/routes/routes/add.tsx b/src/routes/routes/add.tsx
index ac26e94da..47d37a26b 100644
--- a/src/routes/routes/add.tsx
+++ b/src/routes/routes/add.tsx
@@ -29,10 +29,12 @@ import {
   type RoutePostType,
 } from '@/components/form-slice/FormPartRoute/schema';
 import { produceRoute } from '@/components/form-slice/FormPartRoute/util';
+import { produceRmEmptyUpstreamFields } from 
'@/components/form-slice/FormPartUpstream/util';
 import { FormTOCBox } from '@/components/form-slice/FormSection';
 import PageHeader from '@/components/page/PageHeader';
 import { req } from '@/config/req';
 import type { APISIXType } from '@/types/schema/apisix';
+import { pipeProduce } from '@/utils/producer';
 
 type Props = {
   navigate: (res: APISIXType['RespRouteDetail']) => Promise<void>;
@@ -44,7 +46,14 @@ export const RouteAddForm = (props: Props) => {
   const { t } = useTranslation();
 
   const postRoute = useMutation({
-    mutationFn: (d: RoutePostType) => postRouteReq(req, produceRoute(d)),
+    mutationFn: (d: RoutePostType) =>
+      postRouteReq(
+        req,
+        pipeProduce(
+          produceRmEmptyUpstreamFields,
+          produceRoute
+        )(d) as APISIXType['Route']
+      ),
     async onSuccess(res) {
       notifications.show({
         message: t('info.add.success', { name: t('routes.singular') }),
diff --git a/src/routes/routes/detail.$id.tsx b/src/routes/routes/detail.$id.tsx
index 48f124783..7fc8856f3 100644
--- a/src/routes/routes/detail.$id.tsx
+++ b/src/routes/routes/detail.$id.tsx
@@ -17,13 +17,13 @@
 import { zodResolver } from '@hookform/resolvers/zod';
 import { Button, Group, Skeleton } from '@mantine/core';
 import { notifications } from '@mantine/notifications';
-import { useMutation, useQuery } from '@tanstack/react-query';
+import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
 import {
   createFileRoute,
   useNavigate,
   useParams,
 } from '@tanstack/react-router';
-import { useEffect, useMemo } from 'react';
+import { Suspense, useEffect, useMemo } from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useBoolean } from 'react-use';
@@ -40,7 +40,11 @@ import {
   produceRoute,
   produceVarsToForm,
 } from '@/components/form-slice/FormPartRoute/util';
-import { produceToUpstreamForm } from 
'@/components/form-slice/FormPartUpstream/util';
+import {
+  produceRmEmptyUpstreamFields,
+  produceToNestedUpstreamForm,
+  produceToUpstreamForm,
+} from '@/components/form-slice/FormPartUpstream/util';
 import { FormTOCBox } from '@/components/form-slice/FormSection';
 import { FormSectionGeneral } from 
'@/components/form-slice/FormSectionGeneral';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
@@ -48,6 +52,7 @@ import PageHeader from '@/components/page/PageHeader';
 import { API_ROUTES } from '@/config/constant';
 import { req } from '@/config/req';
 import { type APISIXType } from '@/types/schema/apisix';
+import { pipeProduce } from '@/utils/producer';
 
 type Props = {
   readOnly: boolean;
@@ -59,18 +64,16 @@ const RouteDetailForm = (props: Props) => {
   const { readOnly, setReadOnly, id } = props;
   const { t } = useTranslation();
 
-  const routeQuery = useQuery(getRouteQueryOptions(id));
-  const { data: routeData, isLoading, refetch } = routeQuery;
+  const routeQuery = useSuspenseQuery(getRouteQueryOptions(id));
+  const { data: routeData, refetch } = routeQuery;
 
-  const formDefaults = useMemo(() => {
-    if (routeData?.value) {
-      const upstreamProduced = produceToUpstreamForm(
-        routeData.value.upstream || {},
-        routeData.value
-      );
-      return produceVarsToForm(upstreamProduced);
-    }
-  }, [routeData]);
+  const formDefaults = useMemo(
+    () =>
+      produceVarsToForm(
+        produceToUpstreamForm(routeData.value.upstream || {}, routeData.value)
+      ),
+    [routeData.value]
+  );
 
   const form = useForm({
     resolver: zodResolver(RoutePutSchema),
@@ -78,17 +81,27 @@ const RouteDetailForm = (props: Props) => {
     shouldFocusError: true,
     mode: 'all',
     disabled: readOnly,
+    defaultValues: formDefaults,
   });
 
   useEffect(() => {
-    if (formDefaults && !isLoading) {
-      form.reset(formDefaults);
-    }
-  }, [formDefaults, form, isLoading]);
+    if (!routeData.value) return;
+
+    const upstreamProduced = produceToNestedUpstreamForm(
+      routeData.value
+    );
+    form.reset(produceVarsToForm(upstreamProduced));
+  }, [routeData, form]);
 
   const putRoute = useMutation({
     mutationFn: (d: RoutePutType) =>
-      putRouteReq(req, produceRoute(d) as APISIXType['Route']),
+      putRouteReq(
+        req,
+        pipeProduce(
+          produceRmEmptyUpstreamFields,
+          produceRoute
+        )(d) as APISIXType['Route']
+      ),
     async onSuccess() {
       notifications.show({
         message: t('info.edit.success', { name: t('routes.singular') }),
@@ -97,12 +110,15 @@ const RouteDetailForm = (props: Props) => {
       await refetch();
       setReadOnly(true);
     },
+    onError(err: Error & { response?: { data?: { error_msg?: string } } }) {
+      notifications.show({
+        title: 'Error',
+        message: err.response?.data?.error_msg || err.message || 'Failed to 
update route',
+        color: 'red',
+      });
+    },
   });
 
-  if (isLoading) {
-    return <Skeleton height={400} />;
-  }
-
   return (
     <FormProvider {...form}>
       <form onSubmit={form.handleSubmit((d) => putRoute.mutateAsync(d))}>
@@ -155,13 +171,21 @@ export const RouteDetail = (props: RouteDetailProps) => {
           ),
         })}
       />
-      <FormTOCBox>
-        <RouteDetailForm
-          readOnly={readOnly}
-          setReadOnly={setReadOnly}
-          id={id}
-        />
-      </FormTOCBox>
+      <Suspense
+        fallback={
+          <FormTOCBox>
+            <Skeleton height={400} />
+          </FormTOCBox>
+        }
+      >
+        <FormTOCBox>
+          <RouteDetailForm
+            readOnly={readOnly}
+            setReadOnly={setReadOnly}
+            id={id}
+          />
+        </FormTOCBox>
+      </Suspense>
     </>
   );
 };
diff --git a/src/routes/services/detail.$id/index.tsx 
b/src/routes/services/detail.$id/index.tsx
index 4e4a518de..d6691d5b9 100644
--- a/src/routes/services/detail.$id/index.tsx
+++ b/src/routes/services/detail.$id/index.tsx
@@ -23,7 +23,7 @@ import {
   useNavigate,
   useParams,
 } from '@tanstack/react-router';
-import { useEffect } from 'react';
+import { Suspense, useEffect } from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useBoolean } from 'react-use';
@@ -32,7 +32,10 @@ import { getServiceQueryOptions } from '@/apis/hooks';
 import { putServiceReq } from '@/apis/services';
 import { FormSubmitBtn } from '@/components/form/Btn';
 import { FormPartService } from '@/components/form-slice/FormPartService';
-import { produceRmEmptyUpstreamFields } from 
'@/components/form-slice/FormPartUpstream/util';
+import {
+  produceRmEmptyUpstreamFields,
+  produceToNestedUpstreamForm,
+} from '@/components/form-slice/FormPartUpstream/util';
 import { FormTOCBox } from '@/components/form-slice/FormSection';
 import { FormSectionGeneral } from 
'@/components/form-slice/FormSectionGeneral';
 import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
@@ -54,7 +57,7 @@ const ServiceDetailForm = (props: Props) => {
   const { id } = useParams({ from: '/services/detail/$id' });
 
   const serviceQuery = useSuspenseQuery(getServiceQueryOptions(id));
-  const { data: serviceData, isLoading, refetch } = serviceQuery;
+  const { data: serviceData, refetch } = serviceQuery;
 
   const form = useForm({
     resolver: zodResolver(APISIX.Service),
@@ -62,19 +65,21 @@ const ServiceDetailForm = (props: Props) => {
     shouldFocusError: true,
     mode: 'all',
     disabled: readOnly,
+    defaultValues: serviceData.value,
   });
 
   useEffect(() => {
-    if (serviceData?.value && !isLoading) {
-      form.reset(serviceData.value);
-    }
-  }, [serviceData, form, isLoading]);
+    form.reset(produceToNestedUpstreamForm(serviceData.value));
+  }, [serviceData, form]);
 
   const putService = useMutation({
     mutationFn: (d: APISIXType['Service']) =>
       putServiceReq(
         req,
-        pipeProduce(produceRmUpstreamWhenHas('upstream_id'), 
produceRmEmptyUpstreamFields)(d)
+        pipeProduce(
+          produceRmUpstreamWhenHas('upstream_id'),
+          produceRmEmptyUpstreamFields
+        )(d) as APISIXType['Service']
       ),
     async onSuccess() {
       notifications.show({
@@ -86,10 +91,6 @@ const ServiceDetailForm = (props: Props) => {
     },
   });
 
-  if (isLoading) {
-    return <Skeleton height={400} />;
-  }
-
   return (
     <FormProvider {...form}>
       <form onSubmit={form.handleSubmit((d) => putService.mutateAsync(d))}>
@@ -140,9 +141,17 @@ function RouteComponent() {
           ),
         })}
       />
-      <FormTOCBox>
-        <ServiceDetailForm readOnly={readOnly} setReadOnly={setReadOnly} />
-      </FormTOCBox>
+      <Suspense
+        fallback={
+          <FormTOCBox>
+            <Skeleton height={400} />
+          </FormTOCBox>
+        }
+      >
+        <FormTOCBox>
+          <ServiceDetailForm readOnly={readOnly} setReadOnly={setReadOnly} />
+        </FormTOCBox>
+      </Suspense>
     </>
   );
 }
diff --git a/src/routes/stream_routes/detail.$id.tsx 
b/src/routes/stream_routes/detail.$id.tsx
index 4abffa63d..85a10e22e 100644
--- a/src/routes/stream_routes/detail.$id.tsx
+++ b/src/routes/stream_routes/detail.$id.tsx
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 import { zodResolver } from '@hookform/resolvers/zod';
-import { Button, Group,Skeleton } from '@mantine/core';
+import { Button, Group, Skeleton } from '@mantine/core';
 import { notifications } from '@mantine/notifications';
-import { useMutation, useQuery } from '@tanstack/react-query';
+import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
 import {
   createFileRoute,
   useNavigate,
   useParams,
 } from '@tanstack/react-router';
-import { useEffect } from 'react';
+import { Suspense, useEffect } from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useBoolean } from 'react-use';
@@ -52,8 +52,8 @@ const StreamRouteDetailForm = (props: Props) => {
   const { readOnly, setReadOnly, id } = props;
   const { t } = useTranslation();
 
-  const streamRouteQuery = useQuery(getStreamRouteQueryOptions(id));
-  const { data: streamRouteData, isLoading, refetch } = streamRouteQuery;
+  const streamRouteQuery = useSuspenseQuery(getStreamRouteQueryOptions(id));
+  const { data: streamRouteData, refetch } = streamRouteQuery;
 
   const form = useForm({
     resolver: zodResolver(APISIX.StreamRoute),
@@ -61,13 +61,12 @@ const StreamRouteDetailForm = (props: Props) => {
     shouldFocusError: true,
     mode: 'all',
     disabled: readOnly,
+    defaultValues: streamRouteData.value,
   });
 
   useEffect(() => {
-    if (streamRouteData?.value && !isLoading) {
-      form.reset(streamRouteData.value);
-    }
-  }, [streamRouteData, form, isLoading]);
+    form.reset(streamRouteData.value);
+  }, [streamRouteData, form]);
 
   const putStreamRoute = useMutation({
     mutationFn: (d: APISIXType['StreamRoute']) =>
@@ -82,10 +81,6 @@ const StreamRouteDetailForm = (props: Props) => {
     },
   });
 
-  if (isLoading) {
-    return <Skeleton height={400} />;
-  }
-
   return (
     <FormProvider {...form}>
       <form onSubmit={form.handleSubmit((d) => putStreamRoute.mutateAsync(d))}>
@@ -104,6 +99,8 @@ const StreamRouteDetailForm = (props: Props) => {
   );
 };
 
+
+
 type StreamRouteDetailProps = Pick<Props, 'id'> & {
   onDeleteSuccess: () => void;
 };
@@ -139,13 +136,21 @@ export const StreamRouteDetail = (props: 
StreamRouteDetailProps) => {
           ),
         })}
       />
-      <FormTOCBox>
-        <StreamRouteDetailForm
-          readOnly={readOnly}
-          setReadOnly={setReadOnly}
-          id={id}
-        />
-      </FormTOCBox>
+      <Suspense
+        fallback={
+          <FormTOCBox>
+            <Skeleton height={400} />
+          </FormTOCBox>
+        }
+      >
+        <FormTOCBox>
+          <StreamRouteDetailForm
+            readOnly={readOnly}
+            setReadOnly={setReadOnly}
+            id={id}
+          />
+        </FormTOCBox>
+      </Suspense>
     </>
   );
 };
diff --git a/src/routes/upstreams/add.tsx b/src/routes/upstreams/add.tsx
index 28d2150e0..b03105720 100644
--- a/src/routes/upstreams/add.tsx
+++ b/src/routes/upstreams/add.tsx
@@ -62,7 +62,11 @@ const UpstreamAddForm = () => {
 
   return (
     <FormProvider {...form}>
-      <form onSubmit={form.handleSubmit((d) => postUpstream.mutateAsync(d as 
PostUpstreamType))}>
+      <form
+        onSubmit={form.handleSubmit((d) =>
+          
postUpstream.mutateAsync(pipeProduce(produceRmEmptyUpstreamFields)(d))
+        )}
+      >
         <FormPartUpstream />
         <FormSubmitBtn>{t('form.btn.add')}</FormSubmitBtn>
       </form>
diff --git a/src/routes/upstreams/detail.$id.tsx 
b/src/routes/upstreams/detail.$id.tsx
index 9ed24dda9..1e972bc2c 100644
--- a/src/routes/upstreams/detail.$id.tsx
+++ b/src/routes/upstreams/detail.$id.tsx
@@ -27,15 +27,19 @@ import {
   useNavigate,
   useParams,
 } from '@tanstack/react-router';
-import { useEffect, useMemo } from 'react';
+import { Suspense, useEffect, useMemo } from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useBoolean } from 'react-use';
+import type { z } from 'zod';
 
 import { getUpstreamReq, putUpstreamReq } from '@/apis/upstreams';
 import { FormSubmitBtn } from '@/components/form/Btn';
 import { FormPartUpstream } from '@/components/form-slice/FormPartUpstream';
-import { FormPartUpstreamSchema } from 
'@/components/form-slice/FormPartUpstream/schema';
+import {
+  FormPartUpstreamSchema,
+  type FormPartUpstreamType,
+} from '@/components/form-slice/FormPartUpstream/schema';
 import {
   produceRmEmptyUpstreamFields,
   produceToUpstreamForm,
@@ -67,20 +71,20 @@ const UpstreamDetailForm = (
   const { t } = useTranslation();
   const {
     data: { value: upstreamData },
-    isLoading,
     refetch,
   } = useSuspenseQuery(getUpstreamQueryOptions(id));
 
   const formDefaults = useMemo(
-    () => produceToUpstreamForm(upstreamData),
+    () => produceToUpstreamForm(upstreamData) as FormPartUpstreamType,
     [upstreamData]
   );
-
-  const form = useForm({
+  type FormPartUpstreamInput = z.input<typeof FormPartUpstreamSchema>;
+  const form = useForm<FormPartUpstreamInput, unknown, FormPartUpstreamType>({
     resolver: zodResolver(FormPartUpstreamSchema),
     shouldUnregister: true,
     mode: 'all',
     disabled: readOnly,
+    defaultValues: formDefaults,
   });
 
   const putUpstream = useMutation({
@@ -110,21 +114,17 @@ const UpstreamDetailForm = (
   });
 
   useEffect(() => {
-    if (upstreamData && !isLoading) {
-      form.reset(formDefaults);
+    if (upstreamData) {
+      form.reset(produceToUpstreamForm(upstreamData));
     }
-  }, [formDefaults, form, isLoading, upstreamData]);
-
-  if (isLoading) {
-    return <Skeleton height={400} />;
-  }
+  }, [upstreamData, form]);
 
   return (
     <FormTOCBox>
       <FormProvider {...form}>
         <form
-          onSubmit={form.handleSubmit((d) => {
-            putUpstream.mutateAsync(d as APISIXType['Upstream']);
+          onSubmit={form.handleSubmit((d: FormPartUpstreamType) => {
+            
putUpstream.mutateAsync(pipeProduce(produceRmEmptyUpstreamFields)(d));
           })}
         >
           <FormSectionGeneral readOnly />
@@ -175,11 +175,13 @@ function RouteComponent() {
           ),
         })}
       />
-      <UpstreamDetailForm
-        id={id}
-        readOnly={readOnly}
-        setReadOnly={setReadOnly}
-      />
+      <Suspense fallback={<Skeleton height={400} />}>
+        <UpstreamDetailForm
+          id={id}
+          readOnly={readOnly}
+          setReadOnly={setReadOnly}
+        />
+      </Suspense>
     </>
   );
 }

Reply via email to