diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b4908..f624afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,16 @@ ### Features +- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 + ### Bug Fixes ### Improvements +### Testing + +- test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复 + ## v1.11.0 ### Features diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 4ef0a3d..61623a5 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -10,10 +10,16 @@ ### Features +- feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit + ### Bug Fixes ### Improvements +### Testing + +- test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads + ## v1.11.0 ### Features diff --git a/e2e/tests/api/admin-config.spec.ts b/e2e/tests/api/admin-config.spec.ts new file mode 100644 index 0000000..011860b --- /dev/null +++ b/e2e/tests/api/admin-config.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; +import { WORKER_URL } from '../../fixtures/test-helpers'; + +const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' }; + +test.describe('Admin Config and D1 Storage', () => { + test('reports database size in the database status response', async ({ request }) => { + const response = await request.get(`${WORKER_URL}/admin/db_version`, { + headers: ADMIN_HEADERS, + }); + + expect(response.ok()).toBe(true); + const body = await response.json(); + expect(body.database_size).toEqual(expect.any(Number)); + expect(body.database_size).toBeGreaterThan(0); + }); + + test('saves and retrieves a namespaced config value', async ({ request }) => { + const saveResponse = await request.post(`${WORKER_URL}/admin/config`, { + headers: ADMIN_HEADERS, + data: { key: 'd1_storage_plan', value: 'free' }, + }); + + expect(saveResponse.ok()).toBe(true); + expect(await saveResponse.json()).toEqual({ + success: true, + key: 'd1_storage_plan', + value: 'free', + }); + + const getResponse = await request.get(`${WORKER_URL}/admin/config/d1_storage_plan`, { + headers: ADMIN_HEADERS, + }); + + expect(getResponse.ok()).toBe(true); + expect(await getResponse.json()).toEqual({ + key: 'd1_storage_plan', + value: 'free', + }); + }); + + test('keeps config values isolated from internal settings', async ({ request }) => { + const versionBeforeResponse = await request.get(`${WORKER_URL}/admin/db_version`, { + headers: ADMIN_HEADERS, + }); + const versionBefore = (await versionBeforeResponse.json()).current_db_version; + + const saveResponse = await request.post(`${WORKER_URL}/admin/config`, { + headers: ADMIN_HEADERS, + data: { key: 'db_version', value: 'shadow-version' }, + }); + expect(saveResponse.ok()).toBe(true); + + const versionAfterResponse = await request.get(`${WORKER_URL}/admin/db_version`, { + headers: ADMIN_HEADERS, + }); + expect((await versionAfterResponse.json()).current_db_version).toBe(versionBefore); + }); + + test('rejects invalid keys and non-string values', async ({ request }) => { + const invalidKeyResponse = await request.post(`${WORKER_URL}/admin/config`, { + headers: ADMIN_HEADERS, + data: { key: 'invalid key', value: 'value' }, + }); + expect(invalidKeyResponse.status()).toBe(400); + + const invalidValueResponse = await request.post(`${WORKER_URL}/admin/config`, { + headers: ADMIN_HEADERS, + data: { key: 'valid_key', value: 1 }, + }); + expect(invalidValueResponse.status()).toBe(400); + }); +}); diff --git a/e2e/tests/browser/database-storage.spec.ts b/e2e/tests/browser/database-storage.spec.ts new file mode 100644 index 0000000..cd9d354 --- /dev/null +++ b/e2e/tests/browser/database-storage.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test'; +import { FRONTEND_URL, WORKER_URL } from '../../fixtures/test-helpers'; + +const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' }; + +test('persists the selected D1 plan and restores it after reload', async ({ page, request }) => { + const seedResponse = await request.post(`${WORKER_URL}/admin/config`, { + headers: ADMIN_HEADERS, + data: { key: 'd1_storage_plan', value: 'free' }, + }); + expect(seedResponse.ok()).toBe(true); + + await page.addInitScript(() => { + localStorage.setItem('adminAuth', 'e2e-admin-pass'); + sessionStorage.setItem('adminTab', 'qucickSetup'); + }); + await page.goto(`${FRONTEND_URL}/en/admin`); + + const storagePanel = page.locator('.storage-panel'); + const planSelect = storagePanel.locator('.plan-select .n-select'); + + await expect(storagePanel.getByText('Current Database Size', { exact: true })).toBeVisible(); + await expect(storagePanel.getByText('Database Capacity Limit', { exact: true })).toBeVisible(); + await expect(storagePanel.getByText('Capacity Usage', { exact: true })).toBeVisible(); + await expect(planSelect).toContainText('Free'); + + await planSelect.click(); + await page.locator('.n-base-select-option').filter({ hasText: 'Workers Paid' }).click(); + + await expect(page.getByText('Workers plan saved')).toBeVisible(); + await expect(storagePanel).toContainText('10.0 GB'); + + await page.reload(); + + await expect(planSelect).toContainText('Workers Paid'); + await expect(storagePanel).toContainText('10.0 GB'); +}); diff --git a/frontend/src/i18n/locales/source/de.ts b/frontend/src/i18n/locales/source/de.ts index e0d7398..89fe4e7 100644 --- a/frontend/src/i18n/locales/source/de.ts +++ b/frontend/src/i18n/locales/source/de.ts @@ -1,4 +1,16 @@ export const deMessages = { + "views.admin.DatabaseManager.current_database_size": "Aktuelle Datenbankgröße", + "views.admin.DatabaseManager.free_plan": "Free", + "views.admin.DatabaseManager.paid_plan": "Workers Paid", + "views.admin.DatabaseManager.plan": "Workers-Tarif", + "views.admin.DatabaseManager.plan_placeholder": "Cloudflare-Workers-Tarif auswählen", + "views.admin.DatabaseManager.planSaved": "Workers-Tarif gespeichert", + "views.admin.DatabaseManager.single_database_limit": "Datenbank-Kapazitätslimit", + "views.admin.DatabaseManager.storage_description": "Vergleiche die aktuelle Datenbankgröße mit den Tariflimits.", + "views.admin.DatabaseManager.storage_tip": "Die Auslastung wird anhand der aktuellen Datenbankgröße und des gewählten Tariflimits berechnet.", + "views.admin.DatabaseManager.storage_title": "D1-Speicherkapazität", + "views.admin.DatabaseManager.storage_usage": "Kapazitätsauslastung", + "views.admin.DatabaseManager.unavailable": "Nicht verfügbar", "views.index.SimpleIndex.mailCount": "{current} / {total} E-Mails", "views.admin.Statistics.activeAddressCount30days": "Aktive Adressanzahl in 30 Tagen", "views.admin.Statistics.activeAddressCount7days": "Aktive Adressanzahl in 7 Tagen", diff --git a/frontend/src/i18n/locales/source/es.ts b/frontend/src/i18n/locales/source/es.ts index 2fa3d5e..e3f27e1 100644 --- a/frontend/src/i18n/locales/source/es.ts +++ b/frontend/src/i18n/locales/source/es.ts @@ -1,4 +1,16 @@ export const esMessages = { + "views.admin.DatabaseManager.current_database_size": "Tamaño actual de la base de datos", + "views.admin.DatabaseManager.free_plan": "Free", + "views.admin.DatabaseManager.paid_plan": "Workers Paid", + "views.admin.DatabaseManager.plan": "Plan de Workers", + "views.admin.DatabaseManager.plan_placeholder": "Selecciona tu plan de Cloudflare Workers", + "views.admin.DatabaseManager.planSaved": "Plan de Workers guardado", + "views.admin.DatabaseManager.single_database_limit": "Límite de capacidad de la base de datos", + "views.admin.DatabaseManager.storage_description": "Compara el tamaño actual de la base de datos con los límites de tu plan.", + "views.admin.DatabaseManager.storage_tip": "El uso se calcula con el tamaño actual de la base de datos y el límite del plan seleccionado.", + "views.admin.DatabaseManager.storage_title": "Capacidad de almacenamiento D1", + "views.admin.DatabaseManager.storage_usage": "Uso de capacidad", + "views.admin.DatabaseManager.unavailable": "No disponible", "views.index.SimpleIndex.mailCount": "{current} / {total} correos", "views.admin.Statistics.activeAddressCount30days": "Cantidad de direcciones activas en 30 días", "views.admin.Statistics.activeAddressCount7days": "Cantidad de direcciones activas en 7 días", diff --git a/frontend/src/i18n/locales/source/ja.ts b/frontend/src/i18n/locales/source/ja.ts index 978ed18..26bb941 100644 --- a/frontend/src/i18n/locales/source/ja.ts +++ b/frontend/src/i18n/locales/source/ja.ts @@ -1,4 +1,16 @@ export const jaMessages = { + "views.admin.DatabaseManager.current_database_size": "現在のデータベースサイズ", + "views.admin.DatabaseManager.free_plan": "Free", + "views.admin.DatabaseManager.paid_plan": "Workers Paid", + "views.admin.DatabaseManager.plan": "Workers プラン", + "views.admin.DatabaseManager.plan_placeholder": "Cloudflare Workers プランを選択", + "views.admin.DatabaseManager.planSaved": "Workers プランを保存しました", + "views.admin.DatabaseManager.single_database_limit": "データベース容量上限", + "views.admin.DatabaseManager.storage_description": "現在のデータベースサイズとプラン上限を比較します。", + "views.admin.DatabaseManager.storage_tip": "使用率は現在のデータベースサイズと選択したプラン上限から計算されます。", + "views.admin.DatabaseManager.storage_title": "D1 ストレージ容量", + "views.admin.DatabaseManager.storage_usage": "容量使用率", + "views.admin.DatabaseManager.unavailable": "利用不可", "views.index.SimpleIndex.mailCount": "{current} / {total} 件のメール", "views.admin.Statistics.activeAddressCount30days": "30日間のアクティブなアドレス数", "views.admin.Statistics.activeAddressCount7days": "7日間のアクティブなアドレス数", diff --git a/frontend/src/i18n/locales/source/ptBR.ts b/frontend/src/i18n/locales/source/ptBR.ts index 6268727..66dc8f6 100644 --- a/frontend/src/i18n/locales/source/ptBR.ts +++ b/frontend/src/i18n/locales/source/ptBR.ts @@ -1,4 +1,16 @@ export const ptBRMessages = { + "views.admin.DatabaseManager.current_database_size": "Tamanho atual do banco de dados", + "views.admin.DatabaseManager.free_plan": "Free", + "views.admin.DatabaseManager.paid_plan": "Workers Paid", + "views.admin.DatabaseManager.plan": "Plano do Workers", + "views.admin.DatabaseManager.plan_placeholder": "Selecione seu plano do Cloudflare Workers", + "views.admin.DatabaseManager.planSaved": "Plano do Workers salvo", + "views.admin.DatabaseManager.single_database_limit": "Limite de capacidade do banco de dados", + "views.admin.DatabaseManager.storage_description": "Compare o tamanho atual do banco de dados com os limites do seu plano.", + "views.admin.DatabaseManager.storage_tip": "O uso é calculado com o tamanho atual do banco de dados e o limite do plano selecionado.", + "views.admin.DatabaseManager.storage_title": "Capacidade de armazenamento D1", + "views.admin.DatabaseManager.storage_usage": "Uso da capacidade", + "views.admin.DatabaseManager.unavailable": "Indisponível", "views.index.SimpleIndex.mailCount": "{current} / {total} e-mails", "views.admin.Statistics.activeAddressCount30days": "Quantidade de endereços ativos em 30 dias", "views.admin.Statistics.activeAddressCount7days": "Quantidade de endereços ativos em 7 dias", diff --git a/frontend/src/i18n/message-registry.ts b/frontend/src/i18n/message-registry.ts index f3d76b2..b1d6c1e 100644 --- a/frontend/src/i18n/message-registry.ts +++ b/frontend/src/i18n/message-registry.ts @@ -1694,6 +1694,14 @@ export const MESSAGE_REGISTRY = { "en": "Current DB Version", "zh": "当前数据库版本" }, + "current_database_size": { + "en": "Current Database Size", + "zh": "当前数据库大小" + }, + "free_plan": { + "en": "Free", + "zh": "Free" + }, "init": { "en": "Initialize Database", "zh": "初始化数据库" @@ -1717,6 +1725,46 @@ export const MESSAGE_REGISTRY = { "need_migration_tip": { "en": "Database migration is required. Please migrate the database.", "zh": "需要迁移数据库,请迁移数据库" + }, + "paid_plan": { + "en": "Workers Paid", + "zh": "Workers Paid" + }, + "plan": { + "en": "Workers Plan", + "zh": "Workers 套餐" + }, + "plan_placeholder": { + "en": "Select your Cloudflare Workers plan", + "zh": "请选择 Cloudflare Workers 套餐" + }, + "planSaved": { + "en": "Workers plan saved", + "zh": "Workers 套餐已保存" + }, + "single_database_limit": { + "en": "Database Capacity Limit", + "zh": "数据库容量上限" + }, + "storage_description": { + "en": "Compare the current database size with your plan limits.", + "zh": "将当前数据库大小与套餐容量上限进行对比" + }, + "storage_tip": { + "en": "Usage is calculated from the current database size and the selected plan limit.", + "zh": "使用率按当前数据库大小与所选套餐的数据库容量上限计算。" + }, + "storage_title": { + "en": "D1 Storage Capacity", + "zh": "D1 存储容量" + }, + "storage_usage": { + "en": "Capacity Usage", + "zh": "容量使用率" + }, + "unavailable": { + "en": "Unavailable", + "zh": "暂不可用" } }, "views.admin.IpBlacklistSettings": { diff --git a/frontend/src/views/admin/DatabaseManager.vue b/frontend/src/views/admin/DatabaseManager.vue index 01e8278..9aee395 100644 --- a/frontend/src/views/admin/DatabaseManager.vue +++ b/frontend/src/views/admin/DatabaseManager.vue @@ -1,5 +1,5 @@