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

young 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 f5793d230 fix(FormItem): Labels (#3174)
f5793d230 is described below

commit f5793d230e3855b54cef93d92a0a957e65166794
Author: YYYoung <isk...@outlook.com>
AuthorDate: Tue Aug 19 11:06:50 2025 +0800

    fix(FormItem): Labels (#3174)
---
 e2e/pom/ssls.ts                     | 56 +++++++++++++++++++++++
 e2e/tests/ssls.check-labels.spec.ts | 88 +++++++++++++++++++++++++++++++++++++
 e2e/utils/ui/labels.ts              | 63 ++++++++++++++++++++++++++
 e2e/utils/ui/ssls.ts                | 88 +++++++++++++++++++++++++++++++++++++
 src/apis/ssls.ts                    | 19 ++++++++
 src/components/form/Labels.tsx      | 17 +++----
 6 files changed, 320 insertions(+), 11 deletions(-)

diff --git a/e2e/pom/ssls.ts b/e2e/pom/ssls.ts
new file mode 100644
index 000000000..821c68b53
--- /dev/null
+++ b/e2e/pom/ssls.ts
@@ -0,0 +1,56 @@
+/**
+ * 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 { uiGoto } from '@e2e/utils/ui';
+import { expect, type Page } from '@playwright/test';
+
+const locator = {
+  getSSLNavBtn: (page: Page) => page.getByRole('link', { name: 'SSLs' }),
+  getAddSSLBtn: (page: Page) => page.getByRole('button', { name: 'Add SSL' }),
+  getAddBtn: (page: Page) =>
+    page.getByRole('button', { name: 'Add', exact: true }),
+};
+
+const assert = {
+  isIndexPage: async (page: Page) => {
+    await expect(page).toHaveURL((url) => url.pathname.endsWith('/ssls'));
+    const title = page.getByRole('heading', { name: 'SSLs' });
+    await expect(title).toBeVisible();
+  },
+  isAddPage: async (page: Page) => {
+    await expect(page).toHaveURL((url) => url.pathname.endsWith('/ssls/add'));
+    const title = page.getByRole('heading', { name: 'Add SSL' });
+    await expect(title).toBeVisible();
+  },
+  isDetailPage: async (page: Page) => {
+    await expect(page).toHaveURL((url) =>
+      url.pathname.includes('/ssls/detail')
+    );
+    const title = page.getByRole('heading', { name: 'SSL Detail' });
+    await expect(title).toBeVisible();
+  },
+};
+
+const goto = {
+  toIndex: (page: Page) => uiGoto(page, '/ssls'),
+  toAdd: (page: Page) => uiGoto(page, '/ssls/add'),
+};
+
+export const sslsPom = {
+  ...locator,
+  ...assert,
+  ...goto,
+};
diff --git a/e2e/tests/ssls.check-labels.spec.ts 
b/e2e/tests/ssls.check-labels.spec.ts
new file mode 100644
index 000000000..debb626c5
--- /dev/null
+++ b/e2e/tests/ssls.check-labels.spec.ts
@@ -0,0 +1,88 @@
+/**
+ * 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 { sslsPom } from '@e2e/pom/ssls';
+import { test } from '@e2e/utils/test';
+import { uiCheckLabels, uiFillLabels } from '@e2e/utils/ui/labels';
+import { expect } from '@playwright/test';
+
+const testLabels = {
+  env: 'test',
+  version: 'v1',
+  team: 'e2e',
+};
+
+const additionalLabels = {
+  stage: 'production',
+  region: 'us-west',
+};
+
+test('should support labels functionality in SSL forms', async ({ page }) => {
+  await sslsPom.toIndex(page);
+  await sslsPom.isIndexPage(page);
+
+  await sslsPom.getAddSSLBtn(page).click();
+  await sslsPom.isAddPage(page);
+
+  await test.step('verify labels field is present and functional', async () => 
{
+    // Verify Labels field is present
+    const labelsField = page.getByRole('textbox', { name: 'Labels' });
+    await expect(labelsField).toBeVisible();
+    await expect(labelsField).toBeEnabled();
+  });
+
+  await test.step('test adding labels functionality', async () => {
+    // Add multiple labels
+    await uiFillLabels(page, testLabels);
+
+    // Verify labels are displayed after addition
+    await uiCheckLabels(page, testLabels);
+  });
+
+  await test.step('test adding additional labels', async () => {
+    // Add more labels to test multiple labels functionality
+    await uiFillLabels(page, additionalLabels);
+
+    // Verify all labels (original + additional) are displayed
+    const allLabels = { ...testLabels, ...additionalLabels };
+    await uiCheckLabels(page, allLabels);
+  });
+
+  await test.step('verify labels persist in form', async () => {
+    // Fill some other fields to verify labels persist
+    await page.getByLabel('SNI', { exact: true }).fill('test.example.com');
+
+    // Verify labels are still there
+    const allLabels = { ...testLabels, ...additionalLabels };
+    await uiCheckLabels(page, allLabels);
+  });
+
+  await test.step('verify labels field behavior', async () => {
+    // Test that labels field clears after adding a label
+    const labelsField = page.getByRole('textbox', { name: 'Labels' });
+
+    // Add another label
+    await labelsField.click();
+    await labelsField.fill('new:label');
+    await labelsField.press('Enter');
+
+    // Verify the input field is cleared after adding
+    await expect(labelsField).toHaveValue('');
+
+    // Verify the new label is displayed
+    await expect(page.getByText('new:label')).toBeVisible();
+  });
+});
diff --git a/e2e/utils/ui/labels.ts b/e2e/utils/ui/labels.ts
new file mode 100644
index 000000000..79e711843
--- /dev/null
+++ b/e2e/utils/ui/labels.ts
@@ -0,0 +1,63 @@
+/**
+ * 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 type { Locator, Page } from '@playwright/test';
+import { expect } from '@playwright/test';
+
+export async function uiFillLabels(
+  ctx: Page | Locator,
+  labels: Record<string, string>
+) {
+  const labelsField = ctx.getByRole('textbox', { name: 'Labels' });
+  await expect(labelsField).toBeEnabled();
+
+  for (const [key, value] of Object.entries(labels)) {
+    const labelText = `${key}:${value}`;
+    await labelsField.click();
+    await labelsField.fill(labelText);
+    await labelsField.press('Enter');
+
+    // Verify the label was added by checking if the input is cleared
+    // This indicates the tag was successfully created
+    await expect(labelsField).toHaveValue('');
+  }
+}
+
+export async function uiCheckLabels(
+  ctx: Page | Locator,
+  labels: Record<string, string>
+) {
+  for (const [key, value] of Object.entries(labels)) {
+    const labelText = `${key}:${value}`;
+    await expect(ctx.getByText(labelText)).toBeVisible();
+  }
+}
+
+export async function uiAddSingleLabel(
+  ctx: Page | Locator,
+  key: string,
+  value: string
+) {
+  await uiFillLabels(ctx, { [key]: value });
+}
+
+export async function uiCheckSingleLabel(
+  ctx: Page | Locator,
+  key: string,
+  value: string
+) {
+  await uiCheckLabels(ctx, { [key]: value });
+}
diff --git a/e2e/utils/ui/ssls.ts b/e2e/utils/ui/ssls.ts
new file mode 100644
index 000000000..73ed28c1a
--- /dev/null
+++ b/e2e/utils/ui/ssls.ts
@@ -0,0 +1,88 @@
+/**
+ * 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 type { Locator, Page } from '@playwright/test';
+import { expect } from '@playwright/test';
+
+import type { APISIXType } from '@/types/schema/apisix';
+
+import { genTLS } from '../common';
+import { uiCheckLabels, uiFillLabels } from './labels';
+
+export async function uiFillSSLRequiredFields(
+  ctx: Page | Locator,
+  ssl: Partial<APISIXType['SSL']>
+) {
+  // Generate TLS certificate if not provided
+  const tls = ssl.cert && ssl.key ? ssl : genTLS();
+
+  await ctx.getByRole('textbox', { name: 'Certificate 1' }).fill(tls.cert);
+  await ctx.getByRole('textbox', { name: 'Private Key 1' }).fill(tls.key);
+  if (ssl.sni) {
+    await ctx.getByLabel('SNI', { exact: true }).fill(ssl.sni);
+  }
+  if (ssl.snis && ssl.snis.length > 0) {
+    const snisField = ctx.getByRole('textbox', { name: 'SNIs' });
+    for (const sni of ssl.snis) {
+      await snisField.click();
+      await snisField.fill(sni);
+      await snisField.press('Enter');
+      await expect(snisField).toHaveValue('');
+    }
+  }
+  if (ssl.labels) {
+    await uiFillLabels(ctx, ssl.labels);
+  }
+}
+
+export async function uiCheckSSLRequiredFields(
+  ctx: Page | Locator,
+  ssl: Partial<APISIXType['SSL']>
+) {
+  const ID = ctx.getByRole('textbox', { name: 'ID', exact: true });
+  if (await ID.isVisible()) {
+    await expect(ID).toBeVisible();
+    await expect(ID).toBeDisabled();
+  }
+
+  const certField = ctx.getByRole('textbox', { name: 'Certificate 1' });
+  await expect(certField).toBeVisible();
+  if (ssl.cert) {
+    await expect(certField).toHaveValue(ssl.cert);
+  }
+
+  const keyField = ctx.getByRole('textbox', { name: 'Private Key 1' });
+  await expect(keyField).toBeVisible();
+  if (ssl.key) {
+    await expect(keyField).toHaveValue(ssl.key);
+  }
+
+  if (ssl.sni) {
+    const sniField = ctx.getByLabel('SNI', { exact: true });
+    await expect(sniField).toHaveValue(ssl.sni);
+    await expect(sniField).toBeDisabled();
+  }
+
+  if (ssl.snis && ssl.snis.length > 0) {
+    for (const sni of ssl.snis) {
+      await expect(ctx.getByText(sni)).toBeVisible();
+    }
+  }
+
+  if (ssl.labels) {
+    await uiCheckLabels(ctx, ssl.labels);
+  }
+}
diff --git a/src/apis/ssls.ts b/src/apis/ssls.ts
index 4499fd8ea..6cc7322da 100644
--- a/src/apis/ssls.ts
+++ b/src/apis/ssls.ts
@@ -43,3 +43,22 @@ export const putSSLReq = (req: AxiosInstance, data: 
APISIXType['SSL']) => {
 
 export const postSSLReq = (req: AxiosInstance, data: SSLPostType) =>
   req.post<APISIXType['SSL'], APISIXType['RespSSLDetail']>(API_SSLS, data);
+
+export const deleteAllSSLs = async (req: AxiosInstance) => {
+  const { PAGE_SIZE_MIN, PAGE_SIZE_MAX } = await import('@/config/constant');
+  const totalRes = await getSSLListReq(req, {
+    page: 1,
+    page_size: PAGE_SIZE_MIN,
+  });
+  const total = totalRes.total;
+  if (total === 0) return;
+  for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) {
+    const res = await getSSLListReq(req, {
+      page: 1,
+      page_size: PAGE_SIZE_MAX,
+    });
+    await Promise.all(
+      res.list.map((d) => req.delete(`${API_SSLS}/${d.value.id}`))
+    );
+  }
+};
diff --git a/src/components/form/Labels.tsx b/src/components/form/Labels.tsx
index c9eecafd2..bf05fc57d 100644
--- a/src/components/form/Labels.tsx
+++ b/src/components/form/Labels.tsx
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 import { TagsInput, type TagsInputProps } from '@mantine/core';
-import { useListState } from '@mantine/hooks';
-import { useCallback, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import {
   type FieldValues,
   useController,
   type UseControllerProps,
 } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
-import { useMount } from 'react-use';
 
 import type { APISIXType } from '@/types/schema/apisix';
 
@@ -44,14 +42,12 @@ export const FormItemLabels = <T extends FieldValues>(
     fieldState,
   } = useController<T>(controllerProps);
   const { t } = useTranslation();
-  const [values, handle] = useListState<string>();
   const [internalError, setInternalError] = useState<string | null>();
 
-  useMount(() => {
-    Object.entries(value || {}).forEach(([key, value]) => {
-      handle.append(`${key}:${value}`);
-    });
-  });
+  const values = useMemo(() => {
+    if (!value) return [];
+    return Object.entries(value).map(([key, val]) => `${key}:${val}`);
+  }, [value]);
 
   const handleSearchChange = useCallback(
     (val: string) => {
@@ -78,11 +74,10 @@ export const FormItemLabels = <T extends FieldValues>(
         obj[tuple[0]] = tuple[1];
       }
       setInternalError(null);
-      handle.setState(vals);
       fOnChange(obj);
       restProps.onChange?.(obj);
     },
-    [handle, fOnChange, restProps, t]
+    [fOnChange, restProps, t]
   );
 
   return (

Reply via email to