feat(admin): show storage policies

This commit is contained in:
yoyuzh
2026-04-08 21:54:22 +08:00
parent 3e67760712
commit c5362ebe31
14 changed files with 282 additions and 3 deletions

View File

@@ -1,11 +1,13 @@
import FolderOutlined from '@mui/icons-material/FolderOutlined';
import GroupsOutlined from '@mui/icons-material/GroupsOutlined';
import StorageRounded from '@mui/icons-material/StorageRounded';
import { Admin, Resource } from 'react-admin';
import { portalAdminAuthProvider } from './auth-provider';
import { portalAdminDataProvider } from './data-provider';
import { PortalAdminDashboard } from './dashboard';
import { PortalAdminFilesList } from './files-list';
import { PortalAdminStoragePoliciesList } from './storage-policies-list';
import { PortalAdminUsersList } from './users-list';
export default function PortalAdminApp() {
@@ -33,6 +35,13 @@ export default function PortalAdminApp() {
options={{ label: '文件资源' }}
recordRepresentation="filename"
/>
<Resource
name="storagePolicies"
icon={StorageRounded}
list={PortalAdminStoragePoliciesList}
options={{ label: '存储策略' }}
recordRepresentation="name"
/>
</Admin>
);
}

View File

@@ -6,6 +6,7 @@ import type { AdminFile, PageResponse } from '@/src/lib/types';
import {
buildAdminListPath,
buildFilesListPath,
buildStoragePoliciesListPath,
mapFilesListResponse,
} from './data-provider';
@@ -106,3 +107,16 @@ test('buildAdminListPath rejects the removed school snapshots resource', () => {
/schoolSnapshots/,
);
});
test('buildStoragePoliciesListPath maps react-admin pagination to the backend storage policies list query', () => {
assert.equal(
buildStoragePoliciesListPath({
pagination: {
page: 2,
perPage: 10,
},
filter: {},
}),
'/admin/storage-policies?page=1&size=10',
);
});

View File

