✨ Quota and better drive dashboard
This commit is contained in:
@@ -122,7 +122,7 @@ function renderPoolSelectLabel(option: SelectOption & SnFilePool) {
|
||||
),
|
||||
policy.require_privilege &&
|
||||
h('span', `Require ${perkPrivilegeList[policy.require_privilege - 1]} Program`),
|
||||
h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)} NSD`),
|
||||
h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)}`),
|
||||
]
|
||||
.filter((el) => el)
|
||||
.flatMap((el, idx, arr) =>
|
||||
|
@@ -64,7 +64,7 @@ const guestOptions = [
|
||||
|
||||
const userOptions = computed(() => [
|
||||
{
|
||||
label: 'Usage',
|
||||
label: 'Dashboard',
|
||||
key: 'dashboardUsage',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
|
@@ -3,7 +3,16 @@
|
||||
<div class="h-full flex justify-center items-center" v-if="!usage">
|
||||
<n-spin />
|
||||
</div>
|
||||
<n-grid cols="1 s:2 m:3 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else>
|
||||
<n-grid cols="1 s:2 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else>
|
||||
<n-gi span="4">
|
||||
<n-alert title="Billing Tips" size="small" type="info" closable>
|
||||
<p>
|
||||
The minimal billable unit is MiB, if your file is not enough 1 MiB it will be counted as
|
||||
1 MiB.
|
||||
</p>
|
||||
<p>The <b>1 MiB = 1024 KiB = 1,048,576 B</b></p>
|
||||
</n-alert>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="All Uploads" tabular-nums>
|
||||
@@ -25,23 +34,45 @@
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="Cost" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_cost" :precision="2" />
|
||||
<template #suffix>NSD</template>
|
||||
<n-statistic label="Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_quota" />
|
||||
<template #suffix>MiB</template>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="Pools" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.pool_usages.length" />
|
||||
</n-statistic>
|
||||
<div class="flex gap-2 justify-between items-end">
|
||||
<n-statistic label="Used Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="quotaUsagePercentage" :precision="2" />
|
||||
<template #suffix>%</template>
|
||||
</n-statistic>
|
||||
<n-progress
|
||||
type="circle"
|
||||
:percentage="quotaUsagePercentage"
|
||||
:show-indicator="false"
|
||||
:stroke-width="16"
|
||||
style="width: 40px"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="ratio-video" title="Pool Usage">
|
||||
<n-card class="aspect-video" title="Pool Usage">
|
||||
<pie
|
||||
:data="chartData"
|
||||
:data="poolChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
|
||||
}"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="aspect-video h-full" title="Verbose Quota">
|
||||
<pie
|
||||
:data="quotaChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
@@ -55,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation } from 'naive-ui'
|
||||
import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation, NAlert, NProgress } from 'naive-ui'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement } from 'chart.js'
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
@@ -66,7 +97,7 @@ ChartJS.register(Title, Tooltip, Legend, ArcElement)
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isDesktop = breakpoints.greaterOrEqual('md')
|
||||
|
||||
const chartData = computed(() => ({
|
||||
const poolChartData = computed(() => ({
|
||||
labels: usage.value.pool_usages.map((pool: any) => pool.pool_name),
|
||||
datasets: [
|
||||
{
|
||||
@@ -74,7 +105,7 @@ const chartData = computed(() => ({
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: usage.value.pool_usages.map((pool: any) => pool.usage_bytes),
|
||||
},
|
||||
]
|
||||
],
|
||||
}))
|
||||
|
||||
const usage = ref<any>()
|
||||
@@ -91,6 +122,36 @@ async function fetchUsage() {
|
||||
}
|
||||
onMounted(() => fetchUsage())
|
||||
|
||||
const verboseQuota = ref<
|
||||
{ based_quota: number; extra_quota: number; total_quota: number } | undefined
|
||||
>()
|
||||
async function fetchVerboseQuota() {
|
||||
try {
|
||||
const response = await fetch('/api/billing/quota')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
verboseQuota.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch verbose data:', error)
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchVerboseQuota())
|
||||
|
||||
const quotaChartData = computed(() => ({
|
||||
labels: ['Base Quota', 'Extra Quota'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Verbose Quota',
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: [verboseQuota.value?.based_quota ?? 0, verboseQuota.value?.extra_quota ?? 0],
|
||||
},
|
||||
],
|
||||
}))
|
||||
const quotaUsagePercentage = computed(
|
||||
() => (usage.value.used_quota / usage.value.total_quota) * 100,
|
||||
)
|
||||
|
||||
function toGigabytes(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="h-full relative flex items-center justify-center">
|
||||
<n-card class="max-w-lg" title="About" v-if="!userStore.user">
|
||||
<n-card class="max-w-lg my-4 mx-8" title="About" v-if="!userStore.user">
|
||||
<p>Welcome to the <b>Solar Drive</b></p>
|
||||
<p>We help you upload, collect, and share files with ease in mind.</p>
|
||||
<p>To continue, login first.</p>
|
||||
@@ -22,6 +22,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-collapse-transition :show="showRecycleHint">
|
||||
<n-alert size="small" type="warning" title="Recycle Enabled" class="mb-3">
|
||||
You're uploading to a pool which enabled recycle. If the file you uploaded didn't
|
||||
referenced from the Solar Network. It will be marked and will be deleted some while later.
|
||||
</n-alert>
|
||||
</n-collapse-transition>
|
||||
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
@@ -107,6 +114,7 @@ import {
|
||||
NSwitch,
|
||||
NCollapseTransition,
|
||||
NDatePicker,
|
||||
NAlert,
|
||||
type UploadCustomRequestOptions,
|
||||
type UploadSettledFileInfo,
|
||||
type UploadFileInfo,
|
||||
@@ -149,6 +157,10 @@ const currentFilePool = computed(() => {
|
||||
if (!filePool.value) return null
|
||||
return pools.value?.find((pool) => pool.id === filePool.value) ?? null
|
||||
})
|
||||
const showRecycleHint = computed(() => {
|
||||
if (!filePool.value) return true
|
||||
return currentFilePool.value.policy_config?.enable_recycle || false
|
||||
})
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
|
Reference in New Issue
Block a user