mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-29 03:57:05 +08:00
feat: show D1 storage capacity in admin (#1117)
* feat: show D1 storage usage in admin * test: add D1 storage E2E coverage * test: use exact D1 storage labels
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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日間のアクティブなアドレス数",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
@@ -7,24 +7,95 @@ import { api } from '../../api'
|
||||
import { init } from 'vooks/lib/on-fonts-ready';
|
||||
|
||||
const message = useMessage()
|
||||
const D1_STORAGE_PLAN_CONFIG_KEY = 'd1_storage_plan'
|
||||
const dbVersionData = ref({
|
||||
need_initialization: false,
|
||||
need_migration: false,
|
||||
current_db_version: '',
|
||||
code_db_version: ''
|
||||
code_db_version: '',
|
||||
database_size: null
|
||||
})
|
||||
const selectedPlan = ref(null)
|
||||
const savedPlan = ref(null)
|
||||
const savingPlan = ref(false)
|
||||
|
||||
const planOptions = computed(() => [
|
||||
{
|
||||
label: t('free_plan'),
|
||||
value: 'free',
|
||||
databaseLimit: 500 * 1024 ** 2
|
||||
},
|
||||
{
|
||||
label: t('paid_plan'),
|
||||
value: 'paid',
|
||||
databaseLimit: 10 * 1024 ** 3
|
||||
}
|
||||
])
|
||||
|
||||
const selectedPlanDetails = computed(() => (
|
||||
planOptions.value.find((plan) => plan.value === selectedPlan.value)
|
||||
))
|
||||
|
||||
const storagePercentage = computed(() => {
|
||||
if (!selectedPlanDetails.value || dbVersionData.value.database_size === null) return 0
|
||||
return dbVersionData.value.database_size / selectedPlanDetails.value.databaseLimit * 100
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => Math.min(storagePercentage.value, 100))
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
if (storagePercentage.value >= 90) return 'error'
|
||||
if (storagePercentage.value >= 75) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const { t } = useScopedI18n('views.admin.DatabaseManager')
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === null || bytes === undefined) return t('unavailable')
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const value = bytes / 1024 ** unitIndex
|
||||
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch('/admin/db_version');
|
||||
if (res) Object.assign(dbVersionData.value, res);
|
||||
const [versionRes, configRes] = await Promise.all([
|
||||
api.fetch('/admin/db_version'),
|
||||
api.fetch(`/admin/config/${D1_STORAGE_PLAN_CONFIG_KEY}`)
|
||||
]);
|
||||
if (versionRes) Object.assign(dbVersionData.value, versionRes);
|
||||
|
||||
const configuredPlan = configRes?.value
|
||||
if (planOptions.value.some((plan) => plan.value === configuredPlan)) {
|
||||
selectedPlan.value = configuredPlan
|
||||
savedPlan.value = configuredPlan
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const savePlan = async (plan) => {
|
||||
savingPlan.value = true
|
||||
try {
|
||||
await api.fetch('/admin/config', {
|
||||
method: 'POST',
|
||||
body: { key: D1_STORAGE_PLAN_CONFIG_KEY, value: plan }
|
||||
})
|
||||
savedPlan.value = plan
|
||||
message.success(t('planSaved'))
|
||||
} catch (error) {
|
||||
selectedPlan.value = savedPlan.value
|
||||
message.error(error.message || "error")
|
||||
} finally {
|
||||
savingPlan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const initialization = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_initialize', {
|
||||
@@ -77,6 +148,58 @@ onMounted(async () => {
|
||||
</span>
|
||||
</n-alert>
|
||||
|
||||
<div class="storage-panel">
|
||||
<div class="storage-heading">
|
||||
<div>
|
||||
<h3>{{ t('storage_title') }}</h3>
|
||||
<p>{{ t('storage_description') }}</p>
|
||||
</div>
|
||||
<div class="plan-select">
|
||||
<span>{{ t('plan') }}</span>
|
||||
<n-select
|
||||
v-model:value="selectedPlan"
|
||||
:options="planOptions"
|
||||
:placeholder="t('plan_placeholder')"
|
||||
:disabled="dbVersionData.need_initialization"
|
||||
:loading="savingPlan"
|
||||
@update:value="savePlan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-grid cols="1 s:2" responsive="screen" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item>
|
||||
<div class="storage-stat">
|
||||
<span>{{ t('current_database_size') }}</span>
|
||||
<strong>{{ formatBytes(dbVersionData.database_size) }}</strong>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="storage-stat">
|
||||
<span>{{ t('single_database_limit') }}</span>
|
||||
<strong>{{ selectedPlanDetails ? formatBytes(selectedPlanDetails.databaseLimit) : '—' }}</strong>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<div v-if="selectedPlanDetails" class="storage-progress">
|
||||
<div class="storage-progress-label">
|
||||
<span>{{ t('storage_usage') }}</span>
|
||||
<span>{{ storagePercentage.toFixed(2) }}%</span>
|
||||
</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:status="progressStatus"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-alert class="storage-tip" type="default" :show-icon="false" :bordered="false">
|
||||
{{ t('storage_tip') }}
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -100,4 +223,88 @@ onMounted(async () => {
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.storage-panel {
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--n-border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.storage-heading {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.storage-heading h3,
|
||||
.storage-heading p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.storage-heading p {
|
||||
margin-top: 4px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.plan-select {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.plan-select > span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.storage-stat {
|
||||
display: flex;
|
||||
min-height: 72px;
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-radius: var(--n-border-radius);
|
||||
}
|
||||
|
||||
.storage-stat span {
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.storage-stat strong {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
|
||||
.storage-progress {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.storage-progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.storage-tip {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.storage-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plan-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user