feat: enhance adapter usage tracking with new interface and display capacity usage in AdaptersPage

This commit is contained in:
shiyu
2026-05-02 22:47:22 +08:00
parent a8737b883e
commit 1f608974dc
5 changed files with 48 additions and 18 deletions

View File

@@ -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<AdapterItem[]>('/adapters'),
create: (payload: Omit<AdapterItem, 'id'>) => request<AdapterItem>('/adapters', { method: 'POST', json: payload }),

View File

@@ -71,7 +71,7 @@ async function request<T = any>(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;

View File

@@ -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",

View File

@@ -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 节点",

View File

@@ -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<AdapterItem[]>([]);
const [usageMap, setUsageMap] = useState<Record<number, AdapterUsage>>({});
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<AdapterItem | null>(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',