fix(gateway): decouple admin navigation loading

This commit is contained in:
2026-08-09 11:51:00 +00:00
parent fd0f874d55
commit 7361828641
9 changed files with 141 additions and 23 deletions
@@ -105,6 +105,7 @@ const installFixture = async (page: Page) => {
});
}
if (operation === 'admin.system.getNotice') return response({ notice: '' });
if (operation === 'admin.profiles.listNavigation') return response([]);
if (operation === 'admin.profiles.list') return response([]);
if (operation === 'admin.profiles.listScenarios') return response([]);
if (operation === 'admin.users.lookup') {
@@ -194,7 +195,12 @@ const installFixture = async (page: Page) => {
}
throw new Error(`Unhandled tRPC operation: ${operation}`);
});
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
const isBatch = new URL(route.request().url()).searchParams.get('batch') === '1';
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(isBatch ? results : results[0]),
});
});
return mutations;
};
@@ -145,6 +145,15 @@ const installFixture = async (
},
]);
}
if (operation === 'admin.profiles.listNavigation') {
return response([
{
profileName: 'hwe:default',
profile: 'hwe',
meta: {},
},
]);
}
if (operation === 'admin.profiles.list') {
const keepPending = requested && postRequestProfileReads++ < (options.pendingProfileReads ?? 0);
return response([
@@ -235,7 +244,9 @@ const installFixture = async (
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(results),
body: JSON.stringify(
new URL(route.request().url()).searchParams.get('batch') === '1' ? results : results[0]
),
});
});
return { releaseRequest, releaseInstall, requestBodies };
@@ -52,6 +52,19 @@ const installGatewayFixture = async (page: Page, roles: string[]) => {
: []
);
}
if (operation === 'admin.profiles.listNavigation') {
return response(
roles.some((role) => role === 'superuser' || role.includes(':hwe:2'))
? [
{
profileName: 'hwe:2',
profile: 'hwe',
meta: { korName: '환상서버' },
},
]
: []
);
}
if (operation === 'admin.releases.gatewayState') {
return response({ id: 'gateway', updatedAt: '2026-08-01T00:00:00.000Z' });
}
@@ -85,7 +98,9 @@ const installGatewayFixture = async (page: Page, roles: string[]) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(results),
body: JSON.stringify(
new URL(route.request().url()).searchParams.get('batch') === '1' ? results : results[0]
),
});
});
};
@@ -36,8 +36,9 @@ type FixtureState = {
gatewayLogPollCount?: number;
capabilities?: Array<{ permission: string; scope: 'GLOBAL' | 'PROFILE'; scopes: string[] }>;
profileListDelayMs?: number;
profileListRequests?: number;
profileListResolved?: boolean;
profileNavigationDelayMs?: number;
profileNavigationRequests?: number;
profileNavigationResolved?: boolean;
};
const profile = (runtimeRunning: boolean) => ({
@@ -97,12 +98,16 @@ const installFixture = async (page: Page, state: FixtureState) => {
await page.route('**/gateway/api/trpc/**', async (route) => {
const names = operationNames(route);
const body = route.request().postDataJSON() as unknown;
if (names.includes('admin.profiles.list')) {
state.profileListRequests = (state.profileListRequests ?? 0) + 1;
if (state.profileListDelayMs) {
await new Promise((resolve) => setTimeout(resolve, state.profileListDelayMs));
if (names.includes('admin.profiles.listNavigation')) {
expect(names).toEqual(['admin.profiles.listNavigation']);
state.profileNavigationRequests = (state.profileNavigationRequests ?? 0) + 1;
if (state.profileNavigationDelayMs) {
await new Promise((resolve) => setTimeout(resolve, state.profileNavigationDelayMs));
}
state.profileListResolved = true;
state.profileNavigationResolved = true;
}
if (names.includes('admin.profiles.list') && state.profileListDelayMs) {
await new Promise((resolve) => setTimeout(resolve, state.profileListDelayMs));
}
const results = names.map((name) => {
if (route.request().method() === 'POST') {
@@ -111,6 +116,15 @@ const installFixture = async (page: Page, state: FixtureState) => {
if (name === 'admin.profiles.list') {
return response([profile(state.runtimeRunning)]);
}
if (name === 'admin.profiles.listNavigation') {
return response([
{
profileName: 'che:2',
profile: 'che',
meta: { korName: '천하서버' },
},
]);
}
if (name === 'admin.capabilities.list') {
return response(
state.capabilities ?? [
@@ -261,7 +275,9 @@ const installFixture = async (page: Page, state: FixtureState) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(results),
body: JSON.stringify(
new URL(route.request().url()).searchParams.get('batch') === '1' ? results : results[0]
),
});
});
};
@@ -387,16 +403,33 @@ test('renders the fixed-profile version form without waiting for the server list
gatewayOperations: [],
runtimeRunning: true,
requestBodies: [],
profileListDelayMs: 1500,
profileListResolved: false,
profileNavigationDelayMs: 1500,
profileNavigationResolved: false,
};
await installFixture(page, state);
await page.goto('admin/servers/che%3A2/version');
await expect(page.getByTestId('request-deploy')).toBeVisible({ timeout: 900 });
expect(state.profileListResolved).toBe(false);
await expect.poll(() => state.profileListResolved).toBe(true);
expect(state.profileListRequests).toBe(1);
expect(state.profileNavigationResolved).toBe(false);
await expect.poll(() => state.profileNavigationResolved).toBe(true);
expect(state.profileNavigationRequests).toBe(1);
});
test('renders the server navigation before the detailed runtime profile request resolves', async ({ page }) => {
const state: FixtureState = {
operations: [],
gatewayOperations: [],
runtimeRunning: true,
requestBodies: [],
profileListDelayMs: 1500,
};
await installFixture(page, state);
await page.goto('admin/servers/che%3A2');
const navigation = page.getByRole('navigation', { name: '관리자 메뉴' });
await expect(navigation.getByRole('link', { name: '천하서버 (che:2)' })).toBeVisible({ timeout: 900 });
await expect(navigation.getByRole('link', { name: 'Gateway 릴리스' })).toBeVisible({ timeout: 900 });
expect(state.profileNavigationRequests).toBe(1);
});
test('scenario-only operator resets the current version without Git or Gateway controls', async ({ page }) => {