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 (