init project
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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?
|
||||||
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "c2915b3a",
|
||||||
|
"configHash": "e9a24537",
|
||||||
|
"lockfileHash": "e3b0c442",
|
||||||
|
"browserHash": "38dbafaf",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
15
README.md
15
README.md
@@ -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
23
eslint.config.js
Normal 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
13
index.html
Normal 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
4253
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal 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
7
postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal 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
42
src/App.css
Normal 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
63
src/App.tsx
Normal 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
1
src/assets/react.svg
Normal 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 |
43
src/components/ui/button.tsx
Normal file
43
src/components/ui/button.tsx
Normal 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
14
src/index.css
Normal 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;
|
||||||
|
}
|
||||||
296
src/layout/DashboardLayout.tsx
Normal file
296
src/layout/DashboardLayout.tsx
Normal 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
7
src/lib/utils.ts
Normal 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
13
src/main.tsx
Normal 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>,
|
||||||
|
)
|
||||||
9
src/middleware/AuthGate.tsx
Normal file
9
src/middleware/AuthGate.tsx
Normal 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
352
src/pages/ClusterDetail.tsx
Normal 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
78
src/pages/ConfigMaps.tsx
Normal 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
309
src/pages/CreateCluster.tsx
Normal 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
110
src/pages/CronJobs.tsx
Normal 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
87
src/pages/DaemonSets.tsx
Normal 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
94
src/pages/Deployments.tsx
Normal 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
114
src/pages/Jobs.tsx
Normal 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
63
src/pages/Login.tsx
Normal 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
62
src/pages/Namespaces.tsx
Normal 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
101
src/pages/Nodes.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
99
src/pages/PersistentVolumes.tsx
Normal file
99
src/pages/PersistentVolumes.tsx
Normal 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
113
src/pages/Pods.tsx
Normal 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
80
src/pages/Register.tsx
Normal 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
93
src/pages/ReplicaSets.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
74
src/pages/ReplicationControllers.tsx
Normal file
74
src/pages/ReplicationControllers.tsx
Normal 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
11
src/pages/Resources.tsx
Normal 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
82
src/pages/Secrets.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
86
src/pages/ServiceAccounts.tsx
Normal file
86
src/pages/ServiceAccounts.tsx
Normal 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
97
src/pages/Services.tsx
Normal 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
11
src/pages/Settings.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
94
src/pages/StatefulSets.tsx
Normal file
94
src/pages/StatefulSets.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
89
src/pages/StorageClasses.tsx
Normal file
89
src/pages/StorageClasses.tsx
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal 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
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user