From 1f608974dc17d1144adffac33e454045048d582b Mon Sep 17 00:00:00 2001 From: shiyu Date: Sat, 2 May 2026 22:47:22 +0800 Subject: [PATCH] feat: enhance adapter usage tracking with new interface and display capacity usage in AdaptersPage --- web/src/api/adapters.ts | 28 ++++++++++++++-------------- web/src/api/client.ts | 2 +- web/src/i18n/locales/en.json | 2 ++ web/src/i18n/locales/zh.json | 2 ++ web/src/pages/AdaptersPage.tsx | 32 +++++++++++++++++++++++++++++--- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/web/src/api/adapters.ts b/web/src/api/adapters.ts index b26286f..2818d78 100644 --- a/web/src/api/adapters.ts +++ b/web/src/api/adapters.ts @@ -10,6 +10,20 @@ export interface AdapterItem { sub_path?: string | null; } +export interface AdapterUsage { + id: number; + name: string; + type: string; + path: string; + supported: boolean; + used_bytes?: number | null; + total_bytes?: number | null; + free_bytes?: number | null; + source?: string | null; + scope?: string | null; + reason?: string | null; +} + export interface AdapterTypeField { key: string; label: string; @@ -25,20 +39,6 @@ export interface AdapterTypeMeta { config_schema: AdapterTypeField[]; } -export interface AdapterUsage { - id: number; - name: string; - type: string; - path: string; - supported: boolean; - used_bytes?: number | null; - total_bytes?: number | null; - free_bytes?: number | null; - source?: string | null; - scope?: string | null; - reason?: string | null; -} - export const adaptersApi = { list: () => request('/adapters'), create: (payload: Omit) => request('/adapters', { method: 'POST', json: payload }), diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 2351c0e..6689f52 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -71,7 +71,7 @@ async function request(url: string, options: RequestOptions = {}): Prom } export { vfsApi, type VfsEntry, type DirListing } from './vfs'; -export { adaptersApi, type AdapterItem, type AdapterTypeField, type AdapterTypeMeta } from './adapters'; +export { adaptersApi, type AdapterItem, type AdapterTypeField, type AdapterTypeMeta, type AdapterUsage } from './adapters'; export { shareApi, type ShareInfo, type ShareInfoWithPassword } from './share'; export { offlineDownloadsApi, type OfflineDownloadTask, type OfflineDownloadCreate, type TaskProgress } from './offlineDownloads'; export default request; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 51c4fd1..8bc6197 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -514,6 +514,8 @@ "Unique name": "Unique name", "Select adapter type": "Select adapter type", "/ or /drive": "/ or /drive", + "Used Capacity": "Used Capacity", + "Capacity Usage": "Capacity Usage", "Adapter Config": "Adapter Config", "adapter.type.local": "Local Filesystem", "adapter.type.foxel": "Foxel Node", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 5961144..a8584de 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -513,6 +513,8 @@ "Unique name": "唯一名称", "Select adapter type": "选择适配器类型", "/ or /drive": "/或/drive", + "Used Capacity": "已使用容量", + "Capacity Usage": "容量使用", "Adapter Config": "适配器配置", "adapter.type.local": "本地文件系统", "adapter.type.foxel": "Foxel 节点", diff --git a/web/src/pages/AdaptersPage.tsx b/web/src/pages/AdaptersPage.tsx index 9c41e29..235fefd 100644 --- a/web/src/pages/AdaptersPage.tsx +++ b/web/src/pages/AdaptersPage.tsx @@ -1,12 +1,29 @@ import { memo, useState, useEffect, useCallback } from 'react'; import { Table, Button, Space, Drawer, Form, Input, Switch, message, Typography, Popconfirm, Select } from 'antd'; import PageCard from '../components/PageCard'; -import { adaptersApi, type AdapterItem, type AdapterTypeMeta } from '../api/client'; +import { adaptersApi, type AdapterItem, type AdapterTypeMeta, type AdapterUsage } from '../api/client'; import { useI18n } from '../i18n'; +const formatBytes = (bytes?: number | null) => { + if (bytes === null || bytes === undefined) return '-'; + if (bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); + const value = bytes / (1024 ** index); + return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`; +}; + +const formatUsage = (usage?: AdapterUsage) => { + if (!usage?.supported || usage.used_bytes === null || usage.used_bytes === undefined) return '-'; + const used = formatBytes(usage.used_bytes); + if (usage.total_bytes === null || usage.total_bytes === undefined) return used; + return `${used} / ${formatBytes(usage.total_bytes)}`; +}; + const AdaptersPage = memo(function AdaptersPage() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); + const [usageMap, setUsageMap] = useState>({}); const [open, setOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); @@ -16,12 +33,14 @@ const AdaptersPage = memo(function AdaptersPage() { const fetchList = useCallback(async () => { setLoading(true); try { - const [list, types] = await Promise.all([ + const [list, types, usages] = await Promise.all([ adaptersApi.list(), - adaptersApi.available() + adaptersApi.available(), + adaptersApi.usage() ]); setData(list); setAvailableTypes(types); + setUsageMap(Object.fromEntries(usages.map(item => [item.id, item]))); } catch (e: any) { message.error(e.message || t('Load failed')); } finally { @@ -142,6 +161,13 @@ const AdaptersPage = memo(function AdaptersPage() { { title: t('Type'), dataIndex: 'type', width: 140, render: (value: string) => renderTypeLabel(value) }, { title: t('Mount Path'), dataIndex: 'path', width: 140, render: (v: string) => v || '-' }, { title: t('Sub Path'), dataIndex: 'sub_path', width: 140, render: (v: string) => v || '-' }, + { + title: t('Capacity Usage'), + width: 180, + render: (_: any, rec: AdapterItem) => { + return formatUsage(usageMap[rec.id]); + } + }, { title: t('Enabled'), dataIndex: 'enabled',