Skip to content

Commit f5793d2

Browse files
authored
fix(FormItem): Labels (#3174)
1 parent 2b39226 commit f5793d2

File tree

6 files changed

+320
-11
lines changed

6 files changed

+320
-11
lines changed

e2e/pom/ssls.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { uiGoto } from '@e2e/utils/ui';
18+
import { expect, type Page } from '@playwright/test';
19+
20+
const locator = {
21+
getSSLNavBtn: (page: Page) => page.getByRole('link', { name: 'SSLs' }),
22+
getAddSSLBtn: (page: Page) => page.getByRole('button', { name: 'Add SSL' }),
23+
getAddBtn: (page: Page) =>
24+
page.getByRole('button', { name: 'Add', exact: true }),
25+
};
26+
27+
const assert = {
28+
isIndexPage: async (page: Page) => {
29+
await expect(page).toHaveURL((url) => url.pathname.endsWith('/ssls'));
30+
const title = page.getByRole('heading', { name: 'SSLs' });
31+
await expect(title).toBeVisible();
32+
},
33+
isAddPage: async (page: Page) => {
34+
await expect(page).toHaveURL((url) => url.pathname.endsWith('/ssls/add'));
35+
const title = page.getByRole('heading', { name: 'Add SSL' });
36+
await expect(title).toBeVisible();
37+
},
38+
isDetailPage: async (page: Page) => {
39+
await expect(page).toHaveURL((url) =>
40+
url.pathname.includes('/ssls/detail')
41+
);
42+
const title = page.getByRole('heading', { name: 'SSL Detail' });
43+
await expect(title).toBeVisible();
44+
},
45+
};
46+
47+
const goto = {
48+
toIndex: (page: Page) => uiGoto(page, '/ssls'),
49+
toAdd: (page: Page) => uiGoto(page, '/ssls/add'),
50+
};
51+
52+
export const sslsPom = {
53+
...locator,
54+
...assert,
55+
...goto,
56+
};

e2e/tests/ssls.check-labels.spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { sslsPom } from '@e2e/pom/ssls';
18+
import { test } from '@e2e/utils/test';
19+
import { uiCheckLabels, uiFillLabels } from '@e2e/utils/ui/labels';
20+
import { expect } from '@playwright/test';
21+
22+
const testLabels = {
23+
env: 'test',
24+
version: 'v1',
25+
team: 'e2e',
26+
};
27+
28+
const additionalLabels = {
29+
stage: 'production',
30+
region: 'us-west',
31+
};
32+
33+
test('should support labels functionality in SSL forms', async ({ page }) => {
34+
await sslsPom.toIndex(page);
35+
await sslsPom.isIndexPage(page);
36+
37+
await sslsPom.getAddSSLBtn(page).click();
38+
await sslsPom.isAddPage(page);
39+
40+
await test.step('verify labels field is present and functional', async () => {
41+
// Verify Labels field is present
42+
const labelsField = page.getByRole('textbox', { name: 'Labels' });
43+
await expect(labelsField).toBeVisible();
44+
await expect(labelsField).toBeEnabled();
45+
});
46+
47+
await test.step('test adding labels functionality', async () => {
48+
// Add multiple labels
49+
await uiFillLabels(page, testLabels);
50+
51+
// Verify labels are displayed after addition
52+
await uiCheckLabels(page, testLabels);
53+
});
54+
55+
await test.step('test adding additional labels', async () => {
56+
// Add more labels to test multiple labels functionality
57+
await uiFillLabels(page, additionalLabels);
58+
59+
// Verify all labels (original + additional) are displayed
60+
const allLabels = { ...testLabels, ...additionalLabels };
61+
await uiCheckLabels(page, allLabels);
62+
});
63+
64+
await test.step('verify labels persist in form', async () => {
65+
// Fill some other fields to verify labels persist
66+
await page.getByLabel('SNI', { exact: true }).fill('test.example.com');
67+
68+
// Verify labels are still there
69+
const allLabels = { ...testLabels, ...additionalLabels };
70+
await uiCheckLabels(page, allLabels);
71+
});
72+
73+
await test.step('verify labels field behavior', async () => {
74+
// Test that labels field clears after adding a label
75+
const labelsField = page.getByRole('textbox', { name: 'Labels' });
76+
77+
// Add another label
78+
await labelsField.click();
79+
await labelsField.fill('new:label');
80+
await labelsField.press('Enter');
81+
82+
// Verify the input field is cleared after adding
83+
await expect(labelsField).toHaveValue('');
84+
85+
// Verify the new label is displayed
86+
await expect(page.getByText('new:label')).toBeVisible();
87+
});
88+
});

e2e/utils/ui/labels.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import type { Locator, Page } from '@playwright/test';
18+
import { expect } from '@playwright/test';
19+
20+
export async function uiFillLabels(
21+
ctx: Page | Locator,
22+
labels: Record<string, string>
23+
) {
24+
const labelsField = ctx.getByRole('textbox', { name: 'Labels' });
25+
await expect(labelsField).toBeEnabled();
26+
27+
for (const [key, value] of Object.entries(labels)) {
28+
const labelText = `${key}:${value}`;
29+
await labelsField.click();
30+
await labelsField.fill(labelText);
31+
await labelsField.press('Enter');
32+
33+
// Verify the label was added by checking if the input is cleared
34+
// This indicates the tag was successfully created
35+
await expect(labelsField).toHaveValue('');
36+
}
37+
}
38+
39+
export async function uiCheckLabels(
40+
ctx: Page | Locator,
41+
labels: Record<string, string>
42+
) {
43+
for (const [key, value] of Object.entries(labels)) {
44+
const labelText = `${key}:${value}`;
45+
await expect(ctx.getByText(labelText)).toBeVisible();
46+
}
47+
}
48+
49+
export async function uiAddSingleLabel(
50+
ctx: Page | Locator,
51+
key: string,
52+
value: string
53+
) {
54+
await uiFillLabels(ctx, { [key]: value });
55+
}
56+
57+
export async function uiCheckSingleLabel(
58+
ctx: Page | Locator,
59+
key: string,
60+
value: string
61+
) {
62+
await uiCheckLabels(ctx, { [key]: value });
63+
}

e2e/utils/ui/ssls.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import type { Locator, Page } from '@playwright/test';
18+
import { expect } from '@playwright/test';
19+
20+
import type { APISIXType } from '@/types/schema/apisix';
21+
22+
import { genTLS } from '../common';
23+
import { uiCheckLabels, uiFillLabels } from './labels';
24+
25+
export async function uiFillSSLRequiredFields(
26+
ctx: Page | Locator,
27+
ssl: Partial<APISIXType['SSL']>
28+
) {
29+
// Generate TLS certificate if not provided
30+
const tls = ssl.cert && ssl.key ? ssl : genTLS();
31+
32+
await ctx.getByRole('textbox', { name: 'Certificate 1' }).fill(tls.cert);
33+
await ctx.getByRole('textbox', { name: 'Private Key 1' }).fill(tls.key);
34+
if (ssl.sni) {
35+
await ctx.getByLabel('SNI', { exact: true }).fill(ssl.sni);
36+
}
37+
if (ssl.snis && ssl.snis.length > 0) {
38+
const snisField = ctx.getByRole('textbox', { name: 'SNIs' });
39+
for (const sni of ssl.snis) {
40+
await snisField.click();
41+
await snisField.fill(sni);
42+
await snisField.press('Enter');
43+
await expect(snisField).toHaveValue('');
44+
}
45+
}
46+
if (ssl.labels) {
47+
await uiFillLabels(ctx, ssl.labels);
48+
}
49+
}
50+
51+
export async function uiCheckSSLRequiredFields(
52+
ctx: Page | Locator,
53+
ssl: Partial<APISIXType['SSL']>
54+
) {
55+
const ID = ctx.getByRole('textbox', { name: 'ID', exact: true });
56+
if (await ID.isVisible()) {
57+
await expect(ID).toBeVisible();
58+
await expect(ID).toBeDisabled();
59+
}
60+
61+
const certField = ctx.getByRole('textbox', { name: 'Certificate 1' });
62+
await expect(certField).toBeVisible();
63+
if (ssl.cert) {
64+
await expect(certField).toHaveValue(ssl.cert);
65+
}
66+
67+
const keyField = ctx.getByRole('textbox', { name: 'Private Key 1' });
68+
await expect(keyField).toBeVisible();
69+
if (ssl.key) {
70+
await expect(keyField).toHaveValue(ssl.key);
71+
}
72+
73+
if (ssl.sni) {
74+
const sniField = ctx.getByLabel('SNI', { exact: true });
75+
await expect(sniField).toHaveValue(ssl.sni);
76+
await expect(sniField).toBeDisabled();
77+
}
78+
79+
if (ssl.snis && ssl.snis.length > 0) {
80+
for (const sni of ssl.snis) {
81+
await expect(ctx.getByText(sni)).toBeVisible();
82+
}
83+
}
84+
85+
if (ssl.labels) {
86+
await uiCheckLabels(ctx, ssl.labels);
87+
}
88+
}

src/apis/ssls.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,22 @@ export const putSSLReq = (req: AxiosInstance, data: APISIXType['SSL']) => {
4343

4444
export const postSSLReq = (req: AxiosInstance, data: SSLPostType) =>
4545
req.post<APISIXType['SSL'], APISIXType['RespSSLDetail']>(API_SSLS, data);
46+
47+
export const deleteAllSSLs = async (req: AxiosInstance) => {
48+
const { PAGE_SIZE_MIN, PAGE_SIZE_MAX } = await import('@/config/constant');
49+
const totalRes = await getSSLListReq(req, {
50+
page: 1,
51+
page_size: PAGE_SIZE_MIN,
52+
});
53+
const total = totalRes.total;
54+
if (total === 0) return;
55+
for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) {
56+
const res = await getSSLListReq(req, {
57+
page: 1,
58+
page_size: PAGE_SIZE_MAX,
59+
});
60+
await Promise.all(
61+
res.list.map((d) => req.delete(`${API_SSLS}/${d.value.id}`))
62+
);
63+
}
64+
};

src/components/form/Labels.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
* limitations under the License.
1616
*/
1717
import { TagsInput, type TagsInputProps } from '@mantine/core';
18-
import { useListState } from '@mantine/hooks';
19-
import { useCallback, useState } from 'react';
18+
import { useCallback, useMemo, useState } from 'react';
2019
import {
2120
type FieldValues,
2221
useController,
2322
type UseControllerProps,
2423
} from 'react-hook-form';
2524
import { useTranslation } from 'react-i18next';
26-
import { useMount } from 'react-use';
2725

2826
import type { APISIXType } from '@/types/schema/apisix';
2927

@@ -44,14 +42,12 @@ export const FormItemLabels = <T extends FieldValues>(
4442
fieldState,
4543
} = useController<T>(controllerProps);
4644
const { t } = useTranslation();
47-
const [values, handle] = useListState<string>();
4845
const [internalError, setInternalError] = useState<string | null>();
4946

50-
useMount(() => {
51-
Object.entries(value || {}).forEach(([key, value]) => {
52-
handle.append(`${key}:${value}`);
53-
});
54-
});
47+
const values = useMemo(() => {
48+
if (!value) return [];
49+
return Object.entries(value).map(([key, val]) => `${key}:${val}`);
50+
}, [value]);
5551

5652
const handleSearchChange = useCallback(
5753
(val: string) => {
@@ -78,11 +74,10 @@ export const FormItemLabels = <T extends FieldValues>(
7874
obj[tuple[0]] = tuple[1];
7975
}
8076
setInternalError(null);
81-
handle.setState(vals);
8277
fOnChange(obj);
8378
restProps.onChange?.(obj);
8479
},
85-
[handle, fOnChange, restProps, t]
80+
[fOnChange, restProps, t]
8681
);
8782

8883
return (

0 commit comments

Comments
 (0)