feat(admin): show storage policies
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
98
front/src/admin/storage-policies-list.tsx
Normal file
98
front/src/admin/storage-policies-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user