diff --git a/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts deleted file mode 100644 index a1f45ed6..00000000 --- a/ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2024 The Karmada Authors. - -Licensed 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. -*/ - -// apps/dashboard/e2e/namespace-create.spec.ts -import { test, expect } from '@playwright/test'; - -// Set webServer.url and use.baseURL with the location of the WebServer -const HOST = process.env.HOST || 'localhost'; -const PORT = process.env.PORT || 5173; -const baseURL = `http://${HOST}:${PORT}`; -const basePath = '/multicloud-resource-manage'; -const token = process.env.KARMADA_TOKEN || ''; - -test.beforeEach(async ({ page }) => { - await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.reload({ waitUntil: 'networkidle' }); - await page.waitForSelector('text=Dashboard', { timeout: 30000 }); -}); - -test('should create a new namespace', async ({ page }) => { - // 打开 Namespaces 页面 - await page.waitForSelector('text=Namespaces', { timeout: 60000 }); - await page.click('text=Namespaces'); - - // 点击 "Add" 创建新 namespace - await page.waitForSelector('button:has-text("Add")', { timeout: 30000 }); - await page.click('button:has-text("Add")'); - - // 填写唯一 namespace 名称 - const namespaceName = `test-${Date.now()}`; - await page.waitForSelector('#name', { timeout: 30000 }); - await page.fill('#name', namespaceName); - - // 提交创建 - await page.click('label:has-text("No")'); - await page.click('button:has-text("Submit")'); - - // 搜索并验证 namespace 已创建 - const searchBox = page.getByPlaceholder('Search by Name'); - await searchBox.fill(namespaceName); - await searchBox.press('Enter'); - await expect(page.locator('table')).toContainText(namespaceName); - - // Debug - if(process.env.DEBUG === 'true'){ - await page.screenshot({ path: 'debug-namespace-create.png', fullPage: true }); - } -}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts deleted file mode 100644 index 8b24abde..00000000 --- a/ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2024 The Karmada Authors. - -Licensed 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. -*/ - -// apps/dashboard/e2e/namespace-delete.spec.ts -import { test, expect } from '@playwright/test'; - -// Set webServer.url and use.baseURL with the location of the WebServer -const HOST = process.env.HOST || 'localhost'; -const PORT = process.env.PORT || 5173; -const baseURL = `http://${HOST}:${PORT}`; -const basePath = '/multicloud-resource-manage'; -const token = process.env.KARMADA_TOKEN || ''; - -test.beforeEach(async ({ page }) => { - await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.reload({ waitUntil: 'networkidle' }); - await page.waitForSelector('text=Dashboard', { timeout: 30000 }); -}); - -test('should delete a namespace', async ({ page }) => { - await page.waitForSelector('text=Namespaces', { timeout: 60000 }); - await page.click('text=Namespaces'); - - // 创建临时 namespace - const namespaceName = `test-to-delete-${Date.now()}`; - await page.click('button:has-text("Add")'); - await page.fill('#name', namespaceName); - await page.click('label:has-text("No")'); - await page.click('button:has-text("Submit")'); - - // 使用搜索框确认创建成功 - const searchBox = page.getByPlaceholder('Search by Name'); - await searchBox.fill(namespaceName); - await searchBox.press('Enter'); - await page.waitForSelector(`tr:has-text("${namespaceName}")`, { timeout: 30000 }); - - // 删除 namespace - await page.click(`tr:has-text("${namespaceName}") button:has-text("Delete")`); - await page.click('button:has-text("Confirm")'); - - // 刷新页面,确保表格拉取最新数据 - await page.reload({ waitUntil: 'networkidle' }); - - // 使用搜索框确认删除 - await searchBox.fill(namespaceName); - await searchBox.press('Enter'); - - //确认 namespace 已删除 - const table = page.locator('table'); - const start = Date.now(); - let gone = false; - - while (Date.now() - start < 120000) { // 最多等 120 秒 - const content = await table.innerText(); - if (!content.includes(namespaceName)) { - console.log(`Namespace ${namespaceName} 已彻底删除`); - gone = true; - break; - } else if (content.includes('Terminating')) { - console.log(`Namespace ${namespaceName} Terminating`); - } else { - console.log(`Namespace ${namespaceName} 仍然存在`); - } - await page.waitForTimeout(5000); // 每 5 秒检查一次 - await page.reload({ waitUntil: 'networkidle' }); // 强制刷新,拿最新数据 - await searchBox.fill(namespaceName); - await searchBox.press('Enter'); - } - - // 确认最终被删除(如果超时则失败) - expect(gone).toBeTruthy(); - - // 清空搜索框 - await searchBox.clear(); - - // Debug - if(process.env.DEBUG === 'true'){ - await page.screenshot({ path: 'debug-namespace-delete.png', fullPage: true }); - } -}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts deleted file mode 100644 index dabd859c..00000000 --- a/ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2024 The Karmada Authors. - -Licensed 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. -*/ - -// apps/dashboard/e2e/namespace-list.spec.ts -import { test, expect } from '@playwright/test'; - -// Set webServer.url and use.baseURL with the location of the WebServer -const HOST = process.env.HOST || 'localhost'; -const PORT = process.env.PORT || 5173; -const baseURL = `http://${HOST}:${PORT}`; -const basePath = '/multicloud-resource-manage'; -const token = process.env.KARMADA_TOKEN || ''; - -test.beforeEach(async ({ page }) => { - await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.reload({ waitUntil: 'networkidle' }); - await page.waitForSelector('text=Dashboard', { timeout: 30000 }); -}); - -test('should display namespace list', async ({ page }) => { - // 打开 Namespaces 页面 - await page.waitForSelector('text=Namespaces', { timeout: 60000 }); - await page.click('text=Namespaces'); - - // 获取表格元素并验证可见 - const table = page.locator('table'); - await expect(table).toBeVisible({ timeout: 30000 }); - - // 验证表格中包含默认 namespace - await expect(table).toContainText('default'); - - // Debug - if(process.env.DEBUG === 'true'){ - await page.screenshot({ path: 'debug-namespace-list.png', fullPage: true }); - } -}); diff --git a/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts b/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts deleted file mode 100644 index 42205a55..00000000 --- a/ui/apps/dashboard/e2e/namespace/namespace-network-error.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2024 The Karmada Authors. - -Licensed 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. -*/ - -// apps/dashboard/e2e/namespace/namespace-network-error.spec.ts -import { test, expect } from '@playwright/test'; - -// Set webServer.url and use.baseURL with the location of the WebServer -const HOST = process.env.HOST || 'localhost'; -const PORT = process.env.PORT || 5173; -const baseURL = `http://${HOST}:${PORT}`; -const basePath = '/multicloud-resource-manage'; -const token = process.env.KARMADA_TOKEN || ''; - -test('Namespace network failure with refresh', async ({ page }) => { - // 阻塞 Namespace API 请求 - await page.route('**/api/v1/namespaces', route => route.abort()); - - await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); - await page.evaluate((t) => localStorage.setItem('token', t), token); - - // 导航到页面 - await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); - - // 设置 token 并刷新,保证登录状态 - await page.evaluate((t) => localStorage.setItem('token', t), token); - await page.reload({ waitUntil: 'networkidle' }); - - // 等待关键元素加载完成,宽松等待 Namespaces 文字 - await page.waitForSelector('text=Namespaces', { timeout: 15000 }); - - // 验证表格 - const tableRows = page.locator('table tbody tr'); - // 最终断言:网络错误时表格应该为空 - await expect(tableRows).toHaveCount(0); - - // Debug - if(process.env.DEBUG === 'true'){ - await page.screenshot({ path: 'debug-namespace-network-failure.png', fullPage: true }); - } -}); diff --git a/ui/apps/dashboard/e2e/service/ingress/ingress-create.spec.ts b/ui/apps/dashboard/e2e/service/ingress/ingress-create.spec.ts new file mode 100644 index 00000000..6f2ee804 --- /dev/null +++ b/ui/apps/dashboard/e2e/service/ingress/ingress-create.spec.ts @@ -0,0 +1,144 @@ +/* +Copyright 2024 The Karmada Authors. +Licensed 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 { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication, generateTestIngressYaml, deleteK8sIngress } from './test-utils'; +import { parse } from 'yaml'; +import _ from 'lodash'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should create a new ingress', async ({ page }) => { + // Navigate to service menu + await page.click('text=Services'); + + // Click visible Ingress tab + const ingressTab = page.locator('role=option[name="Ingress"]'); + await ingressTab.waitFor({ state: 'visible', timeout: 30000 }); + await ingressTab.click(); + + // Verify selected state + await expect(ingressTab).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('table')).toBeVisible({ timeout: 30000 }); + await page.click('button:has-text("Add")'); + await page.waitForSelector('[role="dialog"]', { timeout: 10000 }); + + // Listen for API calls + const apiRequestPromise = page.waitForResponse(response => { + return response.url().includes('/api/v1/_raw/Ingress') && response.status() === 200; + }, { timeout: 15000 }); + + const testIngressYaml = generateTestIngressYaml(); + + // Set Monaco editor DOM content + await page.evaluate((yaml) => { + const textarea = document.querySelector('.monaco-editor textarea') as HTMLTextAreaElement; + if (textarea) { + textarea.value = yaml; + textarea.focus(); + } + }, testIngressYaml); + + // Call React onChange callback to update component state + await page.evaluate((yaml) => { + + const findReactFiber = (element: any) => { + const keys = Object.keys(element); + return keys.find(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance')); + }; + + const monacoContainer = document.querySelector('.monaco-editor'); + if (monacoContainer) { + const fiberKey = findReactFiber(monacoContainer); + if (fiberKey) { + let fiber = (monacoContainer as any)[fiberKey]; + + while (fiber) { + if (fiber.memoizedProps && fiber.memoizedProps.onChange) { + fiber.memoizedProps.onChange(yaml); + return; + } + fiber = fiber.return; + } + } + } + + const dialog = document.querySelector('[role="dialog"]'); + if (dialog) { + const fiberKey = findReactFiber(dialog); + if (fiberKey) { + let fiber = (dialog as any)[fiberKey]; + + const traverse = (node: any, depth = 0) => { + if (!node || depth > 20) return false; + + if (node.memoizedProps && node.memoizedProps.onChange) { + node.memoizedProps.onChange(yaml); + return true; + } + + if (node.child && traverse(node.child, depth + 1)) return true; + if (node.sibling && traverse(node.sibling, depth + 1)) return true; + + return false; + }; + + traverse(fiber); + } + } + }, testIngressYaml); + + // Wait for submit button to become enabled + await expect(page.locator('[role="dialog"] button:has-text("Submit")')).toBeEnabled(); + await page.click('[role="dialog"] button:has-text("Submit")'); + + // Wait for API call to succeed + await apiRequestPromise; + + // Wait for dialog to close + await page.waitForSelector('[role="dialog"]', { state: 'detached', timeout: 5000 }).catch(() => { + // Dialog may already be closed + }); + + // Verify new ingress appears in list + const yamlObject = parse(testIngressYaml); + const ingressName = _.get(yamlObject,'metadata.name'); + + // Assert ingress name exists + expect(ingressName).toBeTruthy(); + expect(ingressName).toBeDefined(); + + try { + await expect(page.locator('table').locator(`text=${ingressName}`)).toBeVisible({ + timeout: 15000 + }); + } catch { + // If not shown immediately in list, may be due to cache or refresh delay + // But API success indicates ingress was created + } + + // Cleanup: Delete the created ingress + try { + await deleteK8sIngress(ingressName, 'default'); + } catch (error) { + console.warn(`Failed to cleanup ingress ${ingressName}:`, error); + } + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-ingress-create.png', fullPage: true }); + } + +}); diff --git a/ui/apps/dashboard/e2e/service/ingress/ingress-delete.spec.ts b/ui/apps/dashboard/e2e/service/ingress/ingress-delete.spec.ts new file mode 100644 index 00000000..3a78013b --- /dev/null +++ b/ui/apps/dashboard/e2e/service/ingress/ingress-delete.spec.ts @@ -0,0 +1,89 @@ +/* +Copyright 2024 The Karmada Authors. +Licensed 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 { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication, generateTestIngressYaml, createK8sIngress, getIngressNameFromYaml} from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should delete ingress successfully', async ({ page }) => { + // Create a test ingress directly via kubectl to set up test data + const testIngressYaml = generateTestIngressYaml(); + const ingressName = getIngressNameFromYaml(testIngressYaml); + + // Setup: Create ingress using kubectl + await createK8sIngress(testIngressYaml); + + // Navigate to services page + await page.click('text=Services'); + + // Click visible Ingress tab + const ingressTab = page.locator('role=option[name="Ingress"]'); + await ingressTab.waitFor({ state: 'visible', timeout: 30000 }); + await ingressTab.click(); + + // Verify selected state + await expect(ingressTab).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('table')).toBeVisible({ timeout: 30000 }); + + // Wait for ingress to appear in list + const table = page.locator('table'); + await expect(table.locator(`text=${ingressName}`)).toBeVisible({ timeout: 30000 }); + + // Find row containing test ingress name + const targetRow = page.locator(`table tbody tr:has-text("${ingressName}")`); + await expect(targetRow).toBeVisible({ timeout: 15000 }); + + // Find Delete button in that row and click + const deleteButton = targetRow.locator('button[type="button"]').filter({ hasText: /^(Delete)$/ }); + await expect(deleteButton).toBeVisible({ timeout: 10000 }); + + // Listen for delete API call + const deleteApiPromise = page.waitForResponse(response => { + return response.url().includes('/_raw/ingress') && + response.url().includes(`name/${ingressName}`) && + response.request().method() === 'DELETE' && + response.status() === 200; + }, { timeout: 15000 }); + + await deleteButton.click(); + + // Wait for delete confirmation tooltip to appear + await page.waitForSelector('[role="tooltip"]', { timeout: 10000 }); + + // Click Confirm button to confirm deletion + const confirmButton = page.locator('[role="tooltip"] button').filter({ hasText: /^(Confirm)$/ }); + await expect(confirmButton).toBeVisible({ timeout: 5000 }); + await confirmButton.click(); + + // Wait for delete API call to succeed + await deleteApiPromise; + + // Wait for tooltip to close + await page.waitForSelector('[role="tooltip"]', { state: 'detached', timeout: 10000 }).catch(() => {}); + + // Refresh page to ensure UI is updated after deletion + await page.reload(); + await page.click('text=Services'); + await expect(table).toBeVisible({ timeout: 30000 }); + + // Verify ingress no longer exists in table + await expect(table.locator(`text=${ingressName}`)).toHaveCount(0, { timeout: 30000 }); + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-ingress-delete-kubectl.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/service/ingress/ingress-list.spec.ts b/ui/apps/dashboard/e2e/service/ingress/ingress-list.spec.ts new file mode 100644 index 00000000..3f4226bc --- /dev/null +++ b/ui/apps/dashboard/e2e/service/ingress/ingress-list.spec.ts @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed 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. +*/ + +// apps/dashboard/e2e/ingress-list.spec.ts +import { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication } from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should display ingress list', async ({ page }) => { + // Open Services menu + await page.click('text=Services'); + + // Click visible Ingress tab + const ingressTab = page.locator('role=option[name="Ingress"]'); + await ingressTab.waitFor({ state: 'visible', timeout: 30000 }); + await ingressTab.click(); + + // Verify selected state + await expect(ingressTab).toHaveAttribute('aria-selected', 'true'); + + // Verify Ingress list table is visible + const table = page.locator('table'); + await expect(table).toBeVisible({ timeout: 30000 }); + + // Debug + if (process.env.DEBUG === 'true') { + await page.screenshot({ path: 'debug-ingress-list.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/service/ingress/ingress-view.spec.ts b/ui/apps/dashboard/e2e/service/ingress/ingress-view.spec.ts new file mode 100644 index 00000000..9cf45ecf --- /dev/null +++ b/ui/apps/dashboard/e2e/service/ingress/ingress-view.spec.ts @@ -0,0 +1,67 @@ +/* +Copyright 2024 The Karmada Authors. +Licensed 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 { test, expect } from '@playwright/test'; +import { setupDashboardAuthentication, generateTestIngressYaml, createK8sIngress, getIngressNameFromYaml, deleteK8sIngress } from './test-utils'; + +test.beforeEach(async ({ page }) => { + await setupDashboardAuthentication(page); +}); + +test('should view ingress details', async ({ page }) => { + // Create a test ingress directly via API to set up test data + const testIngressYaml = generateTestIngressYaml(); + const ingressName = getIngressNameFromYaml(testIngressYaml); + + // Setup: Create ingress using kubectl + await createK8sIngress(testIngressYaml); + + // Navigate to services page + await page.click('text=Services'); + + // Click visible Ingress tab + const ingressTab = page.locator('role=option[name="Ingress"]'); + await ingressTab.waitFor({ state: 'visible', timeout: 30000 }); + await ingressTab.click(); + + // Verify selected state + await expect(ingressTab).toHaveAttribute('aria-selected', 'true'); + await expect(page.locator('table')).toBeVisible({ timeout: 30000 }); + + // Wait for ingress to appear in list + const table = page.locator('table'); + await expect(table.locator(`text=${ingressName}`)).toBeVisible({ timeout: 30000 }); + + // Find row containing test ingress name + const targetRow = page.locator(`table tbody tr:has-text("${ingressName}")`); + await expect(targetRow).toBeVisible({ timeout: 15000 }); + + // Find View button in that row and click + const viewButton = targetRow.getByText('View'); + await expect(viewButton).toBeVisible({ timeout: 15000 }); + await viewButton.click(); + + // Verify details page is displayed + await page.waitForLoadState('networkidle'); + + // Cleanup: Delete the created ingress + try { + await deleteK8sIngress(ingressName, 'default'); + } catch (error) { + console.warn(`Failed to cleanup ingress ${ingressName}:`, error); + } + + // Debug + if(process.env.DEBUG === 'true'){ + await page.screenshot({ path: 'debug-ingress-view.png', fullPage: true }); + } +}); diff --git a/ui/apps/dashboard/e2e/service/ingress/test-utils.ts b/ui/apps/dashboard/e2e/service/ingress/test-utils.ts new file mode 100644 index 00000000..f446d264 --- /dev/null +++ b/ui/apps/dashboard/e2e/service/ingress/test-utils.ts @@ -0,0 +1,189 @@ +/*Copyright 2024 The Karmada Authors. +Licensed 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 { Page, expect } from '@playwright/test'; +import * as k8s from '@kubernetes/client-node'; +import { parse } from 'yaml'; +import _ from 'lodash'; + +// Set webServer.url and use.baseURL with the location of the WebServer +const HOST = process.env.HOST || 'localhost'; +const PORT = process.env.PORT || 5173; +const baseURL = `http://${HOST}:${PORT}`; +const basePath = '/multicloud-resource-manage'; +const token = process.env.KARMADA_TOKEN || ''; + +// Karmada API server configuration - can be overridden by environment variables +const KARMADA_HOST = process.env.KARMADA_HOST || 'localhost'; +const KARMADA_PORT = process.env.KARMADA_PORT || '5443'; // Standard Karmada API server port +const KARMADA_API_SERVER = `https://${KARMADA_HOST}:${KARMADA_PORT}`; + +/** + * Creates a configured Kubernetes API client for Karmada + * @returns Kubernetes NetworkingV1Api client + */ +function createKarmadaApiClient(): k8s.NetworkingV1Api { + const kc = new k8s.KubeConfig(); + + // Try to use existing kubeconfig first (for CI) + if (process.env.KUBECONFIG) { + try { + kc.loadFromFile(process.env.KUBECONFIG); + // Set context to karmada-apiserver if available + const contexts = kc.getContexts(); + const karmadaContext = contexts.find(c => c.name === 'karmada-apiserver'); + if (karmadaContext) { + kc.setCurrentContext('karmada-apiserver'); + } + return kc.makeApiClient(k8s.NetworkingV1Api); + } catch (error) { + console.error('Failed to load kubeconfig:', error); + } + } + + // Fallback to custom config for local development + const kubeConfigYaml = ` +apiVersion: v1 +kind: Config +clusters: +- cluster: + insecure-skip-tls-verify: true + server: ${KARMADA_API_SERVER} + name: karmada-apiserver +contexts: +- context: + cluster: karmada-apiserver + user: karmada-dashboard + name: default +current-context: default +users: +- name: karmada-dashboard + user: + token: ${token} +`; + + kc.loadFromString(kubeConfigYaml); + return kc.makeApiClient(k8s.NetworkingV1Api); +} + +/** + * Setup dashboard authentication and navigate to ingress page + */ +export async function setupDashboardAuthentication(page: Page) { + await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' }); + await page.evaluate((t) => localStorage.setItem('token', t), token); + await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' }); + await page.evaluate((t) => localStorage.setItem('token', t), token); + await page.reload({ waitUntil: 'networkidle' }); + await page.waitForSelector('text=Overview', { timeout: 30000 }); +} + +/** + * Generate test Ingress YAML with timestamp + */ +export function generateTestIngressYaml() { + const timestamp = Date.now(); + return `apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: test-ingress-${timestamp} + namespace: default + labels: + app: test-app +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: test-service + port: + number: 80`; +} + +/** + * Creates a Kubernetes Ingress using the Kubernetes JavaScript client. + * This is a more robust way to set up test data than UI interaction. + * @param yamlContent The YAML content of the ingress. + * @returns A Promise that resolves when the ingress is created. + */ +export async function createK8sIngress(yamlContent: string): Promise { + try { + const k8sApi = createKarmadaApiClient(); + const yamlObject = parse(yamlContent) as k8s.V1Ingress; + + // Ensure namespace is always defined + const namespace = yamlObject.metadata?.namespace || 'default'; + + // Ensure metadata object exists + if (!yamlObject.metadata) { + yamlObject.metadata = {}; + } + yamlObject.metadata.namespace = namespace; + + await k8sApi.createNamespacedIngress({ + namespace: namespace, + body: yamlObject + }); + + } catch (error: any) { + throw new Error(`Failed to create ingress: ${error.message}`); + } +} + +/** + * Deletes a Kubernetes Ingress using the Kubernetes JavaScript client. + * @param ingressName The name of the ingress to delete. + * @param namespace The namespace of the ingress (default: 'default'). + * @returns A Promise that resolves when the ingress is deleted. + */ +export async function deleteK8sIngress(ingressName: string, namespace: string = 'default'): Promise { + try { + const k8sApi = createKarmadaApiClient(); + + // Assert parameters are valid for test ingress + expect(ingressName).toBeTruthy(); + expect(ingressName).not.toBe(''); + expect(namespace).toBeTruthy(); + + await k8sApi.deleteNamespacedIngress({ + name: ingressName, + namespace: namespace + }); + + } catch (error: any) { + if (error.response?.status === 404 || error.statusCode === 404) { + // Ingress not found - already deleted, this is fine + return; + } + throw new Error(`Failed to delete ingress: ${error.message}`); + } +} + +/** + * Gets ingress name from YAML content using proper YAML parsing. + * @param yamlContent The YAML content. + * @returns The ingress name. + */ +export function getIngressNameFromYaml(yamlContent: string): string { + const yamlObject = parse(yamlContent); + const ingressName = _.get(yamlObject, 'metadata.name'); + + if (!ingressName) { + throw new Error('Could not extract ingress name from YAML'); + } + + return ingressName; +} diff --git a/ui/apps/dashboard/package.json b/ui/apps/dashboard/package.json index b1afd5a6..e25a1011 100644 --- a/ui/apps/dashboard/package.json +++ b/ui/apps/dashboard/package.json @@ -31,6 +31,7 @@ "@karmada/i18n-tool": "workspace:*", "@karmada/terminal": "workspace:*", "@karmada/utils": "workspace:*", + "@kubernetes/client-node": "^1.3.0", "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "^5.59.8", "@uidotdev/usehooks": "^2.4.1", diff --git a/ui/apps/dashboard/playwright.config.ts b/ui/apps/dashboard/playwright.config.ts index 6c2e2738..583aa583 100644 --- a/ui/apps/dashboard/playwright.config.ts +++ b/ui/apps/dashboard/playwright.config.ts @@ -74,4 +74,4 @@ export default defineConfig({ timeout: 120 * 1000, reuseExistingServer: !process.env.CI, }, -}); \ No newline at end of file +}); diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 77aea994..8f01c124 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@karmada/utils': specifier: workspace:* version: link:../../packages/utils + '@kubernetes/client-node': + specifier: ^1.3.0 + version: 1.3.0 '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -357,10 +360,10 @@ importers: version: 8.1.0(@swc/core@1.7.22)(postcss@8.4.47)(typescript@5.6.3) vite: specifier: ^5.2.0 - version: 5.2.11(@types/node@20.12.11)(less@4.2.0) + version: 5.2.11(@types/node@22.18.1)(less@4.2.0) vitest: specifier: ^2.0.5 - version: 2.1.2(@types/node@20.12.11)(less@4.2.0) + version: 2.1.2(@types/node@22.18.1)(less@4.2.0) packages/utils: {} @@ -998,6 +1001,21 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@kubernetes/client-node@1.3.0': + resolution: {integrity: sha512-IE0yrIpOT97YS5fg2QpzmPzm8Wmcdf4ueWMn+FiJSI3jgTTQT1u+LUhoYpdfhdHAVxdrNsaBg2C0UXSnOgMoCQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1479,6 +1497,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1503,6 +1524,9 @@ packages: '@types/node@20.12.11': resolution: {integrity: sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==} + '@types/node@22.18.1': + resolution: {integrity: sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==} + '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -1521,6 +1545,9 @@ packages: '@types/sockjs-client@1.5.4': resolution: {integrity: sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==} + '@types/stream-buffers@3.0.7': + resolution: {integrity: sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw==} + '@typescript-eslint/eslint-plugin@5.62.0': resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1741,6 +1768,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.5.0: resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} engines: {node: '>= 8.0.0'} @@ -1857,9 +1888,45 @@ packages: axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.6.1: + resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} + + bare-fs@4.3.2: + resolution: {integrity: sha512-FAJ00JF69O6/oKAP+oiJYgdem1biZoGAR0NbRkBRQZ26shA87DmdHWbpeY3EVKPrAzHByLoLo+hAzTT6NTJWCg==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + bare-url: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-url: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -2183,6 +2250,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -2400,6 +2470,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -2642,6 +2715,10 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -2716,6 +2793,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -2864,6 +2945,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -2879,6 +2965,9 @@ packages: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + jose@6.1.0: + resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2897,6 +2986,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -2928,6 +3021,11 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3200,6 +3298,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + oauth4webapi@3.8.1: + resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3256,6 +3357,9 @@ packages: zod: optional: true + openid-client@6.7.1: + resolution: {integrity: sha512-kOiE4q0kNogr90hXsxPrKeEDuY+V0kkZazvZScOwZkYept9slsaQ3usXTaKkm6I04vLNuw5caBoX7UfrwC6x8w==} + opentype.js@0.8.0: resolution: {integrity: sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==} hasBin: true @@ -3483,6 +3587,9 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3847,6 +3954,9 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -3957,6 +4067,10 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -3964,6 +4078,14 @@ packages: resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==} engines: {node: '>=12'} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3991,6 +4113,13 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -4080,10 +4209,19 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4266,6 +4404,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -4494,6 +4635,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} @@ -5186,6 +5339,40 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@kubernetes/client-node@1.3.0': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 22.18.1 + '@types/node-fetch': 2.6.11 + '@types/stream-buffers': 3.0.7 + form-data: 4.0.0 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.18.3) + js-yaml: 4.1.0 + jsonpath-plus: 10.3.0 + node-fetch: 2.7.0 + openid-client: 6.7.1 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.0 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bare-url + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.25.7 @@ -5583,6 +5770,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/lodash.isempty@4.4.9': @@ -5608,6 +5797,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.18.1': + dependencies: + undici-types: 6.21.0 + '@types/prop-types@15.7.12': {} '@types/react-dom@18.3.0': @@ -5628,6 +5821,10 @@ snapshots: '@types/sockjs-client@1.5.4': {} + '@types/stream-buffers@3.0.7': + dependencies: + '@types/node': 20.12.11 + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.10.0 @@ -5818,13 +6015,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.12.11)(less@4.2.0))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.18.1)(less@4.2.0))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: - vite: 5.4.8(@types/node@20.12.11)(less@4.2.0) + vite: 5.4.8(@types/node@22.18.1)(less@4.2.0) '@vitest/pretty-format@2.1.2': dependencies: @@ -5898,6 +6095,8 @@ snapshots: acorn@8.11.3: {} + agent-base@7.1.4: {} + agentkeepalive@4.5.0: dependencies: humanize-ms: 1.2.1 @@ -6093,8 +6292,35 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.6.7: {} + balanced-match@1.0.2: {} + bare-events@2.6.1: + optional: true + + bare-fs@4.3.2: + dependencies: + bare-events: 2.6.1 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.6.1) + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.6.1): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.1 + optional: true + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -6403,6 +6629,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -6801,6 +7031,8 @@ snapshots: fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7059,6 +7291,8 @@ snapshots: he@1.2.0: {} + hpagent@1.2.0: {} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 @@ -7126,6 +7360,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@10.0.1: {} + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -7250,6 +7486,10 @@ snapshots: isexe@2.0.0: {} + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 @@ -7270,6 +7510,8 @@ snapshots: jiti@1.21.0: {} + jose@6.1.0: {} + joycon@3.1.1: {} js-base64@3.7.7: {} @@ -7285,6 +7527,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsep@1.4.0: {} + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -7307,6 +7551,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpath-plus@10.3.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -7585,6 +7835,8 @@ snapshots: dependencies: boolbase: 1.0.0 + oauth4webapi@3.8.1: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -7649,6 +7901,11 @@ snapshots: transitivePeerDependencies: - encoding + openid-client@6.7.1: + dependencies: + jose: 6.1.0 + oauth4webapi: 3.8.1 + opentype.js@0.8.0: dependencies: tiny-inflate: 1.0.3 @@ -7832,6 +8089,11 @@ snapshots: pseudomap@1.0.2: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} querystringify@2.2.0: {} @@ -8284,6 +8546,8 @@ snapshots: reusify@1.0.4: {} + rfc4648@1.5.4: {} + rfdc@1.4.1: {} rimraf@3.0.2: @@ -8428,6 +8692,8 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + smart-buffer@4.2.0: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -8443,6 +8709,19 @@ snapshots: transitivePeerDependencies: - supports-color + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.3.7 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.0.1 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map@0.6.1: @@ -8465,6 +8744,15 @@ snapshots: std-env@3.7.0: {} + stream-buffers@3.0.3: {} + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.6.1 + string-argv@0.3.2: {} string-convert@0.2.1: {} @@ -8590,8 +8878,29 @@ snapshots: transitivePeerDependencies: - ts-node + tar-fs@3.1.0: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.3.2 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + - bare-url + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.1 + term-size@2.2.1: {} + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -8762,6 +9071,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.21.0: {} + universalify@0.1.2: {} update-browserslist-db@1.0.15(browserslist@4.23.0): @@ -8791,12 +9102,12 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@2.1.2(@types/node@20.12.11)(less@4.2.0): + vite-node@2.1.2(@types/node@22.18.1)(less@4.2.0): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(@types/node@20.12.11)(less@4.2.0) + vite: 5.4.8(@types/node@22.18.1)(less@4.2.0) transitivePeerDependencies: - '@types/node' - less @@ -8829,13 +9140,13 @@ snapshots: - supports-color - typescript - vite@5.2.11(@types/node@20.12.11)(less@4.2.0): + vite@5.2.11(@types/node@22.18.1)(less@4.2.0): dependencies: esbuild: 0.20.2 postcss: 8.4.47 rollup: 4.17.2 optionalDependencies: - '@types/node': 20.12.11 + '@types/node': 22.18.1 fsevents: 2.3.3 less: 4.2.0 @@ -8849,10 +9160,20 @@ snapshots: fsevents: 2.3.3 less: 4.2.0 - vitest@2.1.2(@types/node@20.12.11)(less@4.2.0): + vite@5.4.8(@types/node@22.18.1)(less@4.2.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.24.0 + optionalDependencies: + '@types/node': 22.18.1 + fsevents: 2.3.3 + less: 4.2.0 + + vitest@2.1.2(@types/node@22.18.1)(less@4.2.0): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.12.11)(less@4.2.0)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.18.1)(less@4.2.0)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -8867,11 +9188,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@20.12.11)(less@4.2.0) - vite-node: 2.1.2(@types/node@20.12.11)(less@4.2.0) + vite: 5.4.8(@types/node@22.18.1)(less@4.2.0) + vite-node: 2.1.2(@types/node@22.18.1)(less@4.2.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.12.11 + '@types/node': 22.18.1 transitivePeerDependencies: - less - lightningcss @@ -9001,6 +9322,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.3: {} + yallist@2.1.2: {} yallist@3.1.1: {}