diff --git a/.gitignore b/.gitignore index d72fe82..c7e7693 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ cluster # IDE .idea/ .vscode/ +.qoder/ *.swp *.swo *~ diff --git a/frontend/src/pages/RegistryImageList.tsx b/frontend/src/pages/RegistryImageList.tsx index 2b92d5c..826c7c8 100644 --- a/frontend/src/pages/RegistryImageList.tsx +++ b/frontend/src/pages/RegistryImageList.tsx @@ -23,12 +23,15 @@ import { DialogContent, DialogActions, LinearProgress, + Checkbox, + FormControlLabel, } from '@mui/material' import MuiAlert, { AlertProps } from '@mui/material/Alert' import DownloadIcon from '@mui/icons-material/Download' import UploadIcon from '@mui/icons-material/Upload' import SettingsIcon from '@mui/icons-material/Settings' import CloseIcon from '@mui/icons-material/Close' +import CloudDownloadIcon from '@mui/icons-material/CloudDownload' import { useSettingsStore } from '../stores/settingsStore' const SnackbarAlert = forwardRef(function SnackbarAlert( @@ -73,20 +76,28 @@ export default function RegistryImageList() { const [error, setError] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) const [uploadOpen, setUploadOpen] = useState(false) + const [fetchImageOpen, setFetchImageOpen] = useState(false) const [selectedFile, setSelectedFile] = useState(null) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [downloadingImage, setDownloadingImage] = useState(null) + const [fetchingImage, setFetchingImage] = useState(false) + const [fetchImageName, setFetchImageName] = useState('') + const [useProxy, setUseProxy] = useState(false) + const [fetchProxyInput, setFetchProxyInput] = useState('') const { - registryAddress, + registryAddress, + registryProxy, loading: configLoading, snackbar, fetchConfig, setRegistryAddress, + setRegistryProxy, hideSnackbar, showSnackbar } = useSettingsStore() const [registryAddressInput, setRegistryAddressInput] = useState(registryAddress) + const [registryProxyInput, setRegistryProxyInput] = useState(registryProxy) const [saving, setSaving] = useState(false) useEffect(() => { @@ -132,9 +143,9 @@ export default function RegistryImageList() { // Get filename from Content-Disposition header or use default const contentDisposition = response.headers.get('Content-Disposition') - let filename = `${imageName.replace(/\//g, '_')}-latest.tar` + let filename = `${imageName.replace(/\//g, '_')}.tar` if (contentDisposition) { - const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i) + const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/i) if (filenameMatch) { filename = filenameMatch[1] } @@ -164,6 +175,7 @@ export default function RegistryImageList() { try { setSaving(true) await setRegistryAddress(registryAddressInput) + await setRegistryProxy(registryProxyInput) setSettingsOpen(false) } catch (error: any) { // Error already handled in store @@ -174,6 +186,7 @@ export default function RegistryImageList() { const handleCloseSettings = () => { setRegistryAddressInput(registryAddress) + setRegistryProxyInput(registryProxy) setSettingsOpen(false) } @@ -181,10 +194,67 @@ export default function RegistryImageList() { setRegistryAddressInput(registryAddress) }, [registryAddress]) + useEffect(() => { + setRegistryProxyInput(registryProxy) + }, [registryProxy]) + useEffect(() => { fetchConfig() }, [fetchConfig]) + useEffect(() => { + setFetchProxyInput(registryProxy) + }, [registryProxy]) + + const handleFetchImage = async () => { + if (!fetchImageName.trim()) { + showSnackbar('请输入镜像名称', 'warning') + return + } + + setFetchingImage(true) + showSnackbar('开始获取镜像...', 'info') + + try { + const response = await fetch('/api/v1/registry/image/fetch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + image: fetchImageName, + proxy: useProxy ? fetchProxyInput : '', + }), + }) + + const result = await response.json() + if (response.ok) { + showSnackbar(result.data?.message || '镜像获取成功', 'success') + setFetchImageOpen(false) + setFetchImageName('') + setUseProxy(false) + setFetchProxyInput(registryProxy) + // Refresh image list + window.location.reload() + } else { + showSnackbar(result.err || result.msg || '获取失败', 'error') + } + } catch (error: any) { + showSnackbar(`请求失败: ${error.message}`, 'error') + } finally { + setFetchingImage(false) + } + } + + const handleCloseFetchImage = () => { + if (!fetchingImage) { + setFetchImageOpen(false) + setFetchImageName('') + setUseProxy(false) + setFetchProxyInput(registryProxy) + } + } + const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (file && file.name.endsWith('.tar')) { @@ -258,6 +328,13 @@ export default function RegistryImageList() { 镜像列表 + + + + + {/* Settings Drawer */} + setRegistryProxyInput(e.target.value)} + fullWidth + helperText="设置镜像代理地址,格式: ://[:]" + variant="outlined" + disabled={configLoading || saving} + /> +