This is an automated email from the ASF dual-hosted git repository.
guoqqqi 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 32b559782 fix: stop deleting inline upstream name/desc/labels on
submit (#3431)
32b559782 is described below
commit 32b5597828aa4750e6b4618e348e57688ac05fa9
Author: Yuhan <[email protected]>
AuthorDate: Mon Jul 20 16:56:53 2026 +0800
fix: stop deleting inline upstream name/desc/labels on submit (#3431)
---
.../form.inline-upstream-basic-fields.spec.ts | 138 +++++++++++++++++++++
src/components/form-slice/FormPartUpstream/util.ts | 12 +-
2 files changed, 147 insertions(+), 3 deletions(-)
diff --git a/e2e/tests/regression/form.inline-upstream-basic-fields.spec.ts
b/e2e/tests/regression/form.inline-upstream-basic-fields.spec.ts
new file mode 100644
index 000000000..a42285995
--- /dev/null
+++ b/e2e/tests/regression/form.inline-upstream-basic-fields.spec.ts
@@ -0,0 +1,138 @@
+/**
+ * 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.
+ */
+
+// Regression for a data-integrity item of apache/apisix-dashboard#3417:
+// the inline-upstream form section renders editable Name / Description /
+// Labels inputs (FormPartBasic under NamePrefixProvider "upstream"), but
+// `produceRmEmptyUpstreamFields` unconditionally `delete`s
+// `upstream.name` / `upstream.desc` / `upstream.labels` from every submit
+// body. Consequences, both directions:
+// - values stored via the Admin API are silently erased by any
+// dashboard edit-save of the parent resource;
+// - values the user types into those inputs on the add page are
+// silently discarded ("successful" save, nothing stored).
+// The Admin API accepts all three fields on an inline upstream, so the
+// deletion is not an API-compatibility workaround.
+
+import { routesPom } from '@e2e/pom/routes';
+import { randomId } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiGoto, uiHasToastMsg } from '@e2e/utils/ui';
+import { uiFillUpstreamRequiredFields } from '@e2e/utils/ui/upstreams';
+import { expect } from '@playwright/test';
+
+import { deleteAllRoutes } from '@/apis/routes';
+import type { APISIXType } from '@/types/schema/apisix';
+
+const seededUpstream = {
+ name: 'inline-upstream-name',
+ desc: 'inline upstream stored via admin api',
+ labels: { env: 'prod', team: 'gateway' },
+ type: 'roundrobin',
+ nodes: { 'inline-basic.local:80': 1 },
+};
+
+test.beforeAll(async () => {
+ await deleteAllRoutes(e2eReq);
+});
+
+test.afterAll(async () => {
+ await deleteAllRoutes(e2eReq);
+});
+
+test('no-op edit-save preserves inline upstream name/desc/labels', async ({
+ page,
+}) => {
+ const name = randomId('reg-inline-basic');
+ const res = await e2eReq.put<{ value: APISIXType['Route'] }>(
+ `/routes/${name}`,
+ { name, uri: `/reg-inline-basic/${name}`, upstream: seededUpstream }
+ );
+ const id = res.data.value.id;
+
+ await uiGoto(page, '/routes/detail/$id', { id });
+ await routesPom.isDetailPage(page);
+
+ // the editable surface whose content the submit pipeline discards:
+ // the upstream section's own Name input must display the stored value
+ const upstreamSection = page.getByRole('group', {
+ name: 'Upstream',
+ exact: true,
+ });
+ await expect(
+ upstreamSection.getByLabel('Name', { exact: true })
+ ).toHaveValue(seededUpstream.name);
+
+ await page.getByRole('button', { name: 'Edit' }).click();
+ await page.getByRole('button', { name: 'Save' }).click();
+ await expect(
+ page.getByRole('alert').filter({ hasText: /success/i })
+ ).toBeVisible();
+
+ const after = await e2eReq.get<{ value: APISIXType['Route'] }>(
+ `/routes/${id}`
+ );
+ expect(after.data.value.upstream?.name).toBe(seededUpstream.name);
+ expect(after.data.value.upstream?.desc).toBe(seededUpstream.desc);
+ expect(after.data.value.upstream?.labels).toEqual(seededUpstream.labels);
+});
+
+test('add page keeps user-typed inline upstream name and desc', async ({
+ page,
+}) => {
+ const routeName = randomId('reg-inline-basic-add');
+ const typedName = 'typed-inline-upstream';
+ const typedDesc = 'typed into the add form';
+
+ await routesPom.toIndex(page);
+ await routesPom.isIndexPage(page);
+ await routesPom.getAddRouteBtn(page).click();
+ await routesPom.isAddPage(page);
+
+ await page.getByLabel('Name', { exact: true }).first().fill(routeName);
+ await page
+ .getByLabel('URI', { exact: true })
+ .fill(`/reg-inline-basic-add/${routeName}`);
+
+ const upstreamSection = page.getByRole('group', {
+ name: 'Upstream',
+ exact: true,
+ });
+ // the helper fills the upstream section's Name input and the nodes (it
+ // always adds a second node row, hence two); it does NOT fill desc
+ await uiFillUpstreamRequiredFields(upstreamSection, {
+ nodes: [
+ { host: 'inline-a.local', port: 80, weight: 100 },
+ { host: 'inline-b.local', port: 80, weight: 100 },
+ ],
+ name: typedName,
+ });
+ await upstreamSection.getByLabel('Description').fill(typedDesc);
+
+ await routesPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, { hasText: 'Add Route Successfully' });
+ await routesPom.isDetailPage(page);
+
+ const list = await e2eReq.get<{
+ list: { value: APISIXType['Route'] }[];
+ }>('/routes');
+ const created = list.data.list.find((r) => r.value.name === routeName);
+ expect(created).toBeTruthy();
+ expect(created?.value.upstream?.name).toBe(typedName);
+ expect(created?.value.upstream?.desc).toBe(typedDesc);
+});
diff --git a/src/components/form-slice/FormPartUpstream/util.ts
b/src/components/form-slice/FormPartUpstream/util.ts
index 84702863e..748118118 100644
--- a/src/components/form-slice/FormPartUpstream/util.ts
+++ b/src/components/form-slice/FormPartUpstream/util.ts
@@ -88,9 +88,15 @@ export const produceRmEmptyUpstreamFields = produce(
if (draft.upstream) {
const u = draft.upstream as Record<string, unknown>;
- delete u.name;
- delete u.desc;
- delete u.labels;
+ // only strip these when EMPTY: the Admin API rejects e.g. name: ''
+ // (minLength 1) but accepts and stores non-empty name/desc/labels
+ // on an inline upstream — unconditional deletes silently erased
+ // stored values on every edit-save and discarded form input (#3417)
+ if (!u.name) delete u.name;
+ if (!u.desc) delete u.desc;
+ if (!u.labels || Object.keys(u.labels as object).length === 0) {
+ delete u.labels;
+ }
if (draft.upstream.timeout && isAllUndefined(draft.upstream.timeout)) {
delete draft.upstream.timeout;