Skip to content

Commit d1f63e5

Browse files
authored
fix: support edit and show vars (#3152)
1 parent 3487911 commit d1f63e5

File tree

12 files changed

+235
-117
lines changed

12 files changed

+235
-117
lines changed

e2e/tests/hot-path.upstream-service-route.spec.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { upstreamsPom } from '@e2e/pom/upstreams';
2020
import { randomId } from '@e2e/utils/common';
2121
import { e2eReq } from '@e2e/utils/req';
2222
import { test } from '@e2e/utils/test';
23-
import { uiClearEditor, uiHasToastMsg } from '@e2e/utils/ui';
23+
import {
24+
uiFillMonacoEditor,
25+
uiGetMonacoEditor,
26+
uiHasToastMsg,
27+
} from '@e2e/utils/ui';
2428
import { expect } from '@playwright/test';
2529

2630
import { deleteAllRoutes } from '@/apis/routes';
@@ -139,7 +143,7 @@ test('can create upstream -> service -> route', async ({ page }) => {
139143
* Plugins: Enable limit-count with custom configuration
140144
*/
141145
const servicePluginName = 'limit-count';
142-
const service: Partial<APISIXType['Service']> = {
146+
const service = {
143147
// will be set in test
144148
id: undefined,
145149
name: randomId('HTTPBIN Service'),
@@ -150,9 +154,10 @@ test('can create upstream -> service -> route', async ({ page }) => {
150154
time_window: 60,
151155
rejected_code: 429,
152156
key: 'remote_addr',
157+
policy: 'local',
153158
},
154159
},
155-
};
160+
} satisfies Partial<APISIXType['Service']>;
156161
await test.step('create service', async () => {
157162
// upstream id should be set
158163
expect(service.upstream_id).not.toBeUndefined();
@@ -197,15 +202,14 @@ test('can create upstream -> service -> route', async ({ page }) => {
197202

198203
// Configure the plugin
199204
const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
200-
const editorLoading = addPluginDialog.getByTestId('editor-loading');
201-
await expect(editorLoading).toBeHidden();
202-
203-
// Clear the editor and add custom configuration
204-
const editor = addPluginDialog.getByRole('code').getByRole('textbox');
205-
await uiClearEditor(page);
205+
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
206206

207207
// Add plugin configuration
208-
await editor.fill(JSON.stringify(service.plugins?.[servicePluginName]));
208+
await uiFillMonacoEditor(
209+
page,
210+
pluginEditor,
211+
JSON.stringify(service.plugins?.[servicePluginName])
212+
);
209213

210214
// Add the plugin
211215
await addPluginDialog.getByRole('button', { name: 'Add' }).click();
@@ -311,15 +315,14 @@ test('can create upstream -> service -> route', async ({ page }) => {
311315

312316
// Configure the plugin
313317
const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
314-
const editorLoading = addPluginDialog.getByTestId('editor-loading');
315-
await expect(editorLoading).toBeHidden();
316-
317-
// Clear the editor and add custom configuration
318-
const editor = addPluginDialog.getByRole('code').getByRole('textbox');
319-
await uiClearEditor(page);
318+
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
320319

321320
// Add plugin configuration
322-
await editor.fill(JSON.stringify(route.plugins?.[routePluginName]));
321+
await uiFillMonacoEditor(
322+
page,
323+
pluginEditor,
324+
JSON.stringify(route.plugins?.[routePluginName])
325+
);
323326

324327
// Add the plugin
325328
await addPluginDialog.getByRole('button', { name: 'Add' }).click();

e2e/tests/routes.crud-all-fields.spec.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import { routesPom } from '@e2e/pom/routes';
1818
import { randomId } from '@e2e/utils/common';
1919
import { e2eReq } from '@e2e/utils/req';
2020
import { test } from '@e2e/utils/test';
21-
import { uiClearEditor, uiHasToastMsg } from '@e2e/utils/ui';
21+
import {
22+
uiClearMonacoEditor,
23+
uiFillMonacoEditor,
24+
uiGetMonacoEditor,
25+
uiHasToastMsg,
26+
} from '@e2e/utils/ui';
2227
import { uiFillUpstreamAllFields } from '@e2e/utils/ui/upstreams';
2328
import { expect } from '@playwright/test';
2429

@@ -33,13 +38,18 @@ const nodes: APISIXType['UpstreamNode'][] = [
3338
{ host: 'test.com', port: 80, weight: 100 },
3439
{ host: 'test2.com', port: 80, weight: 100 },
3540
];
41+
// Define vars values for testing
42+
const initialVars = '[["arg_name", "==", "json"], ["arg_age", ">", 18]]';
43+
const updatedVars = '[["arg_name", "==", "updated"], ["arg_age", ">", 21]]';
3644

3745
test.beforeAll(async () => {
3846
await deleteAllRoutes(e2eReq);
3947
});
4048

4149
test('should CRUD route with all fields', async ({ page }) => {
42-
test.setTimeout(30000);
50+
test.slow();
51+
52+
const varsSection = page.getByText('Vars').locator('..');
4353

4454
// Navigate to the route list page
4555
await routesPom.toIndex(page);
@@ -85,6 +95,10 @@ test('should CRUD route with all fields', async ({ page }) => {
8595
await page.getByRole('option', { name: 'Disabled' }).click();
8696
await expect(status).toHaveValue('Disabled');
8797

98+
// Fill in Vars field
99+
const varsEditor = await uiGetMonacoEditor(page, varsSection);
100+
await uiFillMonacoEditor(page, varsEditor, initialVars);
101+
88102
// Add upstream nodes
89103
const upstreamSection = page.getByRole('group', {
90104
name: 'Upstream',
@@ -120,11 +134,8 @@ test('should CRUD route with all fields', async ({ page }) => {
120134
.click();
121135

122136
const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
123-
const editorLoading = addPluginDialog.getByTestId('editor-loading');
124-
await expect(editorLoading).toBeHidden();
125-
const editor = addPluginDialog.getByRole('code').getByRole('textbox');
126-
await uiClearEditor(page);
127-
await editor.fill('{"hide_credentials": true}');
137+
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
138+
await uiFillMonacoEditor(page, pluginEditor, '{"hide_credentials": true}');
128139
// add plugin
129140
await addPluginDialog.getByRole('button', { name: 'Add' }).click();
130141
await expect(addPluginDialog).toBeHidden();
@@ -135,7 +146,6 @@ test('should CRUD route with all fields', async ({ page }) => {
135146

136147
// should show edit plugin dialog
137148
const editPluginDialog = page.getByRole('dialog', { name: 'Edit Plugin' });
138-
await expect(editorLoading).toBeHidden();
139149

140150
await expect(editPluginDialog.getByText('hide_credentials')).toBeVisible();
141151
// save edit plugin dialog
@@ -157,13 +167,12 @@ test('should CRUD route with all fields', async ({ page }) => {
157167
// real-ip need config, otherwise it will show an error
158168
await addPluginDialog.getByRole('button', { name: 'Add' }).click();
159169
await expect(addPluginDialog).toBeVisible();
160-
await expect(editorLoading).toBeHidden();
161170
await expect(
162171
addPluginDialog.getByText('Missing property "source"')
163172
).toBeVisible();
164173

165174
// clear the editor, will show JSON format is not valid
166-
await uiClearEditor(page);
175+
await uiClearMonacoEditor(page, pluginEditor);
167176
await expect(
168177
addPluginDialog.getByText('JSON format is not valid')
169178
).toBeVisible();
@@ -175,15 +184,18 @@ test('should CRUD route with all fields', async ({ page }) => {
175184
).toBeVisible();
176185

177186
// add a valid config
178-
await editor.fill('{"source": "X-Forwarded-For"}');
187+
await uiFillMonacoEditor(
188+
page,
189+
pluginEditor,
190+
'{"source": "X-Forwarded-For"}'
191+
);
179192
await addPluginDialog.getByRole('button', { name: 'Add' }).click();
180193
await expect(addPluginDialog).toBeHidden();
181194

182195
// check real-ip plugin in edit dialog
183196
const realIpPlugin = page.getByTestId('plugin-real-ip');
184197
await realIpPlugin.getByRole('button', { name: 'Edit' }).click();
185198
await expect(editPluginDialog).toBeVisible();
186-
await expect(editorLoading).toBeHidden();
187199
await expect(editPluginDialog.getByText('X-Forwarded-For')).toBeVisible();
188200
// close
189201
await editPluginDialog.getByRole('button', { name: 'Save' }).click();
@@ -249,6 +261,10 @@ test('should CRUD route with all fields', async ({ page }) => {
249261
const status = page.getByRole('textbox', { name: 'Status', exact: true });
250262
await expect(status).toHaveValue('Disabled');
251263

264+
// Verify Vars field
265+
await expect(varsSection.getByText('arg_name')).toBeVisible();
266+
await expect(varsSection.getByText('json')).toBeVisible();
267+
252268
// Verify Plugins
253269
await expect(page.getByText('basic-auth')).toBeHidden();
254270
await expect(page.getByText('real-ip')).toBeVisible();
@@ -279,6 +295,10 @@ test('should CRUD route with all fields', async ({ page }) => {
279295
// Update Priority
280296
await page.getByLabel('Priority', { exact: true }).first().fill('200');
281297

298+
// Update Vars field
299+
const varsEditor = await uiGetMonacoEditor(page, varsSection);
300+
await uiFillMonacoEditor(page, varsEditor, updatedVars);
301+
282302
// Click the Save button to save changes
283303
const saveBtn = page.getByRole('button', { name: 'Save' });
284304
await saveBtn.click();
@@ -312,6 +332,10 @@ test('should CRUD route with all fields', async ({ page }) => {
312332
page.getByLabel('Priority', { exact: true }).first()
313333
).toHaveValue('200');
314334

335+
// Verify updated Vars field
336+
await expect(varsSection.getByText('arg_name')).toBeVisible();
337+
await expect(varsSection.getByText('updated')).toBeVisible();
338+
315339
// Return to list page and verify the route exists
316340
await routesPom.getRouteNavBtn(page).click();
317341
await routesPom.isIndexPage(page);

e2e/utils/ui/index.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* limitations under the License.
1616
*/
1717
import type { CommonPOM } from '@e2e/pom/type';
18-
import { type Monaco } from '@monaco-editor/react';
1918
import { expect, type Locator, type Page } from '@playwright/test';
2019

2120
import type { FileRouteTypes } from '@/routeTree.gen';
@@ -64,10 +63,38 @@ export async function uiFillHTTPStatuses(
6463
}
6564
}
6665

67-
export async function uiClearEditor(page: Page) {
68-
await page.evaluate(() => {
69-
(window as unknown as { monaco?: Monaco })?.monaco?.editor
70-
?.getEditors()[0]
71-
?.setValue('');
72-
});
66+
export const uiClearMonacoEditor = async (page: Page, editor: Locator) => {
67+
await editor.click();
68+
await page.keyboard.press('ControlOrMeta+A');
69+
await page.keyboard.press('Backspace');
70+
await editor.blur();
71+
};
72+
73+
export const uiGetMonacoEditor = async (
74+
page: Page,
75+
parent: Locator,
76+
clear = true
77+
) => {
78+
// Wait for Monaco editor to load
79+
const editorLoading = parent.getByTestId('editor-loading');
80+
await expect(editorLoading).toBeHidden();
81+
const editor = parent.locator('.monaco-editor').first();
82+
await expect(editor).toBeVisible({ timeout: 10000 });
83+
84+
if (clear) {
85+
await uiClearMonacoEditor(page, editor);
86+
}
87+
88+
return editor;
89+
};
90+
91+
export const uiFillMonacoEditor = async (
92+
page: Page,
93+
editor: Locator,
94+
value: string
95+
) => {
96+
await editor.click();
97+
await editor.getByRole('textbox').pressSequentially(value);
98+
await editor.blur();
99+
await page.waitForTimeout(800);
73100
};

src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717
import { Drawer, Group, Title } from '@mantine/core';
18-
import { isEmpty } from 'rambdax';
18+
import { isEmpty, isNil } from 'rambdax';
1919
import { useEffect } from 'react';
2020
import { FormProvider, useForm } from 'react-hook-form';
2121
import { useTranslation } from 'react-i18next';
@@ -35,7 +35,7 @@ export type PluginEditorDrawerProps = Pick<PluginCardListProps, 'mode'> & {
3535
};
3636

3737
const toConfigStr = (p: object): string => {
38-
return !isEmpty(p) ? JSON.stringify(p, null, 2) : '{}';
38+
return !isEmpty(p) && !isNil(p) ? JSON.stringify(p, null, 2) : '{}';
3939
};
4040
export const PluginEditorDrawer = (props: PluginEditorDrawerProps) => {
4141
const { opened, onSave, onClose, plugin, mode, schema } = props;
@@ -44,7 +44,7 @@ export const PluginEditorDrawer = (props: PluginEditorDrawerProps) => {
4444
const methods = useForm<{ config: string }>({
4545
criteriaMode: 'all',
4646
disabled: mode === 'view',
47-
defaultValues: { config: toConfigStr(plugin) },
47+
defaultValues: { config: toConfigStr(config) },
4848
});
4949
const handleClose = () => {
5050
onClose();

src/components/form-slice/FormItemPlugins/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export const FormItemPlugins = <T extends FieldValues>(
180180
schema={toJS(pluginsOb.curPluginSchema)}
181181
opened={pluginsOb.editorOpened}
182182
onClose={pluginsOb.closeEditor}
183-
plugin={pluginsOb.curPlugin}
183+
plugin={toJS(pluginsOb.curPlugin)}
184184
onSave={pluginsOb.update}
185185
/>
186186
</Drawer.Stack>

src/components/form-slice/FormPartRoute/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Divider, InputWrapper } from '@mantine/core';
1818
import { useFormContext } from 'react-hook-form';
1919
import { useTranslation } from 'react-i18next';
2020

21+
import { FormItemEditor } from '@/components/form/Editor';
2122
import { FormItemNumberInput } from '@/components/form/NumberInput';
2223
import { FormItemSwitch } from '@/components/form/Switch';
2324
import { FormItemTagsInput } from '@/components/form/TagInput';
@@ -94,7 +95,7 @@ const FormSectionMatchRules = () => {
9495
name="remote_addrs"
9596
label={t('form.routes.remoteAddrs')}
9697
/>
97-
<FormItemTagsInput
98+
<FormItemEditor
9899
control={control}
99100
name="vars"
100101
label={t('form.routes.vars')}

src/components/form-slice/FormPartRoute/schema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,25 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
import type { z } from 'zod';
17+
import { z } from 'zod';
1818

1919
import { APISIX } from '@/types/schema/apisix';
2020

2121
export const RoutePostSchema = APISIX.Route.omit({
2222
id: true,
2323
create_time: true,
2424
update_time: true,
25+
}).extend({
26+
// the FormItemEditor (monaco) is for editing text,
27+
// and passing the original schema of `vars` for validation
28+
// is not in line with this usage.
29+
vars: z.string().optional(),
2530
});
2631

2732
export type RoutePostType = z.infer<typeof RoutePostSchema>;
33+
34+
export const RoutePutSchema = APISIX.Route.extend({
35+
vars: z.string().optional(),
36+
});
37+
38+
export type RoutePutType = z.infer<typeof RoutePutSchema>;

src/components/form-slice/FormPartRoute/util.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
import { produce } from 'immer';
18+
1719
import { produceRmUpstreamWhenHas } from '@/utils/form-producer';
1820
import { pipeProduce } from '@/utils/producer';
1921

22+
import type { RoutePostType, RoutePutType } from './schema';
23+
24+
export const produceVarsToForm = produce((draft: RoutePostType) => {
25+
if (draft.vars && Array.isArray(draft.vars)) {
26+
draft.vars = JSON.stringify(draft.vars);
27+
}
28+
}) as (draft: RoutePostType) => RoutePutType;
29+
30+
export const produceVarsToAPI = produce((draft: RoutePostType) => {
31+
if (draft.vars && typeof draft.vars === 'string') {
32+
draft.vars = JSON.parse(draft.vars);
33+
}
34+
});
35+
2036
export const produceRoute = pipeProduce(
21-
produceRmUpstreamWhenHas('service_id', 'upstream_id')
37+
produceRmUpstreamWhenHas('service_id', 'upstream_id'),
38+
produceVarsToAPI
2239
);

0 commit comments

Comments
 (0)