feat: complete OCI registry implementation with docker push/pull support
A lightweight OCI (Open Container Initiative) registry implementation written in Go.
This commit is contained in:
55
frontend/src/App.tsx
Normal file
55
frontend/src/App.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Container, Typography, Box, AppBar, Toolbar, Button, Stack } from '@mui/material'
|
||||
import { Routes, Route, Link } from 'react-router-dom'
|
||||
import { useAppStore } from './stores/appStore'
|
||||
import RegistryImageList from './pages/RegistryImageList'
|
||||
|
||||
function App() {
|
||||
const { count, increment, decrement, reset } = useAppStore()
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, minHeight: '100vh' }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Cluster
|
||||
</Typography>
|
||||
<Button color="inherit" component={Link} to="/">首页</Button>
|
||||
<Button color="inherit" component={Link} to="/registry/image">镜像列表</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Routes>
|
||||
<Route path="/registry/image" element={<RegistryImageList />} />
|
||||
<Route path="/" element={
|
||||
<Box>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
欢迎使用 Cluster
|
||||
</Typography>
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Zustand 状态管理示例
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} alignItems="center" justifyContent="center" sx={{ mt: 2 }}>
|
||||
<Button variant="contained" onClick={decrement}>
|
||||
-
|
||||
</Button>
|
||||
<Typography variant="h5" sx={{ minWidth: 60 }}>
|
||||
{count}
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={increment}>
|
||||
+
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={reset} sx={{ ml: 2 }}>
|
||||
重置
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
} />
|
||||
</Routes>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
0
frontend/src/components/.gitkeep
Normal file
0
frontend/src/components/.gitkeep
Normal file
15
frontend/src/index.css
Normal file
15
frontend/src/index.css
Normal file
@@ -0,0 +1,15 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'LXGW WenKai', '霞鹜文楷', Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
18
frontend/src/main.tsx
Normal file
18
frontend/src/main.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App.tsx'
|
||||
import { theme } from './theme'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
85
frontend/src/pages/RegistryImageList.tsx
Normal file
85
frontend/src/pages/RegistryImageList.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, CircularProgress, Alert } from '@mui/material'
|
||||
|
||||
interface RegistryImage {
|
||||
id: number
|
||||
name: string
|
||||
upload_time: string
|
||||
size: number
|
||||
}
|
||||
|
||||
// Format bytes to human readable format
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
export default function RegistryImageList() {
|
||||
const [images, setImages] = useState<RegistryImage[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let abort = false
|
||||
async function fetchImages() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch('/api/v1/registry/image/list')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const result = await res.json()
|
||||
// Backend returns: {status, msg, data: {images: [...]}}
|
||||
const list: RegistryImage[] = result.data?.images || []
|
||||
if (!abort) setImages(list)
|
||||
} catch (e: any) {
|
||||
if (!abort) setError(e.message)
|
||||
} finally {
|
||||
if (!abort) setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchImages()
|
||||
return () => { abort = true }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>镜像列表</Typography>
|
||||
{loading && <CircularProgress />}
|
||||
{error && <Alert severity="error">加载失败: {error}</Alert>}
|
||||
{!loading && !error && (
|
||||
<Paper>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>名称</TableCell>
|
||||
<TableCell>上传时间</TableCell>
|
||||
<TableCell>大小</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{images.map(img => (
|
||||
<TableRow key={img.id} hover>
|
||||
<TableCell>{img.id}</TableCell>
|
||||
<TableCell>{img.name}</TableCell>
|
||||
<TableCell>{img.upload_time}</TableCell>
|
||||
<TableCell>{formatSize(img.size)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{images.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center">暂无镜像</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
15
frontend/src/stores/appStore.ts
Normal file
15
frontend/src/stores/appStore.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface AppState {
|
||||
count: number
|
||||
increment: () => void
|
||||
decrement: () => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set) => ({
|
||||
count: 0,
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
decrement: () => set((state) => ({ count: state.count - 1 })),
|
||||
reset: () => set({ count: 0 }),
|
||||
}))
|
||||
27
frontend/src/theme.ts
Normal file
27
frontend/src/theme.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createTheme } from '@mui/material/styles'
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
},
|
||||
secondary: {
|
||||
main: '#dc004e',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'"LXGW WenKai"',
|
||||
'"霞鹜文楷"',
|
||||
'Inter',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
].join(','),
|
||||
},
|
||||
})
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user