@@ -3,11 +3,13 @@ import type { DataProvider, GetListParams, GetListResult, Identifier } from 'rea
import { apiRequest } from '@/src/lib/api';
import type {
AdminFile,
AdminStoragePolicy,
AdminUser,
PageResponse,
} from '@/src/lib/types';
const FILES_RESOURCE = 'files';
const STORAGE_POLICIES_RESOURCE = 'storagePolicies';
const USERS_RESOURCE = 'users';
function createUnsupportedError(resource: string, action: string) {
@@ -15,7 +17,7 @@ function createUnsupportedError(resource: string, action: string) {
}
function ensureSupportedResource(resource: string, action: string) {
if (![FILES_RESOURCE, USERS_RESOURCE].includes(resource)) {
if (![FILES_RESOURCE, STORAGE_POLICIES_RESOURCE, USERS_RESOURCE].includes(resource)) {
throw createUnsupportedError(resource, action);
}
}
@@ -54,6 +56,12 @@ export function buildFilesListPath(params: Pick<GetListParams, 'pagination' | 'f
return `/admin/files?${search.toString()}`;
}
export function buildStoragePoliciesListPath(params: Pick<GetListParams, 'pagination' | 'filter'>) {
const page = Math.max(0, params.pagination.page - 1);
const size = Math.max(1, params.pagination.perPage);
return `/admin/storage-policies?page=${page}&size=${size}`;
}
export function mapFilesListResponse(
payload: PageResponse<AdminFile>,
): GetListResult<AdminFile> {
@@ -86,6 +94,14 @@ export const portalAdminDataProvider: DataProvider = {
} as GetListResult;
}
if (resource === STORAGE_POLICIES_RESOURCE) {
const payload = await apiRequest<AdminStoragePolicy[]>(buildStoragePoliciesListPath(params));
return {
data: payload,
total: payload.length,
} as GetListResult;
}
throw createUnsupportedError(resource, 'list');
},
getOne: async (resource) => {

View File

@@ -0,0 +1,98 @@
import { Chip, Stack } from '@mui/material';
import {
BooleanField,
Datagrid,
DateField,
FunctionField,
List,
RefreshButton,
TextField,
TopToolbar,
} from 'react-admin';
import type { AdminStoragePolicy, StoragePolicyCapabilities } from '@/src/lib/types';
const CAPABILITY_LABELS: Array<{ key: keyof StoragePolicyCapabilities; label: string }> = [
{ key: 'directUpload', label: '直传' },
{ key: 'multipartUpload', label: '分片' },
{ key: 'signedDownloadUrl', label: '签名下载' },
{ key: 'serverProxyDownload', label: '服务端下载' },
{ key: 'thumbnailNative', label: '原生缩略图' },
{ key: 'friendlyDownloadName', label: '友好文件名' },
{ key: 'requiresCors', label: 'CORS' },
{ key: 'supportsInternalEndpoint', label: '内网 endpoint' },
];
function StoragePoliciesListActions() {
return (
<TopToolbar>
<RefreshButton />
</TopToolbar>
);
}
function formatFileSize(size: number) {
if (size >= 1024 * 1024 * 1024) {
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
if (size >= 1024 * 1024) {
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
}
if (size >= 1024) {
return `${(size / 1024).toFixed(1)} KB`;
}
return `${size} B`;
}
function renderCapabilities(capabilities: StoragePolicyCapabilities) {
return (
<Stack direction="row" flexWrap="wrap" gap={0.5}>
{CAPABILITY_LABELS.map(({ key, label }) => {
const enabled = capabilities[key] === true;
return (
<Chip
key={key}
color={enabled ? 'success' : 'default'}
label={`${label}${enabled ? '开' : '关'}`}
size="small"
variant={enabled ? 'filled' : 'outlined'}
/>
);
})}
</Stack>
);
}
export function PortalAdminStoragePoliciesList() {
return (
<List
actions={<StoragePoliciesListActions />}
perPage={25}
resource="storagePolicies"
title="存储策略"
sort={{ field: 'id', order: 'ASC' }}
>
<Datagrid bulkActionButtons={false} rowClick={false}>
<TextField source="id" label="ID" />
<TextField source="name" label="名称" />
<TextField source="type" label="类型" />
<TextField source="bucketName" label="Bucket" emptyText="-" />
<TextField source="endpoint" label="Endpoint" emptyText="-" />
<TextField source="region" label="Region" emptyText="-" />
<TextField source="prefix" label="Prefix" emptyText="-" />
<TextField source="credentialMode" label="凭证模式" />
<BooleanField source="enabled" label="启用" />
<BooleanField source="defaultPolicy" label="默认" />
<FunctionField<AdminStoragePolicy>
label="容量上限"
render={(record) => formatFileSize(record.maxSizeBytes)}
/>
<FunctionField<AdminStoragePolicy>
label="能力"
render={(record) => renderCapabilities(record.capabilities)}
/>
<DateField source="updatedAt" label="更新时间" showTime />
</Datagrid>
</List>
);
}

View File

@@ -72,6 +72,39 @@ export interface AdminFile {
ownerEmail: string;
}
export type StoragePolicyType = 'LOCAL' | 'S3_COMPATIBLE';
export type StoragePolicyCredentialMode = 'NONE' | 'STATIC' | 'DOGECLOUD_TEMP';
export interface StoragePolicyCapabilities {
directUpload: boolean;
multipartUpload: boolean;
signedDownloadUrl: boolean;
serverProxyDownload: boolean;
thumbnailNative: boolean;
friendlyDownloadName: boolean;
requiresCors: boolean;
supportsInternalEndpoint: boolean;
maxObjectSize: number;
}
export interface AdminStoragePolicy {
id: number;
name: string;
type: StoragePolicyType;
bucketName: string | null;
endpoint: string | null;
region: string | null;
privateBucket: boolean;
prefix: string | null;
credentialMode: StoragePolicyCredentialMode;
maxSizeBytes: number;
capabilities: StoragePolicyCapabilities;
enabled: boolean;
defaultPolicy: boolean;
createdAt: string;
updatedAt: string;
}
export interface AdminPasswordResetResponse {
temporaryPassword: string;
}