init project

This commit is contained in:
Ybehrooz
2025-08-28 20:29:35 +03:30
parent 3b54907b20
commit e4d83eb1bf
46 changed files with 7238 additions and 1 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,8 @@
{
"hash": "c2915b3a",
"configHash": "e9a24537",
"lockfileHash": "e3b0c442",
"browserHash": "38dbafaf",
"optimized": {},
"chunks": {}
}

3
.vite/deps/package.json Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -1,2 +1,15 @@
# vclusterfront_react # K8s Dashboard (React + Vite + Tailwind)
Minimal Kubernetes dashboard with auth, sidebar navigation, and a Create Cluster page. Clusters are stored in localStorage.
## Setup
```
npm install
npm run dev
```
Pages:
- `/login` and `/register`
- `/app/create-cluster` (create/list clusters)
- `/app/resources` and `/app/settings`

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>K8s Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4253
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tailwindcss/postcss": "^4.1.12",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.12",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}

7
postcss.config.js Normal file
View File

@@ -0,0 +1,7 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

63
src/App.tsx Normal file
View File

@@ -0,0 +1,63 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import Login from './pages/Login'
import Register from './pages/Register'
import DashboardLayout from './layout/DashboardLayout'
import AuthGate from './middleware/AuthGate'
import CreateCluster from './pages/CreateCluster'
import ClusterDetail from './pages/ClusterDetail'
import Namespaces from './pages/Namespaces'
import Nodes from './pages/Nodes'
import Pods from './pages/Pods'
import Deployments from './pages/Deployments'
import ReplicaSets from './pages/ReplicaSets'
import StatefulSets from './pages/StatefulSets'
import DaemonSets from './pages/DaemonSets'
import Jobs from './pages/Jobs'
import CronJobs from './pages/CronJobs'
import ReplicationControllers from './pages/ReplicationControllers'
import Services from './pages/Services'
import ConfigMaps from './pages/ConfigMaps'
import Secrets from './pages/Secrets'
import PersistentVolumes from './pages/PersistentVolumes'
import StorageClasses from './pages/StorageClasses'
import ServiceAccounts from './pages/ServiceAccounts'
import Settings from './pages/Settings'
import Resources from './pages/Resources'
function App() {
return (
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<AuthGate />}>
<Route path="/app" element={<DashboardLayout />}>
<Route index element={<Navigate to="create-cluster" replace />} />
<Route path="create-cluster" element={<CreateCluster />} />
<Route path="clusters/:id" element={<ClusterDetail />} />
<Route path="namespaces" element={<Namespaces />} />
<Route path="nodes" element={<Nodes />} />
<Route path="pods" element={<Pods />} />
<Route path="deployments" element={<Deployments />} />
<Route path="replicasets" element={<ReplicaSets />} />
<Route path="statefulsets" element={<StatefulSets />} />
<Route path="daemonsets" element={<DaemonSets />} />
<Route path="jobs" element={<Jobs />} />
<Route path="cronjobs" element={<CronJobs />} />
<Route path="replicationcontrollers" element={<ReplicationControllers />} />
<Route path="services" element={<Services />} />
<Route path="configmaps" element={<ConfigMaps />} />
<Route path="secrets" element={<Secrets />} />
<Route path="persistentvolumes" element={<PersistentVolumes />} />
<Route path="storageclasses" element={<StorageClasses />} />
<Route path="serviceaccounts" element={<ServiceAccounts />} />
<Route path="settings" element={<Settings />} />
<Route path="resources" element={<Resources />} />
</Route>
</Route>
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
)
}
export default App

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,43 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../lib/utils'
import type { ButtonHTMLAttributes, ReactNode } from 'react'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
default: 'bg-blue-600 text-white hover:bg-blue-700',
outline: 'border border-gray-300 hover:bg-gray-100',
ghost: 'hover:bg-gray-100',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
leftIcon?: ReactNode
rightIcon?: ReactNode
}
export default function Button({ className, variant, size, leftIcon, rightIcon, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props}>
{leftIcon ? <span className="mr-2 inline-flex">{leftIcon}</span> : null}
{props.children}
{rightIcon ? <span className="ml-2 inline-flex">{rightIcon}</span> : null}
</button>
)
}
export { buttonVariants }

14
src/index.css Normal file
View File

@@ -0,0 +1,14 @@
@import "tailwindcss";
/* App base styles */
html, body, #root {
height: 100%;
}
body {
@apply bg-gray-50 text-gray-900;
}
/* shadcn-like utilities */
.container {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}

View File

