重构 K8s 资源管理页面
1. 拆分原有的巨型 K8sResourceList.tsx 文件(1001行)为多个独立页面组件 2. 为每种 K8s 资源类型创建专门的页面: - Namespace: 简单名称输入创建 - Deployment/StatefulSet: YAML 文件上传创建 - Service: 显示"功能开发中"提示 - ConfigMap: Key-Value 编辑器创建(支持文件上传) - Pod: 无创建功能 - PV/PVC: YAML 文件上传创建 3. 创建共享组件和 Hooks 提高代码复用 4. 更新路由配置使用嵌套路由结构 5. 修复 PV/PVC 页面缺少组件导入的问题 优化了代码结构,使每个文件控制在合理大小范围内,便于维护和扩展。
This commit is contained in:
209
frontend/src/components/CreateConfigMapDialog.tsx
Normal file
209
frontend/src/components/CreateConfigMapDialog.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
IconButton,
|
||||
Typography,
|
||||
Stack,
|
||||
Alert,
|
||||
MenuItem,
|
||||
} from '@mui/material'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
||||
|
||||
interface ConfigMapData {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface CreateConfigMapDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSubmit: (name: string, namespace: string, data: Record<string, string>) => void
|
||||
namespaces: string[]
|
||||
}
|
||||
|
||||
export default function CreateConfigMapDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
namespaces,
|
||||
}: CreateConfigMapDialogProps) {
|
||||
const [name, setName] = useState('')
|
||||
const [namespace, setNamespace] = useState('')
|
||||
const [configMapData, setConfigMapData] = useState<ConfigMapData[]>([{ key: '', value: '' }])
|
||||
const [error, setError] = useState('')
|
||||
const fileInputRefs = useRef<(HTMLInputElement | null)[]>([])
|
||||
|
||||
const handleAddData = () => {
|
||||
setConfigMapData([...configMapData, { key: '', value: '' }])
|
||||
}
|
||||
|
||||
const handleRemoveData = (index: number) => {
|
||||
const newData = configMapData.filter((_, i) => i !== index)
|
||||
setConfigMapData(newData.length === 0 ? [{ key: '', value: '' }] : newData)
|
||||
}
|
||||
|
||||
const handleDataChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newData = [...configMapData]
|
||||
newData[index][field] = value
|
||||
setConfigMapData(newData)
|
||||
}
|
||||
|
||||
const handleFileUpload = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
const newData = [...configMapData]
|
||||
newData[index].value = content
|
||||
if (!newData[index].key) {
|
||||
newData[index].key = file.name
|
||||
}
|
||||
setConfigMapData(newData)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!name.trim()) {
|
||||
setError('请输入 ConfigMap 名称')
|
||||
return
|
||||
}
|
||||
if (!namespace) {
|
||||
setError('请选择 Namespace')
|
||||
return
|
||||
}
|
||||
|
||||
const hasEmptyKey = configMapData.some((d) => !d.key.trim())
|
||||
if (hasEmptyKey) {
|
||||
setError('所有 Key 不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
const keys = configMapData.map((d) => d.key)
|
||||
const uniqueKeys = new Set(keys)
|
||||
if (keys.length !== uniqueKeys.size) {
|
||||
setError('Key 不能重复')
|
||||
return
|
||||
}
|
||||
|
||||
const data: Record<string, string> = {}
|
||||
configMapData.forEach((d) => {
|
||||
data[d.key] = d.value
|
||||
})
|
||||
|
||||
onSubmit(name, namespace, data)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setName('')
|
||||
setNamespace('')
|
||||
setConfigMapData([{ key: '', value: '' }])
|
||||
setError('')
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>创建 ConfigMap</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
<TextField
|
||||
label="Namespace"
|
||||
select
|
||||
value={namespace}
|
||||
onChange={(e) => setNamespace(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
>
|
||||
{namespaces.map((ns) => (
|
||||
<MenuItem key={ns} value={ns}>
|
||||
{ns}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="ConfigMap 名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="subtitle1">Data</Typography>
|
||||
<Button startIcon={<AddIcon />} onClick={handleAddData} size="small">
|
||||
添加 Key
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{configMapData.map((data, index) => (
|
||||
<Box key={index} sx={{ mb: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 1, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Key"
|
||||
value={data.key}
|
||||
onChange={(e) => handleDataChange(index, 'key', e.target.value)}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={(el) => (fileInputRefs.current[index] = el)}
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFileUpload(index, e)}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => fileInputRefs.current[index]?.click()}
|
||||
title="从文件上传"
|
||||
>
|
||||
<UploadFileIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => handleRemoveData(index)}
|
||||
disabled={configMapData.length === 1}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Value"
|
||||
value={data.value}
|
||||
onChange={(e) => handleDataChange(index, 'value', e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} variant="contained">
|
||||
创建
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
83
frontend/src/components/k8s/CreateNamespaceDialog.tsx
Normal file
83
frontend/src/components/k8s/CreateNamespaceDialog.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Alert,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
interface CreateNamespaceDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSubmit: (name: string) => void
|
||||
}
|
||||
|
||||
export default function CreateNamespaceDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: CreateNamespaceDialogProps) {
|
||||
const [name, setName] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!name.trim()) {
|
||||
setError('请输入 Namespace 名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(name)) {
|
||||
setError('Namespace 名称只能包含小写字母、数字和连字符,且必须以字母或数字开头和结尾')
|
||||
return
|
||||
}
|
||||
|
||||
onSubmit(name)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setName('')
|
||||
setError('')
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">创建 Namespace</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
<TextField
|
||||
label="Namespace 名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
placeholder="例如: my-namespace"
|
||||
helperText="只能包含小写字母、数字和连字符"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} variant="contained">
|
||||
创建
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
101
frontend/src/components/k8s/CreateYamlDialog.tsx
Normal file
101
frontend/src/components/k8s/CreateYamlDialog.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Stack,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
||||
|
||||
interface CreateYamlDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onApply: (yaml: string) => void
|
||||
yamlContent: string
|
||||
onYamlChange: (value: string) => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function CreateYamlDialog({
|
||||
open,
|
||||
onClose,
|
||||
onApply,
|
||||
yamlContent,
|
||||
onYamlChange,
|
||||
loading,
|
||||
}: CreateYamlDialogProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
onYamlChange(content)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">创建资源</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<Box>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".yaml,.yml"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadFileIcon />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
上传 YAML 文件
|
||||
</Button>
|
||||
</Box>
|
||||
<TextField
|
||||
label="YAML 内容"
|
||||
multiline
|
||||
rows={20}
|
||||
value={yamlContent}
|
||||
onChange={(e) => onYamlChange(e.target.value)}
|
||||
placeholder="粘贴或编辑 YAML 内容..."
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => onApply(yamlContent)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : '应用'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
52
frontend/src/components/k8s/DeleteConfirmDialog.tsx
Normal file
52
frontend/src/components/k8s/DeleteConfirmDialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
|
||||
interface DeleteConfirmDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onConfirm: () => void
|
||||
resourceType: string
|
||||
resourceName: string
|
||||
namespace?: string
|
||||
deleting: boolean
|
||||
}
|
||||
|
||||
export default function DeleteConfirmDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
resourceType,
|
||||
resourceName,
|
||||
namespace,
|
||||
deleting,
|
||||
}: DeleteConfirmDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>确认删除</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
确定要删除 {resourceType} <strong>{resourceName}</strong>
|
||||
{namespace && resourceType !== 'Namespace' ? ` (namespace: ${namespace})` : ''} 吗?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={onConfirm}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? <CircularProgress size={24} /> : '删除'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
73
frontend/src/components/k8s/EditResourceDialog.tsx
Normal file
73
frontend/src/components/k8s/EditResourceDialog.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
interface EditResourceDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSave: (yaml: string) => void
|
||||
resourceType: string
|
||||
resourceName: string
|
||||
namespace: string
|
||||
yaml: string
|
||||
onYamlChange: (value: string) => void
|
||||
saving: boolean
|
||||
}
|
||||
|
||||
export default function EditResourceDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
resourceType,
|
||||
resourceName,
|
||||
namespace,
|
||||
yaml,
|
||||
onYamlChange,
|
||||
saving,
|
||||
}: EditResourceDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">
|
||||
编辑 {resourceType}: {resourceName} ({namespace})
|
||||
</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
multiline
|
||||
rows={20}
|
||||
value={yaml}
|
||||
onChange={(e) => onYamlChange(e.target.value)}
|
||||
placeholder="YAML 内容..."
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => onSave(yaml)}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? <CircularProgress size={24} /> : '保存'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
60
frontend/src/components/k8s/KubeconfigSettingsDrawer.tsx
Normal file
60
frontend/src/components/k8s/KubeconfigSettingsDrawer.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
Drawer,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
Divider,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
interface KubeconfigSettingsDrawerProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
kubeconfig: string
|
||||
onKubeconfigChange: (value: string) => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export default function KubeconfigSettingsDrawer({
|
||||
open,
|
||||
onClose,
|
||||
kubeconfig,
|
||||
onKubeconfigChange,
|
||||
onSave,
|
||||
}: KubeconfigSettingsDrawerProps) {
|
||||
return (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{ '& .MuiDrawer-paper': { width: 500 } }}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">集群配置</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Kubeconfig"
|
||||
multiline
|
||||
rows={20}
|
||||
value={kubeconfig}
|
||||
onChange={(e) => onKubeconfigChange(e.target.value)}
|
||||
placeholder="粘贴 kubeconfig 内容..."
|
||||
fullWidth
|
||||
/>
|
||||
<Button variant="contained" onClick={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
77
frontend/src/components/k8s/PodLogsDialog.tsx
Normal file
77
frontend/src/components/k8s/PodLogsDialog.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
Paper,
|
||||
} from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
interface PodLogsDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
podName: string
|
||||
namespace: string
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
export default function PodLogsDialog({
|
||||
open,
|
||||
onClose,
|
||||
podName,
|
||||
namespace,
|
||||
logs,
|
||||
}: PodLogsDialogProps) {
|
||||
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [logs])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">
|
||||
Pod 日志: {podName} ({namespace})
|
||||
</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: '#1e1e1e',
|
||||
color: '#d4d4d4',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<Typography color="inherit">等待日志...</Typography>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<Box key={index} sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{log}
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</Paper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user