@@ -0,0 +1,296 @@
import { Link, NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom'
import {
Layers,
Settings as SettingsIcon,
Box,
LogOut,
Cpu,
Activity,
Network,
Shield,
HardDrive,
Database,
Users,
Copy,
Clock,
Zap
} from 'lucide-react'
export default function DashboardLayout() {
const navigate = useNavigate()
const location = useLocation()
const isCreateClusterPage = location.pathname === '/app/create-cluster'
return (
<div className="min-h-screen bg-gray-50">
{isCreateClusterPage ? (
// Simple layout for create cluster page without sidebar
<div className="p-4">
<div className="w-full">
<Outlet />
</div>
</div>
) : (
// Full layout with sidebar for all other pages
<div className="grid grid-cols-[240px_1fr] h-screen bg-gray-50">
<aside className="border-r border-gray-200 p-4 space-y-6 bg-white h-full overflow-y-auto">
<Link to="/app" className="block text-xl font-semibold tracking-tight">
K8s Dashboard
</Link>
<nav className="space-y-1 flex-1">
<NavLink
to="create-cluster"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Layers size={18} />
Create Cluster
</NavLink>
<div className="pt-4 border-t border-gray-200">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">
Resources
</div>
<NavLink
to="namespaces"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Box size={18} />
Namespaces (4)
</NavLink>
<NavLink
to="nodes"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Cpu size={18} />
Nodes (3)
</NavLink>
<NavLink
to="pods"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Activity size={18} />
Pods (12)
</NavLink>
<NavLink
to="deployments"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Layers size={18} />
Deployments (8)
</NavLink>
<NavLink
to="replicasets"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Copy size={18} />
ReplicaSets (12)
</NavLink>
<NavLink
to="statefulsets"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Database size={18} />
StatefulSets (3)
</NavLink>
<NavLink
to="daemonsets"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Zap size={18} />
DaemonSets (2)
</NavLink>
<NavLink
to="jobs"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Activity size={18} />
Jobs (5)
</NavLink>
<NavLink
to="cronjobs"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Clock size={18} />
CronJobs (4)
</NavLink>
<NavLink
to="replicationcontrollers"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Copy size={18} />
ReplicationControllers (1)
</NavLink>
<NavLink
to="services"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Network size={18} />
Services (6)
</NavLink>
<NavLink
to="configmaps"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<SettingsIcon size={18} />
ConfigMaps (15)
</NavLink>
<NavLink
to="secrets"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Shield size={18} />
Secrets (9)
</NavLink>
<NavLink
to="persistentvolumes"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<HardDrive size={18} />
PersistentVolumes (5)
</NavLink>
<NavLink
to="storageclasses"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Database size={18} />
StorageClasses (3)
</NavLink>
<NavLink
to="serviceaccounts"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Users size={18} />
ServiceAccounts (7)
</NavLink>
</div>
<div className="pt-4 border-t border-gray-200">
<NavLink
to="resources"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<Box size={18} />
Resources
</NavLink>
<NavLink
to="settings"
className={({ isActive }) =>
`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium ${
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
}`
}
>
<SettingsIcon size={18} />
Settings
</NavLink>
</div>
</nav>
<button
onClick={() => {
localStorage.removeItem('auth:user')
navigate('/login')
}}
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
>
<LogOut size={18} />
Logout
</button>
</aside>
<main className="p-4 overflow-y-auto">
<div className="w-full">
<Outlet />
</div>
</main>
</div>
)}
</div>
)
}

7
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import { type ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

13
src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)

View File

@@ -0,0 +1,9 @@
import { Navigate, Outlet } from 'react-router-dom'
export default function AuthGate() {
const user = localStorage.getItem('auth:user')
if (!user) {
return <Navigate to="/login" replace />
}
return <Outlet />
}

352
src/pages/ClusterDetail.tsx Normal file
View File

@@ -0,0 +1,352 @@
import { Link, useParams } from 'react-router-dom'
import {
Activity,
Box,
Cpu,
Database,
HardDrive,
Layers,
Network,
Settings,
Shield,
Users,
TrendingUp,
AlertTriangle,
CheckCircle,
Clock,
Server,
Zap
} from 'lucide-react'
interface Cluster {
id: string
name: string
clusterId: string
status: string
version: string
alerts: string
endpoint: string
}
function getCluster(id: string): Cluster | null {
const raw = localStorage.getItem('clusters')
const list: Cluster[] = raw ? JSON.parse(raw) : []
console.log('Looking for cluster with ID:', id)
console.log('Available clusters:', list)
return list.find((c) => c.id === id) || null
}
function getAllClusters(): Cluster[] {
const raw = localStorage.getItem('clusters')
return raw ? JSON.parse(raw) : []
}
function createTestCluster() {
const testCluster: Cluster = {
id: 'test-cluster-1',
name: 'test-cluster',
clusterId: 'test123',
status: 'Healthy',
version: 'v1.28.0',
alerts: '0',
endpoint: 'https://test-cluster.example.com'
}
const existingClusters = getAllClusters()
const updatedClusters = [...existingClusters, testCluster]
localStorage.setItem('clusters', JSON.stringify(updatedClusters))
console.log('Created test cluster:', testCluster)
window.location.reload()
}
const resourceTypes = [
{ name: 'Namespaces', icon: Box, count: 4, color: 'bg-blue-500' },
{ name: 'Nodes', icon: Cpu, count: 3, color: 'bg-green-500' },
{ name: 'Pods', icon: Activity, count: 12, color: 'bg-purple-500' },
{ name: 'Deployments', icon: Layers, count: 8, color: 'bg-orange-500' },
{ name: 'Services', icon: Network, count: 6, color: 'bg-indigo-500' },
{ name: 'ConfigMaps', icon: Settings, count: 15, color: 'bg-yellow-500' },
{ name: 'Secrets', icon: Shield, count: 9, color: 'bg-red-500' },
{ name: 'PersistentVolumes', icon: HardDrive, count: 5, color: 'bg-teal-500' },
{ name: 'StorageClasses', icon: Database, count: 3, color: 'bg-pink-500' },
{ name: 'ServiceAccounts', icon: Users, count: 7, color: 'bg-gray-500' },
]
// Sample cluster statistics data
const clusterStats = {
resourceUsage: {
cpu: { used: 65, total: 100, unit: 'cores' },
memory: { used: 8.2, total: 16, unit: 'GB' },
storage: { used: 45, total: 100, unit: 'GB' },
network: { used: 2.1, total: 10, unit: 'Gbps' }
},
performance: {
podStartupTime: '2.3s',
apiLatency: '45ms',
etcdLatency: '12ms',
schedulerLatency: '8ms'
},
health: {
nodesHealthy: 3,
nodesTotal: 3,
podsRunning: 10,
podsTotal: 12,
alerts: 2,
warnings: 1
},
uptime: {
clusterUptime: '15d 8h 32m',
lastMaintenance: '3d ago',
nextMaintenance: '11d from now'
}
}
export default function ClusterDetail() {
const params = useParams<{ id: string }>()
const cluster = params.id ? getCluster(params.id) : null
const allClusters = getAllClusters()
if (!cluster) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-lg font-medium text-gray-900 mb-2">Cluster not found</div>
<div className="text-sm text-gray-600 mb-4">The requested cluster could not be located.</div>
<div className="text-xs text-gray-500 mb-4">Debug: Looking for ID: {params.id}</div>
{/* Debug Information */}
<div className="bg-gray-50 p-4 rounded-lg mb-4 text-left">
<div className="text-sm font-medium text-gray-700 mb-2">Available Clusters:</div>
{allClusters.length === 0 ? (
<div className="text-sm text-gray-600">No clusters found in localStorage</div>
) : (
<div className="space-y-1">
{allClusters.map((c) => (
<div key={c.id} className="text-sm text-gray-600">
ID: {c.id} | Name: {c.name} | ClusterID: {c.clusterId}
</div>
))}
</div>
)}
</div>
<div className="space-x-2">
<button
onClick={createTestCluster}
className="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
>
Create Test Cluster
</button>
<Link to="/app/create-cluster" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Back to clusters
</Link>
</div>
</div>
</div>
)
}
return (
<div className="space-y-6 w-full">
{/* Header */}
<div className="flex items-center justify-between w-full">
<div>
<h1 className="text-3xl font-bold text-gray-900">{cluster.name}</h1>
<div className="flex items-center gap-4 mt-1 text-sm text-gray-600">
<span>Cluster ID: {cluster.clusterId}</span>
<span></span>
<span>Status: {cluster.status}</span>
<span></span>
<span>Version: {cluster.version}</span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
{cluster.status}
</div>
<Link
to="/app/create-cluster"
className="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 text-sm"
>
Back to clusters
</Link>
</div>
</div>
{/* Cluster Statistics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
{/* Resource Usage */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
Resource Usage
</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">CPU</span>
<span className="font-medium">{clusterStats.resourceUsage.cpu.used}/{clusterStats.resourceUsage.cpu.total} {clusterStats.resourceUsage.cpu.unit}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${(clusterStats.resourceUsage.cpu.used / clusterStats.resourceUsage.cpu.total) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">Memory</span>
<span className="font-medium">{clusterStats.resourceUsage.memory.used}/{clusterStats.resourceUsage.memory.total} {clusterStats.resourceUsage.memory.unit}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${(clusterStats.resourceUsage.memory.used / clusterStats.resourceUsage.memory.total) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">Storage</span>
<span className="font-medium">{clusterStats.resourceUsage.storage.used}/{clusterStats.resourceUsage.storage.total} {clusterStats.resourceUsage.storage.unit}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-purple-600 h-2 rounded-full" style={{ width: `${(clusterStats.resourceUsage.storage.used / clusterStats.resourceUsage.storage.total) * 100}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">Network</span>
<span className="font-medium">{clusterStats.resourceUsage.network.used}/{clusterStats.resourceUsage.network.total} {clusterStats.resourceUsage.network.unit}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-orange-600 h-2 rounded-full" style={{ width: `${(clusterStats.resourceUsage.network.used / clusterStats.resourceUsage.network.total) * 100}%` }}></div>
</div>
</div>
</div>
</div>
{/* Performance Metrics */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Zap className="w-5 h-5 text-yellow-600" />
Performance Metrics
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{clusterStats.performance.podStartupTime}</div>
<div className="text-sm text-gray-600">Pod Startup Time</div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{clusterStats.performance.apiLatency}</div>
<div className="text-sm text-gray-600">API Latency</div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{clusterStats.performance.etcdLatency}</div>
<div className="text-sm text-gray-600">etcd Latency</div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{clusterStats.performance.schedulerLatency}</div>
<div className="text-sm text-gray-600">Scheduler Latency</div>
</div>
</div>
</div>
</div>
{/* Health & Status */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full">
{/* Cluster Health */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
Cluster Health
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600">Nodes</span>
<span className="font-medium text-green-600">{clusterStats.health.nodesHealthy}/{clusterStats.health.nodesTotal} Healthy</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Pods</span>
<span className="font-medium text-blue-600">{clusterStats.health.podsRunning}/{clusterStats.health.podsTotal} Running</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Alerts</span>
<span className="font-medium text-red-600">{clusterStats.health.alerts} Active</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Warnings</span>
<span className="font-medium text-yellow-600">{clusterStats.health.warnings} Active</span>
</div>
</div>
</div>
{/* Uptime Information */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-blue-600" />
Uptime & Maintenance
</h3>
<div className="space-y-3">
<div>
<div className="text-sm text-gray-600 mb-1">Cluster Uptime</div>
<div className="font-medium text-gray-900">{clusterStats.uptime.clusterUptime}</div>
</div>
<div>
<div className="text-sm text-gray-600 mb-1">Last Maintenance</div>
<div className="font-medium text-gray-900">{clusterStats.uptime.lastMaintenance}</div>
</div>
<div>
<div className="text-sm text-gray-600 mb-1">Next Maintenance</div>
<div className="font-medium text-gray-900">{clusterStats.uptime.nextMaintenance}</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Server className="w-5 h-5 text-purple-600" />
Quick Actions
</h3>
<div className="space-y-3">
<button className="w-full text-left p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors">
<div className="font-medium text-gray-900">View Cluster Metrics</div>
<div className="text-sm text-gray-600">Monitor CPU, memory, and network usage</div>
</button>
<button className="w-full text-left p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors">
<div className="font-medium text-gray-900">Download kubeconfig</div>
<div className="text-sm text-gray-600">Get cluster access credentials</div>
</button>
<button className="w-full text-left p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors">
<div className="font-medium text-gray-900">View Events</div>
<div className="text-sm text-gray-600">Check cluster and pod events</div>
</button>
</div>
</div>
</div>
{/* Cluster Information */}
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm w-full">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Cluster Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Kubernetes Version</span>
<span className="font-medium">{cluster.version}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">API Server</span>
<span className="font-mono text-sm">{cluster.endpoint}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Cluster ID</span>
<span className="font-medium">{cluster.clusterId}</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Status</span>
<span className="font-medium">{cluster.status}</span>
</div>
</div>
</div>
</div>
)
}

78
src/pages/ConfigMaps.tsx Normal file
View File

@@ -0,0 +1,78 @@
export default function ConfigMaps() {
const configmaps = [
{
name: 'app-config',
namespace: 'default',
data: 3,
age: '2h',
labels: 'app=web'
},
{
name: 'database-config',
namespace: 'default',
data: 5,
age: '1d',
labels: 'app=database'
},
{
name: 'redis-config',
namespace: 'default',
data: 2,
age: '3h',
labels: 'app=redis'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">ConfigMaps</h1>
<p className="text-sm text-gray-600">Manage configuration data for applications.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">ConfigMap List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create ConfigMap
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{configmaps.map((configmap) => (
<tr key={configmap.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{configmap.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{configmap.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{configmap.data}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{configmap.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{configmap.labels}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

309
src/pages/CreateCluster.tsx Normal file
View File

@@ -0,0 +1,309 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Download, Trash2 } from 'lucide-react'
import type { FormEvent } from 'react'
interface Cluster {
id: string
name: string
clusterId: string
status: string
version: string
alerts: string
endpoint: string
}
export default function CreateCluster() {
const [clusters, setClusters] = useState<Cluster[]>(() => {
const saved = localStorage.getItem('clusters')
if (saved) {
return JSON.parse(saved)
}
return [
{ id: '1', name: 'dev-cluster', clusterId: '680d172c', status: 'Healthy', version: 'v1.28.0', alerts: '0', endpoint: 'https://dev-cluster.example.com' },
{ id: '2', name: 'prod-cluster', clusterId: 'd02b06fe', status: 'Healthy', version: 'v1.28.0', alerts: '0', endpoint: 'https://prod-cluster.example.com' },
{ id: '3', name: 'test-prod', clusterId: '937261be', status: 'Healthy', version: 'v1.28.0', alerts: '0', endpoint: 'https://test-prod.example.com' },
]
})
const [showModal, setShowModal] = useState(false)
const [formData, setFormData] = useState({
clusterName: '',
namespace: '',
controlPlane: 'Kubernetes (k8s)',
kubernetesVersion: 'v1.31.6 (recommended)',
cpu: 1,
memory: 2048
})
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
const newCluster: Cluster = {
id: Date.now().toString(),
name: formData.clusterName,
clusterId: Math.random().toString(36).substr(2, 8),
status: 'Creating',
version: formData.kubernetesVersion.split(' ')[0],
alerts: '0',
endpoint: `https://${formData.clusterName}.example.com`
}
const updatedClusters = [...clusters, newCluster]
setClusters(updatedClusters)
localStorage.setItem('clusters', JSON.stringify(updatedClusters))
setShowModal(false)
setFormData({
clusterName: '',
namespace: '',
controlPlane: 'Kubernetes (k8s)',
kubernetesVersion: 'v1.31.6 (recommended)',
cpu: 1,
memory: 2048
})
}
const downloadKubeconfig = (clusterId: string) => {
const kubeconfig = `apiVersion: v1
kind: Config
clusters:
- name: ${clusterId}
cluster:
server: https://${clusterId}.example.com
contexts:
- name: ${clusterId}
context:
cluster: ${clusterId}
user: admin
current-context: ${clusterId}
users:
- name: admin
user:
token: your-token-here`
const blob = new Blob([kubeconfig], { type: 'text/yaml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `kubeconfig-${clusterId}.yaml`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const deleteCluster = (clusterId: string) => {
const updatedClusters = clusters.filter(cluster => cluster.clusterId !== clusterId)
setClusters(updatedClusters)
localStorage.setItem('clusters', JSON.stringify(updatedClusters))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold">Create Cluster</h1>
<p className="text-sm text-gray-600">Create and manage your Kubernetes clusters.</p>
</div>
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
>
Create Cluster
</button>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium">Cluster List</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cluster ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Alerts</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Endpoint</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{clusters.map((cluster) => (
<tr key={cluster.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<Link
to={`/app/clusters/${cluster.id}`}
className="text-sm font-medium text-blue-600 hover:text-blue-900"
>
{cluster.name}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.clusterId}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
cluster.status === 'Healthy' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{cluster.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.version}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.alerts}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.endpoint}</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center space-x-2">
<button
onClick={() => downloadKubeconfig(cluster.clusterId)}
className="p-2 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded-md transition-colors"
title="Download kubeconfig"
>
<Download size={16} />
</button>
<button
onClick={() => deleteCluster(cluster.clusterId)}
className="p-2 text-red-600 hover:text-red-900 hover:bg-red-50 rounded-md transition-colors"
title="Delete cluster"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Create Cluster Modal */}
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold">Create Cluster</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Basic Settings */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-4">Basic Settings</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cluster Name
</label>
<input
type="text"
value={formData.clusterName}
onChange={(e) => setFormData({...formData, clusterName: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Namespace
</label>
<input
type="text"
value={formData.namespace}
onChange={(e) => setFormData({...formData, namespace: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
</div>
</div>
{/* Cluster Configuration */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-4">Cluster Configuration</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Control Plane
</label>
<select
value={formData.controlPlane}
onChange={(e) => setFormData({...formData, controlPlane: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option>Kubernetes (k8s)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kubernetes Version
</label>
<select
value={formData.kubernetesVersion}
onChange={(e) => setFormData({...formData, kubernetesVersion: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option>v1.31.6 (recommended)</option>
<option>v1.30.0</option>
<option>v1.29.0</option>
</select>
</div>
</div>
</div>
{/* Control Plan Resource Configuration */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-lg font-semibold mb-4">Control Plan Resource Configuration</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CPU (cores)
</label>
<input
type="number"
min="1"
max="8"
value={formData.cpu}
onChange={(e) => setFormData({...formData, cpu: parseInt(e.target.value)})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Memory (MB)
</label>
<input
type="number"
min="1024"
max="16384"
step="1024"
value={formData.memory}
onChange={(e) => setFormData({...formData, memory: parseInt(e.target.value)})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={() => setShowModal(false)}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
className="px-6 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600"
>
Create Cluster
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}

110
src/pages/CronJobs.tsx Normal file
View File

@@ -0,0 +1,110 @@
export default function CronJobs() {
const cronJobs = [
{
name: 'backup-cronjob',
namespace: 'default',
schedule: '0 2 * * *',
suspend: false,
lastSchedule: '2h ago',
age: '1d',
image: 'backup:v1.0',
concurrencyPolicy: 'Forbid'
},
{
name: 'cleanup-cronjob',
namespace: 'default',
schedule: '0 0 * * 0',
suspend: false,
lastSchedule: '1d ago',
age: '3d',
image: 'cleanup:v1.0',
concurrencyPolicy: 'Allow'
},
{
name: 'report-cronjob',
namespace: 'default',
schedule: '0 9 * * 1-5',
suspend: false,
lastSchedule: '1h ago',
age: '2d',
image: 'reports:v1.2',
concurrencyPolicy: 'Replace'
},
{
name: 'sync-cronjob',
namespace: 'default',
schedule: '*/30 * * * *',
suspend: true,
lastSchedule: '30m ago',
age: '5d',
image: 'sync:v1.0',
concurrencyPolicy: 'Forbid'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">CronJobs</h1>
<p className="text-sm text-gray-600">Manage scheduled jobs and recurring tasks.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">CronJob List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create CronJob
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Schedule</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Suspend</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Schedule</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Concurrency Policy</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{cronJobs.map((cronJob) => (
<tr key={cronJob.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{cronJob.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cronJob.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{cronJob.schedule}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
cronJob.suspend ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
}`}>
{cronJob.suspend ? 'True' : 'False'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cronJob.lastSchedule}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cronJob.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cronJob.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cronJob.concurrencyPolicy}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Trigger</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Suspend</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

87
src/pages/DaemonSets.tsx Normal file
View File

@@ -0,0 +1,87 @@
export default function DaemonSets() {
const daemonSets = [
{
name: 'fluentd-elasticsearch',
namespace: 'kube-system',
desired: 3,
current: 3,
ready: 3,
upToDate: 3,
available: 3,
age: '2d',
image: 'fluentd:v1.14'
},
{
name: 'kube-proxy',
namespace: 'kube-system',
desired: 3,
current: 3,
ready: 3,
upToDate: 3,
available: 3,
age: '2d',
image: 'kube-proxy:v1.28.0'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">DaemonSets</h1>
<p className="text-sm text-gray-600">Manage daemon sets that run on every node.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">DaemonSet List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create DaemonSet
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Desired</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Up-to-date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Available</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{daemonSets.map((ds) => (
<tr key={ds.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{ds.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.desired}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.current}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.ready}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.upToDate}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.available}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ds.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Rollout</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

94
src/pages/Deployments.tsx Normal file
View File

@@ -0,0 +1,94 @@
export default function Deployments() {
const deployments = [
{
name: 'nginx-deployment',
namespace: 'default',
ready: '3/3',
upToDate: 3,
available: 3,
age: '2h',
image: 'nginx:1.21',
strategy: 'RollingUpdate'
},
{
name: 'app-deployment',
namespace: 'default',
ready: '2/2',
upToDate: 2,
available: 2,
age: '4h',
image: 'app:v1.0',
strategy: 'RollingUpdate'
},
{
name: 'api-deployment',
namespace: 'default',
ready: '1/1',
upToDate: 1,
available: 1,
age: '1d',
image: 'api:v2.1',
strategy: 'Recreate'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Deployments</h1>
<p className="text-sm text-gray-600">Manage application deployments and scaling.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Deployment List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Deployment
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Up-to-date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Available</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Strategy</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{deployments.map((deployment) => (
<tr key={deployment.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{deployment.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.ready}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.upToDate}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.available}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{deployment.strategy}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Scale</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Rollout</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

114
src/pages/Jobs.tsx Normal file
View File

@@ -0,0 +1,114 @@
export default function Jobs() {
const jobs = [
{
name: 'backup-job',
namespace: 'default',
completions: '1/1',
duration: '5m',
age: '1h',
image: 'backup:v1.0',
status: 'Complete'
},
{
name: 'data-migration',
namespace: 'default',
completions: '3/3',
duration: '15m',
age: '2h',
image: 'migration:v2.1',
status: 'Complete'
},
{
name: 'cleanup-job',
namespace: 'default',
completions: '0/1',
duration: '2m',
age: '30m',
image: 'cleanup:v1.0',
status: 'Running'
},
{
name: 'report-generator',
namespace: 'default',
completions: '1/1',
duration: '10m',
age: '4h',
image: 'reports:v1.2',
status: 'Complete'
},
{
name: 'sync-job',
namespace: 'default',
completions: '0/1',
duration: '1m',
age: '5m',
image: 'sync:v1.0',
status: 'Failed'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Jobs</h1>
<p className="text-sm text-gray-600">Manage one-time batch jobs and tasks.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Job List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Job
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Completions</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{jobs.map((job) => (
<tr key={job.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{job.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.completions}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.duration}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.image}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
job.status === 'Complete' ? 'bg-green-100 text-green-800' :
job.status === 'Running' ? 'bg-blue-100 text-blue-800' :
'bg-red-100 text-red-800'
}`}>
{job.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Logs</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

63
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,63 @@
import type { FormEvent } from 'react'
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import Button from '../components/ui/button'
export default function Login() {
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
function onSubmit(e: FormEvent) {
e.preventDefault()
setError(null)
const usersString = localStorage.getItem('auth:users')
const users: Array<{ email: string; password: string }> = usersString ? JSON.parse(usersString) : []
const match = users.find((u) => u.email === email && u.password === password)
if (!match) {
setError('Invalid credentials')
return
}
localStorage.setItem('auth:user', JSON.stringify({ email }))
navigate('/app')
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-sm bg-white border border-gray-200 rounded-lg p-6 shadow">
<h1 className="text-xl font-semibold mb-4">Login</h1>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="email">Email</label>
<input
id="email"
type="email"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="password">Password</label>
<input
id="password"
type="password"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<Button type="submit" className="w-full">Sign in</Button>
</form>
<p className="mt-4 text-sm text-gray-600">
No account?{' '}
<Link to="/register" className="text-blue-600 hover:underline">Register</Link>
</p>
</div>
</div>
)
}

62
src/pages/Namespaces.tsx Normal file
View File

@@ -0,0 +1,62 @@
export default function Namespaces() {
const namespaces = [
{ name: 'default', status: 'Active', age: '2d', labels: 'app=web' },
{ name: 'kube-system', status: 'Active', age: '2d', labels: 'system' },
{ name: 'monitoring', status: 'Active', age: '1d', labels: 'monitoring' },
{ name: 'ingress-nginx', status: 'Active', age: '1d', labels: 'ingress' },
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Namespaces</h1>
<p className="text-sm text-gray-600">Manage Kubernetes namespaces and their resources.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Namespace List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Namespace
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{namespaces.map((ns) => (
<tr key={ns.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{ns.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{ns.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ns.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ns.labels}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

101
src/pages/Nodes.tsx Normal file
View File

@@ -0,0 +1,101 @@
export default function Nodes() {
const nodes = [
{
name: 'worker-node-1',
status: 'Ready',
roles: 'worker',
age: '2d',
version: 'v1.28.0',
cpu: '4/8',
memory: '8Gi/16Gi',
os: 'Ubuntu 22.04'
},
{
name: 'worker-node-2',
status: 'Ready',
roles: 'worker',
age: '2d',
version: 'v1.28.0',
cpu: '3/8',
memory: '6Gi/16Gi',
os: 'Ubuntu 22.04'
},
{
name: 'control-plane',
status: 'Ready',
roles: 'control-plane',
age: '2d',
version: 'v1.28.0',
cpu: '2/4',
memory: '4Gi/8Gi',
os: 'Ubuntu 22.04'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Nodes</h1>
<p className="text-sm text-gray-600">Monitor and manage cluster nodes.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Node List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Add Node
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Roles</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CPU</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">OS</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{nodes.map((node) => (
<tr key={node.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{node.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{node.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{node.roles}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{node.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{node.version}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{node.cpu}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{node.memory}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{node.os}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Cordon</button>
<button className="text-red-600 hover:text-red-900">Drain</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,99 @@
export default function PersistentVolumes() {
const persistentVolumes = [
{
name: 'pv-1',
capacity: '10Gi',
accessMode: 'RWO',
reclaimPolicy: 'Retain',
status: 'Bound',
claim: 'default/pvc-1',
storageClass: 'local-storage',
age: '2h'
},
{
name: 'pv-2',
capacity: '20Gi',
accessMode: 'RWO',
reclaimPolicy: 'Delete',
status: 'Available',
claim: '',
storageClass: 'fast-ssd',
age: '1d'
},
{
name: 'pv-3',
capacity: '5Gi',
accessMode: 'ROX',
reclaimPolicy: 'Retain',
status: 'Bound',
claim: 'default/pvc-2',
storageClass: 'nfs-storage',
age: '3h'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Persistent Volumes</h1>
<p className="text-sm text-gray-600">Manage persistent storage volumes.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Persistent Volume List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create PV
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Capacity</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Access Mode</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Reclaim Policy</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Claim</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Storage Class</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{persistentVolumes.map((pv) => (
<tr key={pv.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{pv.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.capacity}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.accessMode}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.reclaimPolicy}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
pv.status === 'Bound' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{pv.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.claim || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.storageClass}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pv.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

113
src/pages/Pods.tsx Normal file
View File

@@ -0,0 +1,113 @@
export default function Pods() {
const pods = [
{
name: 'nginx-deployment-66b6c48dd5',
namespace: 'default',
ready: '1/1',
status: 'Running',
restarts: 0,
age: '2h',
ip: '10.244.0.5',
node: 'worker-node-1',
image: 'nginx:1.21'
},
{
name: 'redis-master-0',
namespace: 'default',
ready: '1/1',
status: 'Running',
restarts: 0,
age: '1d',
ip: '10.244.0.6',
node: 'worker-node-2',
image: 'redis:6.2'
},
{
name: 'postgres-0',
namespace: 'default',
ready: '1/1',
status: 'Running',
restarts: 1,
age: '3h',
ip: '10.244.0.7',
node: 'worker-node-1',
image: 'postgres:13'
},
{
name: 'app-deployment-7d8f9c2b1a',
namespace: 'default',
ready: '2/2',
status: 'Running',
restarts: 0,
age: '4h',
ip: '10.244.0.8',
node: 'worker-node-2',
image: 'app:v1.0'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Pods</h1>
<p className="text-sm text-gray-600">Monitor and manage application pods.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Pod List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Pod
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Restarts</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Node</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{pods.map((pod) => (
<tr key={pod.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{pod.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.ready}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{pod.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.restarts}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{pod.ip}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.node}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Logs</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

80
src/pages/Register.tsx Normal file
View File

@@ -0,0 +1,80 @@
import type { FormEvent } from 'react'
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import Button from '../components/ui/button'
export default function Register() {
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirm, setConfirm] = useState('')
const [error, setError] = useState<string | null>(null)
function onSubmit(e: FormEvent) {
e.preventDefault()
setError(null)
if (password !== confirm) {
setError('Passwords do not match')
return
}
const usersString = localStorage.getItem('auth:users')
const users: Array<{ email: string; password: string }> = usersString ? JSON.parse(usersString) : []
if (users.some((u) => u.email === email)) {
setError('Email already registered')
return
}
users.push({ email, password })
localStorage.setItem('auth:users', JSON.stringify(users))
localStorage.setItem('auth:user', JSON.stringify({ email }))
navigate('/app')
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-sm bg-white border border-gray-200 rounded-lg p-6 shadow">
<h1 className="text-xl font-semibold mb-4">Register</h1>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="email">Email</label>
<input
id="email"
type="email"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="password">Password</label>
<input
id="password"
type="password"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="space-y-1">
<label className="text-sm font-medium" htmlFor="confirm">Confirm Password</label>
<input
id="confirm"
type="password"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
required
/>
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<Button type="submit" className="w-full">Create account</Button>
</form>
<p className="mt-4 text-sm text-gray-600">
Have an account?{' '}
<Link to="/login" className="text-blue-600 hover:underline">Login</Link>
</p>
</div>
</div>
)
}

93
src/pages/ReplicaSets.tsx Normal file
View File

@@ -0,0 +1,93 @@
export default function ReplicaSets() {
const replicaSets = [
{
name: 'nginx-deployment-66b6c48dd5',
namespace: 'default',
desired: 3,
current: 3,
ready: 3,
age: '2h',
image: 'nginx:1.21',
labels: 'app=nginx'
},
{
name: 'app-deployment-7d8f9c2b1a',
namespace: 'default',
desired: 2,
current: 2,
ready: 2,
age: '4h',
image: 'app:v1.0',
labels: 'app=web'
},
{
name: 'api-deployment-9e4f2c1d8b',
namespace: 'default',
desired: 1,
current: 1,
ready: 1,
age: '1d',
image: 'api:v2.1',
labels: 'app=api'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">ReplicaSets</h1>
<p className="text-sm text-gray-600">Manage replica sets and pod scaling.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">ReplicaSet List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create ReplicaSet
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Desired</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{replicaSets.map((rs) => (
<tr key={rs.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{rs.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.desired}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.current}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.ready}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rs.labels}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Scale</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,74 @@
export default function ReplicationControllers() {
const replicationControllers = [
{
name: 'legacy-app',
namespace: 'default',
desired: 2,
current: 2,
ready: 2,
age: '5d',
image: 'legacy-app:v1.0',
labels: 'app=legacy'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Replication Controllers</h1>
<p className="text-sm text-gray-600">Manage legacy replication controllers (deprecated in favor of Deployments).</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Replication Controller List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Replication Controller
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Desired</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{replicationControllers.map((rc) => (
<tr key={rc.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{rc.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.desired}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.current}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.ready}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{rc.labels}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Scale</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Migrate to Deployment</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

11
src/pages/Resources.tsx Normal file
View File

@@ -0,0 +1,11 @@
export default function Resources() {
return (
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Resources</h1>
<p className="text-sm text-gray-600">View and manage Kubernetes resources.</p>
<div className="mt-4 bg-white border border-gray-200 rounded-lg p-4 text-sm text-gray-700">
Coming soon.
</div>
</div>
)
}

82
src/pages/Secrets.tsx Normal file
View File

@@ -0,0 +1,82 @@
export default function Secrets() {
const secrets = [
{
name: 'db-credentials',
namespace: 'default',
type: 'Opaque',
data: 2,
age: '2h'
},
{
name: 'tls-secret',
namespace: 'default',
type: 'kubernetes.io/tls',
data: 2,
age: '1d'
},
{
name: 'docker-registry',
namespace: 'default',
type: 'kubernetes.io/dockerconfigjson',
data: 1,
age: '3h'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Secrets</h1>
<p className="text-sm text-gray-600">Manage sensitive configuration data and credentials.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Secret List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Secret
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{secrets.map((secret) => (
<tr key={secret.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{secret.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{secret.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{secret.type}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{secret.data}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{secret.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,86 @@
export default function ServiceAccounts() {
const serviceAccounts = [
{
name: 'default',
namespace: 'default',
secrets: 1,
age: '2d',
labels: 'app=web'
},
{
name: 'kube-system',
namespace: 'kube-system',
secrets: 0,
age: '2d',
labels: 'system'
},
{
name: 'app-sa',
namespace: 'default',
secrets: 1,
age: '4h',
labels: 'app=api'
},
{
name: 'monitoring-sa',
namespace: 'monitoring',
secrets: 1,
age: '1d',
labels: 'monitoring'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Service Accounts</h1>
<p className="text-sm text-gray-600">Manage service accounts and their permissions.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Service Account List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Service Account
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Secrets</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{serviceAccounts.map((sa) => (
<tr key={`${sa.namespace}-${sa.name}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{sa.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sa.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sa.secrets}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sa.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sa.labels}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Secrets</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

97
src/pages/Services.tsx Normal file
View File

@@ -0,0 +1,97 @@
export default function Services() {
const services = [
{
name: 'nginx-service',
namespace: 'default',
type: 'ClusterIP',
clusterIP: '10.96.1.10',
externalIP: '<none>',
ports: '80:80/TCP',
age: '2h',
selector: 'app=nginx'
},
{
name: 'redis-service',
namespace: 'default',
type: 'ClusterIP',
clusterIP: '10.96.1.11',
externalIP: '<none>',
ports: '6379:6379/TCP',
age: '1d',
selector: 'app=redis'
},
{
name: 'app-service',
namespace: 'default',
type: 'LoadBalancer',
clusterIP: '10.96.1.12',
externalIP: '192.168.1.100',
ports: '8080:80/TCP',
age: '4h',
selector: 'app=web'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Services</h1>
<p className="text-sm text-gray-600">Manage network services and load balancing.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Service List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Service
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cluster-IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">External-IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ports</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Selector</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{services.map((service) => (
<tr key={service.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{service.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{service.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{service.type}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{service.clusterIP}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{service.externalIP}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{service.ports}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{service.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{service.selector}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

11
src/pages/Settings.tsx Normal file
View File

@@ -0,0 +1,11 @@
export default function Settings() {
return (
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-gray-600">Project-wide preferences and configuration.</p>
<div className="mt-4 bg-white border border-gray-200 rounded-lg p-4 text-sm text-gray-700">
Coming soon.
</div>
</div>
)
}

View File

@@ -0,0 +1,94 @@
export default function StatefulSets() {
const statefulSets = [
{
name: 'redis-master',
namespace: 'default',
ready: '1/1',
current: 1,
updated: 1,
age: '1d',
image: 'redis:6.2',
serviceName: 'redis-master'
},
{
name: 'postgres',
namespace: 'default',
ready: '1/1',
current: 1,
updated: 1,
age: '3h',
image: 'postgres:13',
serviceName: 'postgres'
},
{
name: 'elasticsearch',
namespace: 'monitoring',
ready: '3/3',
current: 3,
updated: 3,
age: '2d',
image: 'elasticsearch:7.17',
serviceName: 'elasticsearch'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">StatefulSets</h1>
<p className="text-sm text-gray-600">Manage stateful applications with persistent storage.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">StatefulSet List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create StatefulSet
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Updated</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{statefulSets.map((sts) => (
<tr key={sts.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{sts.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.ready}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.current}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.updated}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sts.serviceName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Scale</button>
<button className="text-orange-600 hover:text-orange-900 mr-3">Rollout</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,89 @@
export default function StorageClasses() {
const storageClasses = [
{
name: 'fast-ssd',
provisioner: 'kubernetes.io/aws-ebs',
reclaimPolicy: 'Delete',
volumeBindingMode: 'Immediate',
allowVolumeExpansion: true,
age: '2h'
},
{
name: 'slow-hdd',
provisioner: 'kubernetes.io/aws-ebs',
reclaimPolicy: 'Retain',
volumeBindingMode: 'WaitForFirstConsumer',
allowVolumeExpansion: false,
age: '1d'
},
{
name: 'local-storage',
provisioner: 'kubernetes.io/no-provisioner',
reclaimPolicy: 'Delete',
volumeBindingMode: 'WaitForFirstConsumer',
allowVolumeExpansion: false,
age: '3h'
},
]
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">Storage Classes</h1>
<p className="text-sm text-gray-600">Manage storage class definitions and provisioning.</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Storage Class List</h2>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Create Storage Class
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Provisioner</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Reclaim Policy</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Volume Binding Mode</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Allow Volume Expansion</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{storageClasses.map((sc) => (
<tr key={sc.name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{sc.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{sc.provisioner}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sc.reclaimPolicy}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sc.volumeBindingMode}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
sc.allowVolumeExpansion ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}>
{sc.allowVolumeExpansion ? 'Yes' : 'No'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{sc.age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
<button className="text-green-600 hover:text-green-900 mr-3">Edit</button>
<button className="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

27
tsconfig.app.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})