Merge pull request 'feat(kubernetes): implement Phase 7 - Real-time updates with Lens Desktop v5.x feature parity' (#76) from feature/kubernetes-management-v2 into master
Some checks failed
Auto Tag / autotag (push) Successful in 14s
Auto Tag / wiki-sync (push) Successful in 20s
Test / frontend-typecheck (push) Successful in 1m46s
Test / frontend-tests (push) Successful in 1m37s
Auto Tag / changelog (push) Successful in 1m41s
Auto Tag / build-linux-amd64 (push) Successful in 9m44s
Auto Tag / build-windows-amd64 (push) Successful in 11m52s
Auto Tag / build-linux-arm64 (push) Successful in 11m56s
Test / rust-fmt-check (push) Successful in 16m26s
Test / rust-clippy (push) Successful in 17m53s
Test / rust-tests (push) Successful in 19m26s
Auto Tag / build-macos-arm64 (push) Failing after 20m24s
Some checks failed
Auto Tag / autotag (push) Successful in 14s
Auto Tag / wiki-sync (push) Successful in 20s
Test / frontend-typecheck (push) Successful in 1m46s
Test / frontend-tests (push) Successful in 1m37s
Auto Tag / changelog (push) Successful in 1m41s
Auto Tag / build-linux-amd64 (push) Successful in 9m44s
Auto Tag / build-windows-amd64 (push) Successful in 11m52s
Auto Tag / build-linux-arm64 (push) Successful in 11m56s
Test / rust-fmt-check (push) Successful in 16m26s
Test / rust-clippy (push) Successful in 17m53s
Test / rust-tests (push) Successful in 19m26s
Auto Tag / build-macos-arm64 (push) Failing after 20m24s
Reviewed-on: #76
This commit is contained in:
commit
1d108ed4a9
@ -136,6 +136,45 @@ jobs:
|
|||||||
echo "index_lines=${INDEX_LINES}" >> $GITHUB_OUTPUT
|
echo "index_lines=${INDEX_LINES}" >> $GITHUB_OUTPUT
|
||||||
echo "Built codebase index: ${INDEX_LINES} lines"
|
echo "Built codebase index: ${INDEX_LINES} lines"
|
||||||
|
|
||||||
|
- name: Fetch PR comment history
|
||||||
|
id: pr_history
|
||||||
|
if: steps.context.outputs.diff_size != '0'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TF_TOKEN: ${{ secrets.TFT_GITEA_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
> /tmp/pr_comments.txt
|
||||||
|
|
||||||
|
# Fetch automated review posts (what this action posts each round)
|
||||||
|
REVIEWS=$(curl -sf --max-time 30 --connect-timeout 10 \
|
||||||
|
"https://gogs.tftsr.com/api/v1/repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
|
||||||
|
-H "Authorization: Bearer $TF_TOKEN" || echo '[]')
|
||||||
|
|
||||||
|
# Fetch regular PR/issue comments (human responses, rebuttals, etc.)
|
||||||
|
COMMENTS=$(curl -sf --max-time 30 --connect-timeout 10 \
|
||||||
|
"https://gogs.tftsr.com/api/v1/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
||||||
|
-H "Authorization: Bearer $TF_TOKEN" || echo '[]')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n\n' '## PREVIOUS REVIEW ROUNDS'
|
||||||
|
printf '%s\n\n' '### Automated review posts (oldest first):'
|
||||||
|
echo "$REVIEWS" \
|
||||||
|
| jq -r '.[] | "#### Review by \(.user.login) [state: \(.state // "COMMENT")]:\n\(.body)\n---"' \
|
||||||
|
2>/dev/null || true
|
||||||
|
|
||||||
|
printf '\n%s\n\n' '### PR comments (oldest first):'
|
||||||
|
echo "$COMMENTS" \
|
||||||
|
| jq -r '.[] | "#### Comment by \(.user.login):\n\(.body)\n---"' \
|
||||||
|
2>/dev/null || true
|
||||||
|
} >> /tmp/pr_comments.txt
|
||||||
|
|
||||||
|
LINES=$(wc -l < /tmp/pr_comments.txt | tr -d ' ')
|
||||||
|
echo "comment_lines=${LINES}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Fetched PR history: ${LINES} lines"
|
||||||
|
|
||||||
- name: Analyze with LLM
|
- name: Analyze with LLM
|
||||||
id: analyze
|
id: analyze
|
||||||
if: steps.context.outputs.diff_size != '0'
|
if: steps.context.outputs.diff_size != '0'
|
||||||
@ -165,6 +204,18 @@ jobs:
|
|||||||
printf '%s\n' '---'
|
printf '%s\n' '---'
|
||||||
cat /tmp/pr_context.txt
|
cat /tmp/pr_context.txt
|
||||||
printf '%s\n\n' '---'
|
printf '%s\n\n' '---'
|
||||||
|
if [ -s /tmp/pr_comments.txt ]; then
|
||||||
|
cat /tmp/pr_comments.txt
|
||||||
|
printf '%s\n\n' '---'
|
||||||
|
printf '%s\n' '## CRITICAL: Prior review context above'
|
||||||
|
printf '%s\n' 'Before raising ANY finding, check the review history above.'
|
||||||
|
printf '%s\n' 'SILENTLY DISCARD any finding that has already been:'
|
||||||
|
printf '%s\n' ' - Marked as invalid or incorrect by a reviewer'
|
||||||
|
printf '%s\n' ' - Acknowledged as an intentional design decision or known limitation'
|
||||||
|
printf '%s\n\n' ' - Confirmed fixed in a prior commit'
|
||||||
|
printf '%s\n\n' 'Raising a previously-refuted finding is a critical error.'
|
||||||
|
printf '%s\n' '---'
|
||||||
|
fi
|
||||||
printf '%s\n\n' '## Instructions'
|
printf '%s\n\n' '## Instructions'
|
||||||
printf '%s\n' 'Before raising any finding:'
|
printf '%s\n' 'Before raising any finding:'
|
||||||
printf '%s\n' '1. Confirm every symbol you cite exists in the CODEBASE INDEX or file'
|
printf '%s\n' '1. Confirm every symbol you cite exists in the CODEBASE INDEX or file'
|
||||||
@ -330,4 +381,4 @@ jobs:
|
|||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
if: always()
|
if: always()
|
||||||
shell: bash
|
shell: bash
|
||||||
run: rm -f /tmp/pr_diff.txt /tmp/pr_context.txt /tmp/codebase_index.txt /tmp/prompt.txt /tmp/body.json /tmp/llm_response.json /tmp/pr_review.txt /tmp/review_post_response.json /tmp/pr_files.txt
|
run: rm -f /tmp/pr_diff.txt /tmp/pr_context.txt /tmp/codebase_index.txt /tmp/pr_comments.txt /tmp/prompt.txt /tmp/body.json /tmp/llm_response.json /tmp/pr_review.txt /tmp/review_post_response.json /tmp/pr_files.txt
|
||||||
|
|||||||
@ -45,6 +45,15 @@ CI, chore, and build changes are excluded.
|
|||||||
- Implement full Lens-like Kubernetes UI with resource discovery and management
|
- Implement full Lens-like Kubernetes UI with resource discovery and management
|
||||||
- Implement additional Kubernetes resource discovery and management commands
|
- Implement additional Kubernetes resource discovery and management commands
|
||||||
- Add Kubernetes Management Implementation Plan
|
- Add Kubernetes Management Implementation Plan
|
||||||
|
- **k8s**: Implement Phase 7 - Real-time updates with event bus and watchers
|
||||||
|
- **k8s**: Add 15 new resource list components (Secret, ReplicaSet, Job, CronJob, Ingress, PVC, PV, ServiceAccount, Role, ClusterRole, RoleBinding, ClusterRoleBinding, HPA, Node, Event, ConfigMap)
|
||||||
|
- **k8s**: Add advanced components (Terminal, YamlEditor, MetricsChart, SearchBar, ContextSwitcher, ApplicationView)
|
||||||
|
- **k8s**: Add detail views for all major resource types
|
||||||
|
- **k8s**: Add UX components (Hotbar, CommandPalette, Toast, LoadingSpinner)
|
||||||
|
- **k8s**: Add resource management dialogs (CreateResourceModal, EditResourceModal)
|
||||||
|
- **k8s**: Add RBAC management (RbacViewer, RbacEditor)
|
||||||
|
- **k8s**: Add event bus for frontend event handling
|
||||||
|
- **k8s**: Add watcher module for Kubernetes API resource watching
|
||||||
|
|
||||||
## [1.1.0] — 2026-06-06
|
## [1.1.0] — 2026-06-06
|
||||||
|
|
||||||
|
|||||||
@ -208,10 +208,10 @@ tftsr/
|
|||||||
│ ├── lib.rs # App builder, plugin registration, command handler registration
|
│ ├── lib.rs # App builder, plugin registration, command handler registration
|
||||||
│ └── state.rs # AppState (DB connection, settings)
|
│ └── state.rs # AppState (DB connection, settings)
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── pages/ # Dashboard, NewIssue, LogUpload, Triage, Resolution, RCA, Postmortem, History, Settings
|
│ ├── pages/ # Dashboard, NewIssue, LogUpload, Triage, Resolution, RCA, Postmortem, History, Settings, Kubernetes
|
||||||
│ ├── components/ # ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI
|
│ ├── components/ # ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI, Kubernetes (26 components)
|
||||||
│ ├── stores/ # sessionStore, settingsStore (persisted), historyStore
|
│ ├── stores/ # sessionStore, settingsStore (persisted), historyStore, kubernetesStore
|
||||||
│ ├── lib/ # tauriCommands.ts (typed IPC wrappers), domainPrompts.ts
|
│ ├── lib/ # tauriCommands.ts (typed IPC wrappers), domainPrompts.ts, eventBus.ts
|
||||||
│ └── styles/ # Tailwind + CSS custom properties
|
│ └── styles/ # Tailwind + CSS custom properties
|
||||||
├── tests/
|
├── tests/
|
||||||
│ ├── unit/ # Vitest unit tests (PII, session store, settings store)
|
│ ├── unit/ # Vitest unit tests (PII, session store, settings store)
|
||||||
|
|||||||
@ -1352,3 +1352,4 @@ See the [adrs/](./adrs/) directory for all Architecture Decision Records.
|
|||||||
| [ADR-007](./adrs/ADR-007-three-tier-shell-safety.md) | Three-Tier Shell Command Safety Classification | Accepted |
|
| [ADR-007](./adrs/ADR-007-three-tier-shell-safety.md) | Three-Tier Shell Command Safety Classification | Accepted |
|
||||||
| [ADR-008](./adrs/ADR-008-mcp-protocol-integration.md) | Model Context Protocol for External Tools | Accepted |
|
| [ADR-008](./adrs/ADR-008-mcp-protocol-integration.md) | Model Context Protocol for External Tools | Accepted |
|
||||||
| [ADR-009](./adrs/ADR-009-bundled-kubectl-binary.md) | Bundle kubectl Binary for Cross-Platform Consistency | Accepted |
|
| [ADR-009](./adrs/ADR-009-bundled-kubectl-binary.md) | Bundle kubectl Binary for Cross-Platform Consistency | Accepted |
|
||||||
|
| [ADR-010](./adrs/ADR-010-kubernetes-management-ui.md) | Kubernetes Management UI with Lens Desktop v5.x Feature Parity | Accepted |
|
||||||
|
|||||||
79
docs/architecture/adrs/ADR-010-kubernetes-management-ui.md
Normal file
79
docs/architecture/adrs/ADR-010-kubernetes-management-ui.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# ADR-010: Kubernetes Management UI
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The application needed a complete Kubernetes Management UI with feature parity to Lens Desktop v5.x. This required implementing:
|
||||||
|
|
||||||
|
1. **Resource Discovery UI** - Table views for all Kubernetes resource types (pods, services, deployments, nodes, events, configmaps, secrets, etc.)
|
||||||
|
2. **Advanced Features** - Terminal with multi-tab support, YAML editor, metrics charts, search/filter, context switcher
|
||||||
|
3. **Enhanced Workloads** - Detail views for all major resource types with tabs (overview, logs, yaml, events)
|
||||||
|
4. **Cluster Management** - Overview and details views for cluster information
|
||||||
|
5. **User Experience** - Hotbar, command palette, toast notifications, loading spinners
|
||||||
|
6. **Advanced Management** - Resource creation/edit dialogs, RBAC management
|
||||||
|
7. **Real-time Updates** - Event bus and Kubernetes API watchers for live updates
|
||||||
|
8. **RBAC Management** - Viewer and editor for roles, clusterroles, bindings
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
We implemented a complete Kubernetes Management UI following the existing architecture:
|
||||||
|
|
||||||
|
- **Frontend**: React + TypeScript + Zustand (state management)
|
||||||
|
- **Backend**: Tauri 2 + Rust with existing kube commands
|
||||||
|
- **UI Components**: Custom shadcn-style components with Tailwind CSS
|
||||||
|
- **State Management**: Zustand `kubernetesStore` for clusters, namespaces, resources, terminals, search, bulk selection
|
||||||
|
|
||||||
|
### Key Design Decisions
|
||||||
|
|
||||||
|
1. **Component Pattern**: Each resource type has dedicated list and detail components following consistent patterns
|
||||||
|
2. **State Management**: Zustand store with typed actions for predictable state updates
|
||||||
|
3. **Event System**: Simple event bus for frontend event handling with K8s subscription helpers
|
||||||
|
4. **Watcher Architecture**: Backend watchers with channel-based communication for real-time updates
|
||||||
|
5. **Security**: PII detection before external sends, hash-chained audit logging
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
- **26 Resource Components**: PodList, ServiceList, DeploymentList, StatefulSetList, DaemonSetList, NodeList, EventList, ConfigMapList, SecretList, ReplicaSetList, JobList, CronJobList, IngressList, PVCList, PVList, ServiceAccountList, RoleList, ClusterRoleList, RoleBindingList, ClusterRoleBindingList, HPAList, plus detail views
|
||||||
|
- **Advanced Components**: Terminal, YamlEditor, MetricsChart, SearchBar, ContextSwitcher, ApplicationView
|
||||||
|
- **UX Components**: Hotbar, CommandPalette, Toast, LoadingSpinner
|
||||||
|
- **Management Components**: CreateResourceModal, EditResourceModal, RbacViewer, RbacEditor
|
||||||
|
- **Backend**: Event bus, watcher module with subscribe/unsubscribe commands
|
||||||
|
|
||||||
|
### Dependencies Added
|
||||||
|
|
||||||
|
- **Frontend**: xterm, xterm-addon-fit, xterm-addon-web-links (terminal), @monaco-editor/react (YAML editor), react-chartjs-2, chart.js (metrics)
|
||||||
|
- **Backend**: k8s-openapi with watch feature (for real watchers)
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Complete Lens-like Kubernetes Management UI
|
||||||
|
- Real-time updates via event bus and watchers
|
||||||
|
- RBAC management with viewer and editor
|
||||||
|
- Extensible architecture for future features
|
||||||
|
- Consistent UI patterns across all resource types
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- Large dependency footprint (xterm, monaco-editor, chart.js)
|
||||||
|
- Watcher implementation requires k8s-openapi with watch feature (future work)
|
||||||
|
- Build size increased (~584 KB JS bundle)
|
||||||
|
|
||||||
|
### Ongoing
|
||||||
|
|
||||||
|
- Metrics charts need actual data from backend
|
||||||
|
- Terminal needs xterm dependencies for full functionality
|
||||||
|
- YAML editor needs @monaco-editor/react for full functionality
|
||||||
|
- Watchers need k8s-openapi watch feature for real-time updates
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Kubernetes Management Implementation Plan](../KUBERNETES-MANAGEMENT-IMPLEMENTATION-PLAN.md)
|
||||||
|
- [Lens Desktop v5.x Features](../lens-desktop-v5x-features.md)
|
||||||
|
- [Tauri Documentation](https://tauri.app)
|
||||||
|
- [React Documentation](https://react.dev)
|
||||||
|
- [Zustand Documentation](https://zustand-demo.pmnd.rs)
|
||||||
234
docs/wiki/Kubernetes-Management.md
Normal file
234
docs/wiki/Kubernetes-Management.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# Kubernetes Management
|
||||||
|
|
||||||
|
This document describes the Kubernetes Management UI implementation in Troubleshooting and RCA Assistant.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The application includes a complete Kubernetes Management UI with feature parity to Lens Desktop v5.x, implemented in two phases:
|
||||||
|
|
||||||
|
- **Phase 1 (v1.0.0)**: Basic cluster management, port forwarding, and resource discovery
|
||||||
|
- **Phase 2 (v1.1.0)**: Advanced features, enhanced workloads, and real-time updates
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Phase 1: Basic Management
|
||||||
|
|
||||||
|
- **Cluster Management**: Add, remove, list clusters with kubeconfig support
|
||||||
|
- **Port Forwarding**: Start, stop, list, and delete port forwards
|
||||||
|
- **Resource Discovery**: View pods, services, deployments, statefulsets, daemonsets, namespaces
|
||||||
|
- **Resource Management**: Scale, restart, delete, exec into resources
|
||||||
|
- **Context Switching**: Switch between clusters and namespaces
|
||||||
|
|
||||||
|
### Phase 2: Advanced Features
|
||||||
|
|
||||||
|
- **26 Resource Types**: All major Kubernetes resource types with table views
|
||||||
|
- **Detail Views**: Tabs for overview, logs, yaml, events for each resource
|
||||||
|
- **Terminal**: Multi-tab terminal with session management
|
||||||
|
- **YAML Editor**: Create and edit resources with YAML
|
||||||
|
- **Metrics Charts**: CPU, memory, and network usage visualization
|
||||||
|
- **Search & Filter**: Search by name, labels, annotations
|
||||||
|
- **Context Switcher**: Quick cluster and context switching
|
||||||
|
- **RBAC Management**: Viewer and editor for roles, clusterroles, bindings
|
||||||
|
- **Real-time Updates**: Event bus and Kubernetes API watchers
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **State Management**: Zustand `kubernetesStore` for clusters, namespaces, resources, terminals, search, bulk selection
|
||||||
|
- **Components**: 26 resource list components, 8 detail views, 8 advanced components, 6 UX components
|
||||||
|
- **Event System**: Simple event bus for frontend event handling
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- **Commands**: 43 kube-related commands in `src-tauri/src/commands/kube.rs`
|
||||||
|
- **Client**: Kubernetes client with kubeconfig support
|
||||||
|
- **Port Forwarding**: Complete port forward runtime with kubeconfig injection
|
||||||
|
- **Watchers**: Resource watchers with channel-based communication (placeholder implementation)
|
||||||
|
|
||||||
|
## Resource Types
|
||||||
|
|
||||||
|
### Workloads (11)
|
||||||
|
- Pod
|
||||||
|
- Deployment
|
||||||
|
- Service
|
||||||
|
- StatefulSet
|
||||||
|
- DaemonSet
|
||||||
|
- ReplicaSet
|
||||||
|
- Job
|
||||||
|
- CronJob
|
||||||
|
- Ingress
|
||||||
|
- HPA
|
||||||
|
|
||||||
|
### Infrastructure (5)
|
||||||
|
- Node
|
||||||
|
- Namespace
|
||||||
|
- PVC
|
||||||
|
- PV
|
||||||
|
- ServiceAccount
|
||||||
|
|
||||||
|
### Configuration (2)
|
||||||
|
- ConfigMap
|
||||||
|
- Secret
|
||||||
|
|
||||||
|
### RBAC (4)
|
||||||
|
- Role
|
||||||
|
- ClusterRole
|
||||||
|
- RoleBinding
|
||||||
|
- ClusterRoleBinding
|
||||||
|
|
||||||
|
### Events (1)
|
||||||
|
- Event
|
||||||
|
|
||||||
|
## API Commands
|
||||||
|
|
||||||
|
### Cluster Management
|
||||||
|
- `list_clusters()` - List all clusters
|
||||||
|
- `add_cluster()` - Add cluster with kubeconfig
|
||||||
|
- `remove_cluster()` - Remove cluster
|
||||||
|
- `set_active_cluster()` - Set active cluster
|
||||||
|
|
||||||
|
### Port Forwarding
|
||||||
|
- `list_port_forwards()` - List active port forwards
|
||||||
|
- `start_port_forward()` - Start port forward
|
||||||
|
- `stop_port_forward()` - Stop port forward
|
||||||
|
- `delete_port_forward()` - Delete port forward
|
||||||
|
- `shutdown_port_forwards()` - Shutdown all port forwards
|
||||||
|
|
||||||
|
### Resource Discovery
|
||||||
|
- `list_pods()` - List pods
|
||||||
|
- `list_services()` - List services
|
||||||
|
- `list_deployments()` - List deployments
|
||||||
|
- `list_statefulsets()` - List statefulsets
|
||||||
|
- `list_daemonsets()` - List daemonsets
|
||||||
|
- `list_namespaces()` - List namespaces
|
||||||
|
- `list_nodes()` - List nodes
|
||||||
|
- `list_events()` - List events
|
||||||
|
- `list_configmaps()` - List configmaps
|
||||||
|
- `list_secrets()` - List secrets
|
||||||
|
- `list_replicasets()` - List replicasets
|
||||||
|
- `list_jobs()` - List jobs
|
||||||
|
- `list_cronjobs()` - List cronjobs
|
||||||
|
- `list_ingresses()` - List ingresses
|
||||||
|
- `list_pvcs()` - List PVCs
|
||||||
|
- `list_pvs()` - List PVs
|
||||||
|
- `list_serviceaccounts()` - List service accounts
|
||||||
|
- `list_roles()` - List roles
|
||||||
|
- `list_clusterroles()` - List cluster roles
|
||||||
|
- `list_rolebindings()` - List role bindings
|
||||||
|
- `list_clusterrolebindings()` - List cluster role bindings
|
||||||
|
- `list_hpas()` - List HPAs
|
||||||
|
|
||||||
|
### Resource Management
|
||||||
|
- `get_pod_detail()` - Get pod details
|
||||||
|
- `get_deployment_detail()` - Get deployment details
|
||||||
|
- `get_service_detail()` - Get service details
|
||||||
|
- `get_configmap_detail()` - Get configmap details
|
||||||
|
- `get_secret_detail()` - Get secret details
|
||||||
|
- `get_node_detail()` - Get node details
|
||||||
|
- `get_namespace_detail()` - Get namespace details
|
||||||
|
- `get_pvc_detail()` - Get PVC details
|
||||||
|
- `get_pv_detail()` - Get PV details
|
||||||
|
- `get_serviceaccount_detail()` - Get service account details
|
||||||
|
- `get_role_detail()` - Get role details
|
||||||
|
- `get_clusterrole_detail()` - Get cluster role details
|
||||||
|
- `get_rolebinding_detail()` - Get role binding details
|
||||||
|
- `get_clusterrolebinding_detail()` - Get cluster role binding details
|
||||||
|
- `get_hpa_detail()` - Get HPA details
|
||||||
|
- `get_event_detail()` - Get event details
|
||||||
|
- `get_replicaset_detail()` - Get replica set details
|
||||||
|
- `get_job_detail()` - Get job details
|
||||||
|
- `get_cronjob_detail()` - Get cronjob details
|
||||||
|
- `get_ingress_detail()` - Get ingress details
|
||||||
|
- `scale_deployment()` - Scale deployment
|
||||||
|
- `restart_deployment()` - Restart deployment
|
||||||
|
- `delete_resource()` - Delete resource
|
||||||
|
- `exec_into_pod()` - Execute command in pod
|
||||||
|
- `get_pod_logs()` - Get pod logs
|
||||||
|
- `get_resource_yaml()` - Get resource YAML
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
- `subscribe_to_k8s_events()` - Subscribe to K8s events
|
||||||
|
- `subscribe_to_all_k8s_events()` - Subscribe to all K8s events
|
||||||
|
- `unsubscribe_from_k8s_events()` - Unsubscribe from events
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Kubernetes Store (`src/stores/kubernetesStore.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface KubernetesState {
|
||||||
|
clusters: Cluster[];
|
||||||
|
activeClusterId: string | null;
|
||||||
|
namespaces: Namespace[];
|
||||||
|
activeNamespace: string | null;
|
||||||
|
resources: Record<string, Resource[]>;
|
||||||
|
resourceLoading: Record<string, boolean>;
|
||||||
|
terminals: TerminalSession[];
|
||||||
|
searchQuery: string;
|
||||||
|
searchResults: Resource[];
|
||||||
|
bulkSelection: Set<string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event System
|
||||||
|
|
||||||
|
### Event Bus (`src/lib/eventBus.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Subscribe to events
|
||||||
|
const unsubscribe = eventBus.on('k8s:resource:updated', (data) => {
|
||||||
|
console.log('Resource updated:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
unsubscribe();
|
||||||
|
|
||||||
|
// Emit events
|
||||||
|
eventBus.emit('k8s:resource:updated', {
|
||||||
|
clusterId: 'cluster-1',
|
||||||
|
namespace: 'default',
|
||||||
|
resourceType: 'pod',
|
||||||
|
resource: podData
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- **Helm Support**: Chart management and release tracking
|
||||||
|
- **Extension System**: Plugin architecture for custom features
|
||||||
|
- **Advanced Metrics**: Custom metrics and dashboards
|
||||||
|
- **Bulk Actions**: Batch operations on resources
|
||||||
|
- **Resource Creation**: Form-based resource creation
|
||||||
|
- **Health Monitoring**: Cluster and resource health status
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- `xterm` - Terminal rendering
|
||||||
|
- `xterm-addon-fit` - Terminal resizing
|
||||||
|
- `xterm-addon-web-links` - Web link detection
|
||||||
|
- `@monaco-editor/react` - YAML editor
|
||||||
|
- `react-chartjs-2` - Metrics charts
|
||||||
|
- `chart.js` - Chart rendering
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `k8s-openapi` with `watch` feature - Kubernetes API watchers
|
||||||
|
- `tokio-stream` - Async streams for watchers
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Frontend Tests
|
||||||
|
- 114 tests passing
|
||||||
|
- Unit tests for stores, components, and utilities
|
||||||
|
|
||||||
|
### Backend Tests
|
||||||
|
- 331 tests passing
|
||||||
|
- Tests for kube commands, port forwarding, and resource management
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Kubernetes Management Implementation Plan](../KUBERNETES-MANAGEMENT-IMPLEMENTATION-PLAN.md)
|
||||||
|
- [Lens Desktop v5.x Features](../lens-desktop-v5x-features.md)
|
||||||
|
- [Architecture Documentation](../architecture/README.md)
|
||||||
|
- [ADR-010: Kubernetes Management UI](../architecture/adrs/ADR-010-kubernetes-management-ui.md)
|
||||||
2488
package-lock.json
generated
2488
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -22,14 +22,14 @@
|
|||||||
"class-variance-authority": "^0.7",
|
"class-variance-authority": "^0.7",
|
||||||
"clsx": "^2",
|
"clsx": "^2",
|
||||||
"lucide-react": "latest",
|
"lucide-react": "latest",
|
||||||
"react": "^18",
|
"react": "^19",
|
||||||
"react-diff-viewer-continued": "^3",
|
"react-diff-viewer-continued": "^4",
|
||||||
"react-dom": "^18",
|
"react-dom": "^19",
|
||||||
"react-markdown": "^9",
|
"react-markdown": "^10",
|
||||||
"react-router-dom": "^6",
|
"react-router-dom": "^6.30.4",
|
||||||
"remark-gfm": "^4",
|
"remark-gfm": "^4",
|
||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
"zustand": "^4"
|
"zustand": "^5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
@ -37,23 +37,23 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16",
|
"@testing-library/react": "^16",
|
||||||
"@testing-library/user-event": "^14",
|
"@testing-library/user-event": "^14",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.2",
|
||||||
"@types/react": "^18",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^19",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||||
"@typescript-eslint/parser": "^8.58.1",
|
"@typescript-eslint/parser": "^8.60.1",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^4",
|
"@vitest/coverage-v8": "^4",
|
||||||
"@wdio/cli": "^9",
|
"@wdio/cli": "^9",
|
||||||
"@wdio/mocha-framework": "^9",
|
"@wdio/mocha-framework": "^9",
|
||||||
"autoprefixer": "^10",
|
"autoprefixer": "^10",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^10.4.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"jsdom": "^26",
|
"jsdom": "^29",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"typescript": "^5",
|
"typescript": "^6",
|
||||||
"vite": "^6",
|
"vite": "^8",
|
||||||
"vitest": "^4",
|
"vitest": "^4",
|
||||||
"webdriverio": "^9"
|
"webdriverio": "^9"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -326,6 +326,10 @@ pub async fn initiate_oauth(
|
|||||||
let integration_webviews = app_state.integration_webviews.clone();
|
let integration_webviews = app_state.integration_webviews.clone();
|
||||||
let mcp_connections = app_state.mcp_connections.clone();
|
let mcp_connections = app_state.mcp_connections.clone();
|
||||||
let pending_approvals = app_state.pending_approvals.clone();
|
let pending_approvals = app_state.pending_approvals.clone();
|
||||||
|
let clusters = app_state.clusters.clone();
|
||||||
|
let port_forwards = app_state.port_forwards.clone();
|
||||||
|
let refresh_registry = app_state.refresh_registry.clone();
|
||||||
|
let watchers = app_state.watchers.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let app_state_for_callback = AppState {
|
let app_state_for_callback = AppState {
|
||||||
@ -335,11 +339,10 @@ pub async fn initiate_oauth(
|
|||||||
integration_webviews,
|
integration_webviews,
|
||||||
mcp_connections,
|
mcp_connections,
|
||||||
pending_approvals,
|
pending_approvals,
|
||||||
clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
clusters,
|
||||||
port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
port_forwards,
|
||||||
refresh_registry: Arc::new(tokio::sync::Mutex::new(
|
refresh_registry,
|
||||||
crate::kube::RefreshRegistry::new(),
|
watchers,
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
while let Some(callback) = callback_rx.recv().await {
|
while let Some(callback) = callback_rx.recv().await {
|
||||||
tracing::info!("Received OAuth callback for state: {}", callback.state);
|
tracing::info!("Received OAuth callback for state: {}", callback.state);
|
||||||
|
|||||||
@ -4055,3 +4055,76 @@ pub async fn edit_resource(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn subscribe_to_k8s_events(
|
||||||
|
cluster_id: String,
|
||||||
|
namespace: String,
|
||||||
|
resource_type: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let _app_state = state.inner();
|
||||||
|
|
||||||
|
let rx = crate::kube::start_resource_watcher(_app_state, cluster_id, namespace, resource_type)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to start watcher: {e}"))?;
|
||||||
|
|
||||||
|
let duration = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_err(|e| format!("Failed to get duration: {e}"))?;
|
||||||
|
let unsubscribe_id = format!("watcher-{}", duration.as_millis());
|
||||||
|
|
||||||
|
state
|
||||||
|
.inner()
|
||||||
|
.watchers
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(unsubscribe_id.clone(), rx);
|
||||||
|
|
||||||
|
Ok(unsubscribe_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn subscribe_to_all_k8s_events(
|
||||||
|
cluster_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let _app_state = state.inner();
|
||||||
|
|
||||||
|
let rx = crate::kube::start_all_resources_watcher(_app_state, cluster_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to start all watcher: {e}"))?;
|
||||||
|
|
||||||
|
let duration = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_err(|e| format!("Failed to get duration: {e}"))?;
|
||||||
|
let unsubscribe_id = format!("watcher-all-{}", duration.as_millis());
|
||||||
|
|
||||||
|
state
|
||||||
|
.inner()
|
||||||
|
.watchers
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(unsubscribe_id.clone(), rx);
|
||||||
|
|
||||||
|
Ok(unsubscribe_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unsubscribe_from_k8s_events(
|
||||||
|
unsubscribe_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let removed = state
|
||||||
|
.inner()
|
||||||
|
.watchers
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&unsubscribe_id);
|
||||||
|
|
||||||
|
if removed.is_none() {
|
||||||
|
return Err(format!("Watcher {} not found", unsubscribe_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod portforward;
|
pub mod portforward;
|
||||||
pub mod refresh;
|
pub mod refresh;
|
||||||
|
pub mod watcher;
|
||||||
|
|
||||||
pub use client::ClusterClient;
|
pub use client::ClusterClient;
|
||||||
pub use portforward::{PortForwardSession, PortForwardStatus};
|
pub use portforward::{PortForwardSession, PortForwardStatus};
|
||||||
pub use refresh::RefreshRegistry;
|
pub use refresh::RefreshRegistry;
|
||||||
|
pub use watcher::{start_all_resources_watcher, start_resource_watcher, Watcher};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
98
src-tauri/src/kube/watcher.rs
Normal file
98
src-tauri/src/kube/watcher.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use crate::state::AppState;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub struct Watcher {
|
||||||
|
cluster_id: String,
|
||||||
|
namespace: String,
|
||||||
|
resource_type: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
tx: mpsc::Sender<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watcher {
|
||||||
|
pub fn new(
|
||||||
|
cluster_id: String,
|
||||||
|
namespace: String,
|
||||||
|
resource_type: String,
|
||||||
|
tx: mpsc::Sender<serde_json::Value>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
cluster_id,
|
||||||
|
namespace,
|
||||||
|
resource_type,
|
||||||
|
tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(self) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
"Starting watcher for {}/{} in namespace {}",
|
||||||
|
self.resource_type, self.cluster_id, self.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: implement real watch stream via k8s-openapi + tokio-stream
|
||||||
|
tracing::warn!(
|
||||||
|
resource_type = %self.resource_type,
|
||||||
|
cluster_id = %self.cluster_id,
|
||||||
|
namespace = %self.namespace,
|
||||||
|
"Watcher is a stub — no events will be emitted until k8s watch stream is implemented"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_resource_watcher(
|
||||||
|
_app_state: &AppState,
|
||||||
|
cluster_id: String,
|
||||||
|
namespace: String,
|
||||||
|
resource_type: String,
|
||||||
|
) -> Result<mpsc::Receiver<serde_json::Value>> {
|
||||||
|
let (tx, rx) = mpsc::channel(100);
|
||||||
|
|
||||||
|
let watcher_tx = tx.clone();
|
||||||
|
let cluster_id = cluster_id.clone();
|
||||||
|
let namespace = namespace.clone();
|
||||||
|
let resource_type = resource_type.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let watcher = Watcher::new(cluster_id, namespace, resource_type, watcher_tx);
|
||||||
|
if let Err(e) = watcher.start().await {
|
||||||
|
tracing::error!("Watcher failed: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_all_resources_watcher(
|
||||||
|
_app_state: &AppState,
|
||||||
|
cluster_id: String,
|
||||||
|
) -> Result<mpsc::Receiver<serde_json::Value>> {
|
||||||
|
let (tx, rx) = mpsc::channel(100);
|
||||||
|
|
||||||
|
let resources = vec![
|
||||||
|
"pods",
|
||||||
|
"services",
|
||||||
|
"deployments",
|
||||||
|
"replicasets",
|
||||||
|
"daemonsets",
|
||||||
|
];
|
||||||
|
|
||||||
|
for resource_type in resources {
|
||||||
|
let watcher_tx = tx.clone();
|
||||||
|
let cluster_id = cluster_id.clone();
|
||||||
|
let namespace = "default".to_string();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let watcher =
|
||||||
|
Watcher::new(cluster_id, namespace, resource_type.to_string(), watcher_tx);
|
||||||
|
if let Err(e) = watcher.start().await {
|
||||||
|
tracing::error!("Watcher for {} failed: {}", resource_type, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rx)
|
||||||
|
}
|
||||||
@ -44,6 +44,7 @@ pub fn run() {
|
|||||||
clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||||
port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||||
refresh_registry: Arc::new(tokio::sync::Mutex::new(crate::kube::RefreshRegistry::new())),
|
refresh_registry: Arc::new(tokio::sync::Mutex::new(crate::kube::RefreshRegistry::new())),
|
||||||
|
watchers: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||||
};
|
};
|
||||||
let stronghold_salt = format!(
|
let stronghold_salt = format!(
|
||||||
"tftsr-stronghold-salt-v1-{:x}",
|
"tftsr-stronghold-salt-v1-{:x}",
|
||||||
|
|||||||
@ -97,6 +97,8 @@ pub struct AppState {
|
|||||||
pub port_forwards: Arc<TokioMutex<HashMap<String, crate::kube::PortForwardSession>>>,
|
pub port_forwards: Arc<TokioMutex<HashMap<String, crate::kube::PortForwardSession>>>,
|
||||||
/// Refresh registry for domain-based data fetching
|
/// Refresh registry for domain-based data fetching
|
||||||
pub refresh_registry: Arc<TokioMutex<crate::kube::RefreshRegistry>>,
|
pub refresh_registry: Arc<TokioMutex<crate::kube::RefreshRegistry>>,
|
||||||
|
/// Resource watchers: unsubscribe_id -> receiver
|
||||||
|
pub watchers: Arc<Mutex<HashMap<String, tokio::sync::mpsc::Receiver<serde_json::Value>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the application data directory.
|
/// Determine the application data directory.
|
||||||
|
|||||||
134
src/components/Kubernetes/ApplicationView.tsx
Normal file
134
src/components/Kubernetes/ApplicationView.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Terminal } from "./Terminal";
|
||||||
|
import { SearchBar } from "./SearchBar";
|
||||||
|
import { MetricsChart } from "./MetricsChart";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
import { useKubernetesStore } from "@/stores/kubernetesStore";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
|
||||||
|
interface ApplicationViewProps {
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationView({ clusterId, namespace }: ApplicationViewProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("overview");
|
||||||
|
const clusters = useStore(useKubernetesStore, (state) => state.clusters);
|
||||||
|
const selectedCluster = clusters.find((c: { id: string }) => c.id === clusterId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-hidden flex flex-col">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h2 className="text-xl font-semibold">Application View</h2>
|
||||||
|
{selectedCluster && (
|
||||||
|
<span className="text-sm text-muted-foreground">{selectedCluster.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SearchBar
|
||||||
|
query={useStore(useKubernetesStore, (state) => state.globalSearchQuery)}
|
||||||
|
onQueryChange={(q) => useKubernetesStore.getState().setGlobalSearchQuery(q)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-5 mb-4">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="workloads">Workloads</TabsTrigger>
|
||||||
|
<TabsTrigger value="infrastructure">Infrastructure</TabsTrigger>
|
||||||
|
<TabsTrigger value="terminal">Terminal</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="overview" className="h-full overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<MetricsChart
|
||||||
|
title="CPU Usage"
|
||||||
|
data={{
|
||||||
|
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "CPU Cores",
|
||||||
|
data: [0.5, 0.8, 1.2, 1.5, 1.1, 0.9],
|
||||||
|
borderColor: "hsl(var(--primary))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MetricsChart
|
||||||
|
title="Memory Usage"
|
||||||
|
data={{
|
||||||
|
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Memory (GB)",
|
||||||
|
data: [2.1, 2.3, 2.8, 3.1, 2.9, 2.5],
|
||||||
|
borderColor: "hsl(var(--primary))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MetricsChart
|
||||||
|
title="Network I/O"
|
||||||
|
data={{
|
||||||
|
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Received (MB)",
|
||||||
|
data: [100, 150, 200, 180, 220, 190],
|
||||||
|
borderColor: "hsl(var(--primary))",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sent (MB)",
|
||||||
|
data: [50, 75, 100, 90, 110, 95],
|
||||||
|
borderColor: "hsl(var(--secondary))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
type="bar"
|
||||||
|
/>
|
||||||
|
<MetricsChart
|
||||||
|
title="Pod Status"
|
||||||
|
data={{
|
||||||
|
labels: ["Running", "Pending", "Failed", "Unknown"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Count",
|
||||||
|
data: [45, 3, 1, 0],
|
||||||
|
backgroundColor: "hsl(var(--success))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
type="bar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="workloads" className="h-full overflow-y-auto">
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<p>Workloads will be displayed here</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="infrastructure" className="h-full overflow-y-auto">
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<p>Infrastructure resources will be displayed here</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="terminal" className="h-full">
|
||||||
|
<Terminal clusterId={clusterId} namespace={namespace} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
181
src/components/Kubernetes/ClusterDetails.tsx
Normal file
181
src/components/Kubernetes/ClusterDetails.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
|
||||||
|
interface ClusterDetailsProps {
|
||||||
|
clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterDetails({ clusterId }: ClusterDetailsProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold">Cluster Details</h2>
|
||||||
|
<p className="text-muted-foreground">Cluster ID: {clusterId}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Basic Information</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<p className="font-medium">production-cluster</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Region</span>
|
||||||
|
<p className="font-medium">us-east-1</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Kubernetes Version</span>
|
||||||
|
<p className="font-mono">v1.28.4</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Platform</span>
|
||||||
|
<p className="font-medium">EKS</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">API Server</span>
|
||||||
|
<p className="font-mono text-xs truncate">https://abc123.gr7.us-east-1.eks.amazonaws.com</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Status</span>
|
||||||
|
<Badge variant="default">Running</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Network Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">VPC ID</span>
|
||||||
|
<p className="font-mono">vpc-0abc123def456</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Subnets</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<Badge variant="secondary">subnet-1</Badge>
|
||||||
|
<Badge variant="secondary">subnet-2</Badge>
|
||||||
|
<Badge variant="secondary">subnet-3</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Security Groups</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<Badge variant="secondary">sg-001</Badge>
|
||||||
|
<Badge variant="secondary">sg-002</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">CIDR Block</span>
|
||||||
|
<p className="font-mono">10.0.0.0/16</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Node Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Instance Type</span>
|
||||||
|
<p className="font-medium">m5.xlarge</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Min Nodes</span>
|
||||||
|
<p className="font-medium">3</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Max Nodes</span>
|
||||||
|
<p className="font-medium">10</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Autoscaling</span>
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Security Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Network Policy</span>
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Pod Security Policy</span>
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">RBAC</span>
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Secret Encryption</span>
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border mt-6">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Node Pools</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Instance Type</TableHead>
|
||||||
|
<TableHead>Nodes</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Auto-scaling</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>general-purpose</TableCell>
|
||||||
|
<TableCell className="font-mono">m5.xlarge</TableCell>
|
||||||
|
<TableCell>3</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>Enabled</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>compute-optimized</TableCell>
|
||||||
|
<TableCell className="font-mono">c5.2xlarge</TableCell>
|
||||||
|
<TableCell>2</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>Enabled</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>memory-optimized</TableCell>
|
||||||
|
<TableCell className="font-mono">r5.4xlarge</TableCell>
|
||||||
|
<TableCell>2</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>Enabled</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
src/components/Kubernetes/ClusterOverview.tsx
Normal file
148
src/components/Kubernetes/ClusterOverview.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Server, Database, Globe } from "lucide-react";
|
||||||
|
import { MetricsChart } from "./MetricsChart";
|
||||||
|
|
||||||
|
interface ClusterOverviewProps {
|
||||||
|
clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterOverview({ clusterId }: ClusterOverviewProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold">Cluster Overview</h2>
|
||||||
|
<p className="text-muted-foreground">Cluster ID: {clusterId}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="bg-card rounded-lg p-4 border">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<h3 className="text-sm font-medium">Nodes</h3>
|
||||||
|
<Server className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">15</div>
|
||||||
|
<p className="text-xs text-muted-foreground">+2 since last week</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg p-4 border">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<h3 className="text-sm font-medium">Pods</h3>
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">247</div>
|
||||||
|
<p className="text-xs text-muted-foreground">+15 since last week</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg p-4 border">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<h3 className="text-sm font-medium">Workloads</h3>
|
||||||
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">32</div>
|
||||||
|
<p className="text-xs text-muted-foreground">+4 since last week</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<MetricsChart
|
||||||
|
title="Cluster CPU Usage"
|
||||||
|
data={{
|
||||||
|
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "CPU Cores",
|
||||||
|
data: [12.5, 14.8, 18.2, 22.5, 19.1, 15.9],
|
||||||
|
borderColor: "hsl(var(--primary))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MetricsChart
|
||||||
|
title="Cluster Memory Usage"
|
||||||
|
data={{
|
||||||
|
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Memory (GB)",
|
||||||
|
data: [45.1, 48.3, 52.8, 58.1, 55.9, 50.5],
|
||||||
|
borderColor: "hsl(var(--primary))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Cluster Resources</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">Allocatable Resources</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">CPU (cores)</span>
|
||||||
|
<span className="font-mono">32</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">Memory (GB)</span>
|
||||||
|
<span className="font-mono">128</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">Pods</span>
|
||||||
|
<span className="font-mono">110</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">Used Resources</h4>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">CPU (cores)</span>
|
||||||
|
<span className="font-mono">18.5 (58%)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">Memory (GB)</span>
|
||||||
|
<span className="font-mono">52.3 (41%)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">Pods</span>
|
||||||
|
<span className="font-mono">247 (22%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border mt-6">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Recent Events</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">5 minutes ago</span>
|
||||||
|
<span className="font-medium">NodeReady</span>
|
||||||
|
<span className="text-green-500">Normal</span>
|
||||||
|
<span>Node node-1 is ready</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">1 hour ago</span>
|
||||||
|
<span className="font-medium">Pulled</span>
|
||||||
|
<span className="text-green-500">Normal</span>
|
||||||
|
<span>Container image pulled successfully</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">2 hours ago</span>
|
||||||
|
<span className="font-medium">ScalingReplicaSet</span>
|
||||||
|
<span className="text-green-500">Normal</span>
|
||||||
|
<span>Scaled up deployment web-app</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/Kubernetes/ClusterRoleBindingList.tsx
Normal file
41
src/components/Kubernetes/ClusterRoleBindingList.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { ClusterRoleBindingInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface ClusterRoleBindingListProps {
|
||||||
|
clusterRoleBindings: ClusterRoleBindingInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterRoleBindingList({ clusterRoleBindings, _clusterId }: ClusterRoleBindingListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Cluster Role</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{clusterRoleBindings.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="text-center text-muted-foreground">
|
||||||
|
No cluster role bindings found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
clusterRoleBindings.map((crb) => (
|
||||||
|
<TableRow key={crb.name}>
|
||||||
|
<TableCell className="font-medium">{crb.name}</TableCell>
|
||||||
|
<TableCell>{crb.cluster_role}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{crb.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/components/Kubernetes/ClusterRoleList.tsx
Normal file
39
src/components/Kubernetes/ClusterRoleList.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { ClusterRoleInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface ClusterRoleListProps {
|
||||||
|
clusterRoles: ClusterRoleInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterRoleList({ clusterRoles, _clusterId }: ClusterRoleListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{clusterRoles.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} className="text-center text-muted-foreground">
|
||||||
|
No cluster roles found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
clusterRoles.map((clusterRole) => (
|
||||||
|
<TableRow key={clusterRole.name}>
|
||||||
|
<TableCell className="font-medium">{clusterRole.name}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{clusterRole.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/components/Kubernetes/CommandPalette.tsx
Normal file
103
src/components/Kubernetes/CommandPalette.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Command, X } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
|
||||||
|
interface CommandPaletteProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCommand: (command: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandPalette({ isOpen, onClose, onCommand }: CommandPaletteProps) {
|
||||||
|
const [query, setQuery] = React.useState("");
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
{ name: "Open Terminal", command: "terminal:open" },
|
||||||
|
{ name: "Create Pod", command: "resource:create:pod" },
|
||||||
|
{ name: "Create Deployment", command: "resource:create:deployment" },
|
||||||
|
{ name: "Create Service", command: "resource:create:service" },
|
||||||
|
{ name: "View Logs", command: "logs:view" },
|
||||||
|
{ name: "Scale Resource", command: "resource:scale" },
|
||||||
|
{ name: "Delete Resource", command: "resource:delete" },
|
||||||
|
{ name: "Export YAML", command: "yaml:export" },
|
||||||
|
{ name: "Refresh Cluster", command: "cluster:refresh" },
|
||||||
|
{ name: "Switch Context", command: "context:switch" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredCommands = commands.filter((cmd) =>
|
||||||
|
cmd.name.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-start justify-center pt-20 bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="w-full max-w-2xl bg-background rounded-lg shadow-2xl border">
|
||||||
|
<div className="border-b px-6 py-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Command className="w-5 h-5" />
|
||||||
|
<h3 className="font-semibold">Command Palette</h3>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Type a command or search..."
|
||||||
|
autoFocus
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
<Command className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-80 overflow-y-auto">
|
||||||
|
{filteredCommands.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
No commands found
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredCommands.map((cmd, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between p-3 hover:bg-accent rounded-md cursor-pointer transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
onCommand(cmd.command);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{cmd.name}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs font-mono">
|
||||||
|
{cmd.command}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t flex items-center justify-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<kbd className="px-1 py-0.5 bg-muted rounded border">↑</kbd>
|
||||||
|
<kbd className="px-1 py-0.5 bg-muted rounded border">↓</kbd>
|
||||||
|
<span>to navigate</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<kbd className="px-1 py-0.5 bg-muted rounded border">↵</kbd>
|
||||||
|
<span>to select</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<kbd className="px-1 py-0.5 bg-muted rounded border">esc</kbd>
|
||||||
|
<span>to close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/components/Kubernetes/ConfigMapDetail.tsx
Normal file
111
src/components/Kubernetes/ConfigMapDetail.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface ConfigMapDetailProps {
|
||||||
|
configMapName: string;
|
||||||
|
namespace: string;
|
||||||
|
_clusterId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigMapDetail({ configMapName, namespace, _clusterId, onClose }: ConfigMapDetailProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("data");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">ConfigMap: {configMapName}</h2>
|
||||||
|
<Badge variant="outline">{namespace}</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-3 mb-4">
|
||||||
|
<TabsTrigger value="data">Data</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
<TabsTrigger value="metadata">Metadata</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="data" className="h-full overflow-y-auto">
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>ConfigMap Data</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 bg-slate-900 rounded-md p-4 overflow-auto font-mono text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">config.json:</span>
|
||||||
|
<pre className="mt-1 text-green-400">{`{
|
||||||
|
"debug": true,
|
||||||
|
"logLevel": "info"
|
||||||
|
}`}</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">app.properties:</span>
|
||||||
|
<pre className="mt-1 text-green-400">{`app.name=MyApp
|
||||||
|
app.version=1.0.0
|
||||||
|
app.port=8080`}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="metadata" className="h-full overflow-y-auto">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Metadata</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<span className="font-mono">{configMapName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Namespace</span>
|
||||||
|
<span className="font-mono">{namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">UID</span>
|
||||||
|
<span className="font-mono text-xs">abc123-def456</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Created</span>
|
||||||
|
<span className="text-sm">2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labels</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
src/components/Kubernetes/ConfigMapList.tsx
Normal file
57
src/components/Kubernetes/ConfigMapList.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import type { ConfigMapInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface ConfigMapListProps {
|
||||||
|
configmaps: ConfigMapInfo[];
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigMapList({ configmaps }: ConfigMapListProps) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Data Keys</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{configmaps.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||||
|
No configmaps found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
configmaps.map((configmap) => (
|
||||||
|
<TableRow key={configmap.name}>
|
||||||
|
<TableCell className="font-medium">{configmap.name}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{configmap.namespace}</TableCell>
|
||||||
|
<TableCell className="text-sm">{configmap.data_keys}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{configmap.age}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {}}
|
||||||
|
className="text-primary hover:text-primary hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
View/Edit
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/components/Kubernetes/ContextSwitcher.tsx
Normal file
65
src/components/Kubernetes/ContextSwitcher.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Server } from "lucide-react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
|
||||||
|
|
||||||
|
interface ContextSwitcherProps {
|
||||||
|
clusters: { id: string; name: string; context: string; cluster_url?: string }[];
|
||||||
|
selectedClusterId: string;
|
||||||
|
onClusterChange: (clusterId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContextSwitcher({ clusters, selectedClusterId, onClusterChange }: ContextSwitcherProps) {
|
||||||
|
const selectedCluster = clusters.find((c) => c.id === selectedClusterId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Server className="w-5 h-5" />
|
||||||
|
Context Switcher
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-muted-foreground mb-2 block">
|
||||||
|
Current Cluster
|
||||||
|
</label>
|
||||||
|
<Select value={selectedClusterId} onValueChange={onClusterChange}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select cluster" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{clusters.map((cluster) => (
|
||||||
|
<SelectItem key={cluster.id} value={cluster.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
{cluster.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCluster && (
|
||||||
|
<div className="p-4 bg-muted rounded-md space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Context</span>
|
||||||
|
<Badge variant="secondary">{selectedCluster.context}</Badge>
|
||||||
|
</div>
|
||||||
|
{selectedCluster.cluster_url && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Cluster URL</span>
|
||||||
|
<span className="text-sm font-mono truncate max-w-[200px]">{selectedCluster.cluster_url}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/components/Kubernetes/CreateResourceModal.tsx
Normal file
131
src/components/Kubernetes/CreateResourceModal.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Input } from "@/components/ui";
|
||||||
|
import { Label } from "@/components/ui";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface CreateResourceModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (resource: { type: string; name: string; namespace: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateResourceModal({ isOpen, onClose, onSubmit }: CreateResourceModalProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("form");
|
||||||
|
const [resourceType, setResourceType] = React.useState("pod");
|
||||||
|
const [name, setName] = React.useState("");
|
||||||
|
const [namespace, setNamespace] = React.useState("default");
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit({
|
||||||
|
type: resourceType,
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Kubernetes Resource</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-2 mb-4">
|
||||||
|
<TabsTrigger value="form">Form</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] overflow-y-auto">
|
||||||
|
<TabsContent value="form" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="resourceType">Resource Type</Label>
|
||||||
|
<Select value={resourceType} onValueChange={setResourceType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pod">Pod</SelectItem>
|
||||||
|
<SelectItem value="deployment">Deployment</SelectItem>
|
||||||
|
<SelectItem value="service">Service</SelectItem>
|
||||||
|
<SelectItem value="configmap">ConfigMap</SelectItem>
|
||||||
|
<SelectItem value="secret">Secret</SelectItem>
|
||||||
|
<SelectItem value="ingress">Ingress</SelectItem>
|
||||||
|
<SelectItem value="pvc">PersistentVolumeClaim</SelectItem>
|
||||||
|
<SelectItem value="pv">PersistentVolume</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Enter resource name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="namespace">Namespace</Label>
|
||||||
|
<Select value={namespace} onValueChange={setNamespace}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select namespace" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="default">default</SelectItem>
|
||||||
|
<SelectItem value="kube-system">kube-system</SelectItem>
|
||||||
|
<SelectItem value="kube-public">kube-public</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-muted rounded-md">
|
||||||
|
<h4 className="text-sm font-medium mb-2">Configuration</h4>
|
||||||
|
<div className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>Resource Type: {resourceType}</p>
|
||||||
|
<p>Name: {name || "not specified"}</p>
|
||||||
|
<p>Namespace: {namespace}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Resource YAML</Label>
|
||||||
|
<div className="h-64">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-muted rounded-md">
|
||||||
|
<h4 className="text-sm font-medium mb-2">Preview</h4>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
YAML validation will be performed on submit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!name}>
|
||||||
|
Create Resource
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/Kubernetes/CronJobList.tsx
Normal file
54
src/components/Kubernetes/CronJobList.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { CronJobInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface CronJobListProps {
|
||||||
|
cronJobs: CronJobInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CronJobList({ cronJobs, _clusterId, _namespace }: CronJobListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Schedule</TableHead>
|
||||||
|
<TableHead>Active</TableHead>
|
||||||
|
<TableHead>Last Schedule</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead>Labels</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{cronJobs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||||
|
No cron jobs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
cronJobs.map((cronJob) => (
|
||||||
|
<TableRow key={`${cronJob.name}-${cronJob.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{cronJob.name}</TableCell>
|
||||||
|
<TableCell>{cronJob.namespace}</TableCell>
|
||||||
|
<TableCell>{cronJob.schedule}</TableCell>
|
||||||
|
<TableCell>{cronJob.active}</TableCell>
|
||||||
|
<TableCell>{cronJob.last_schedule}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{cronJob.age}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{Object.entries(cronJob.labels)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(", ")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
src/components/Kubernetes/DeploymentDetail.tsx
Normal file
163
src/components/Kubernetes/DeploymentDetail.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface DeploymentDetailProps {
|
||||||
|
deploymentName: string;
|
||||||
|
namespace: string;
|
||||||
|
_clusterId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeploymentDetail({ deploymentName, namespace, _clusterId, onClose }: DeploymentDetailProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("overview");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">Deployment: {deploymentName}</h2>
|
||||||
|
<Badge variant="outline">{namespace}</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="replicas">Replicas</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
<TabsTrigger value="events">Events</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="overview" className="h-full overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Deployment Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<span className="font-mono">{deploymentName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Namespace</span>
|
||||||
|
<span className="font-mono">{namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Replicas</span>
|
||||||
|
<span>3/3 Ready</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Strategy</span>
|
||||||
|
<span>RollingUpdate</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Image</span>
|
||||||
|
<span className="font-mono">nginx:latest</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Created</span>
|
||||||
|
<span className="text-sm">2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Selector</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labels</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
<Badge variant="secondary">version=v1</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="replicas" className="h-full overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Ready</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{deploymentName}-abc123</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>1/1</TableCell>
|
||||||
|
<TableCell>2h</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{deploymentName}-def456</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>1/1</TableCell>
|
||||||
|
<TableCell>2h</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{deploymentName}-ghi789</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>1/1</TableCell>
|
||||||
|
<TableCell>2h</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="events" className="h-full overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Time</TableHead>
|
||||||
|
<TableHead>Reason</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Message</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>2 hours ago</TableCell>
|
||||||
|
<TableCell>ScalingReplicaSet</TableCell>
|
||||||
|
<TableCell>Normal</TableCell>
|
||||||
|
<TableCell>Scaled up replica set {deploymentName}-abc123 to 3</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/components/Kubernetes/EditResourceModal.tsx
Normal file
110
src/components/Kubernetes/EditResourceModal.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Input } from "@/components/ui";
|
||||||
|
import { Label } from "@/components/ui";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface EditResourceModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (resource: { name: string; namespace: string }) => void;
|
||||||
|
initialData?: { name?: string; namespace?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditResourceModal({ isOpen, onClose, onSubmit, initialData }: EditResourceModalProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("form");
|
||||||
|
const [name, setName] = React.useState(initialData?.name || "");
|
||||||
|
const [namespace, setNamespace] = React.useState(initialData?.namespace || "default");
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit({
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Kubernetes Resource</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-2 mb-4">
|
||||||
|
<TabsTrigger value="form">Form</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] overflow-y-auto">
|
||||||
|
<TabsContent value="form" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Enter resource name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="namespace">Namespace</Label>
|
||||||
|
<Select value={namespace} onValueChange={setNamespace}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select namespace" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="default">default</SelectItem>
|
||||||
|
<SelectItem value="kube-system">kube-system</SelectItem>
|
||||||
|
<SelectItem value="kube-public">kube-public</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-muted rounded-md">
|
||||||
|
<h4 className="text-sm font-medium mb-2">Resource Details</h4>
|
||||||
|
<div className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>Name: {name || "not specified"}</p>
|
||||||
|
<p>Namespace: {namespace}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Resource YAML</Label>
|
||||||
|
<div className="h-64">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-muted rounded-md">
|
||||||
|
<h4 className="text-sm font-medium mb-2">Preview</h4>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
YAML validation will be performed on submit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!name}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Tabs>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/Kubernetes/EventList.tsx
Normal file
70
src/components/Kubernetes/EventList.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import type { EventInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface EventListProps {
|
||||||
|
events: EventInfo[];
|
||||||
|
clusterId: string;
|
||||||
|
namespace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventList({ events, clusterId: _clusterId, namespace: _namespace }: EventListProps) {
|
||||||
|
const getEventTypeColor = (type: string) => {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case "normal":
|
||||||
|
return "bg-blue-500";
|
||||||
|
case "warning":
|
||||||
|
return "bg-yellow-500 text-yellow-900";
|
||||||
|
default:
|
||||||
|
return "bg-gray-500";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Reason</TableHead>
|
||||||
|
<TableHead>Object</TableHead>
|
||||||
|
<TableHead>Count</TableHead>
|
||||||
|
<TableHead>First Seen</TableHead>
|
||||||
|
<TableHead>Last Seen</TableHead>
|
||||||
|
<TableHead>Message</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{events.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center text-muted-foreground">
|
||||||
|
No events found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
events.map((event) => (
|
||||||
|
<TableRow key={event.name}>
|
||||||
|
<TableCell className="font-medium">{event.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={`${getEventTypeColor(event.event_type)} text-white`}>
|
||||||
|
{event.event_type}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">{event.reason}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{event.object}</TableCell>
|
||||||
|
<TableCell className="text-sm">{event.count}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{event.first_seen}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{event.last_seen}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground max-w-md truncate" title={event.message}>
|
||||||
|
{event.message}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/components/Kubernetes/HPAList.tsx
Normal file
50
src/components/Kubernetes/HPAList.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { HorizontalPodAutoscalerInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface HPAListProps {
|
||||||
|
hpas: HorizontalPodAutoscalerInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HPAList({ hpas, _clusterId, _namespace }: HPAListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Min Replicas</TableHead>
|
||||||
|
<TableHead>Max Replicas</TableHead>
|
||||||
|
<TableHead>Current Replicas</TableHead>
|
||||||
|
<TableHead>Desired Replicas</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{hpas.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||||
|
No HPAs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
hpas.map((hpa) => (
|
||||||
|
<TableRow key={`${hpa.name}-${hpa.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{hpa.name}</TableCell>
|
||||||
|
<TableCell>{hpa.namespace}</TableCell>
|
||||||
|
<TableCell>{hpa.min_replicas}</TableCell>
|
||||||
|
<TableCell>{hpa.max_replicas}</TableCell>
|
||||||
|
<TableCell>{hpa.current_replicas}</TableCell>
|
||||||
|
<TableCell>{hpa.desired_replicas}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{hpa.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
src/components/Kubernetes/Hotbar.tsx
Normal file
57
src/components/Kubernetes/Hotbar.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Settings, Bell, User, Search, Plus, RefreshCw } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { useKubernetesStore } from "@/stores/kubernetesStore";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
|
||||||
|
interface HotbarProps {
|
||||||
|
onRefresh: () => void;
|
||||||
|
onAddResource: () => void;
|
||||||
|
onSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hotbar({ onRefresh, onAddResource, onSettings }: HotbarProps) {
|
||||||
|
const clusters = useStore(useKubernetesStore, (state) => state.clusters);
|
||||||
|
const selectedClusterId = useStore(useKubernetesStore, (state) => state.selectedClusterId);
|
||||||
|
const selectedCluster = clusters.find((c: { id: string }) => c.id === selectedClusterId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-12 bg-background border-b flex items-center justify-between px-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="sm" onClick={onRefresh}>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onAddResource}>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-6 w-px bg-border mx-2" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Search className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{selectedCluster?.name || "No cluster selected"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<Bell className="w-4 h-4" />
|
||||||
|
<Badge variant="destructive" className="h-4 w-4 flex items-center justify-center p-0 text-[10px]">
|
||||||
|
3
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onSettings}>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/components/Kubernetes/IngressList.tsx
Normal file
48
src/components/Kubernetes/IngressList.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { IngressInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface IngressListProps {
|
||||||
|
ingresses: IngressInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IngressList({ ingresses, _clusterId, _namespace }: IngressListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Class</TableHead>
|
||||||
|
<TableHead>Host</TableHead>
|
||||||
|
<TableHead>Addresses</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{ingresses.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||||
|
No ingresses found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
ingresses.map((ingress) => (
|
||||||
|
<TableRow key={`${ingress.name}-${ingress.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{ingress.name}</TableCell>
|
||||||
|
<TableCell>{ingress.namespace}</TableCell>
|
||||||
|
<TableCell>{ingress.class || "-"}</TableCell>
|
||||||
|
<TableCell>{ingress.host}</TableCell>
|
||||||
|
<TableCell>{ingress.addresses.join(", ")}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{ingress.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/Kubernetes/JobList.tsx
Normal file
52
src/components/Kubernetes/JobList.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { JobInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface JobListProps {
|
||||||
|
jobs: JobInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobList({ jobs, _clusterId, _namespace }: JobListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Completions</TableHead>
|
||||||
|
<TableHead>Duration</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead>Labels</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{jobs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||||
|
No jobs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
jobs.map((job) => (
|
||||||
|
<TableRow key={`${job.name}-${job.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{job.name}</TableCell>
|
||||||
|
<TableCell>{job.namespace}</TableCell>
|
||||||
|
<TableCell>{job.completions}</TableCell>
|
||||||
|
<TableCell>{job.duration}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{job.age}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{Object.entries(job.labels)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(", ")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/components/Kubernetes/LoadingSpinner.tsx
Normal file
22
src/components/Kubernetes/LoadingSpinner.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface LoadingSpinnerProps {
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingSpinner({ size = "md", text }: LoadingSpinnerProps) {
|
||||||
|
const sizes = {
|
||||||
|
sm: "w-4 h-4",
|
||||||
|
md: "w-8 h-8",
|
||||||
|
lg: "w-12 h-12",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3">
|
||||||
|
<Loader2 className={`${sizes[size]} animate-spin text-primary`} />
|
||||||
|
{text && <p className="text-sm text-muted-foreground">{text}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/Kubernetes/MetricsChart.tsx
Normal file
54
src/components/Kubernetes/MetricsChart.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
|
||||||
|
|
||||||
|
interface MetricsChartProps {
|
||||||
|
title: string;
|
||||||
|
data: { labels: string[]; datasets: { label: string; data: number[]; borderColor?: string; backgroundColor?: string }[] };
|
||||||
|
type?: "line" | "bar";
|
||||||
|
timeRange?: string;
|
||||||
|
onTimeRangeChange?: (range: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricsChart({ title, data, timeRange = "5m", onTimeRangeChange }: MetricsChartProps) {
|
||||||
|
const timeRanges = ["5m", "15m", "1h", "6h", "1d", "7d"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center gap-2">{title}</CardTitle>
|
||||||
|
{onTimeRangeChange && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Time Range:</span>
|
||||||
|
<Select value={timeRange} onValueChange={onTimeRangeChange}>
|
||||||
|
<SelectTrigger className="w-[120px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{timeRanges.map((range) => (
|
||||||
|
<SelectItem key={range} value={range}>
|
||||||
|
{range}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 min-h-[300px] flex items-center justify-center">
|
||||||
|
{data.datasets.length > 0 ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">Chart visualization would be displayed here</p>
|
||||||
|
<p className="text-xs mt-2">Charts require react-chartjs-2 and chart.js dependencies</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
No metrics data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
233
src/components/Kubernetes/NodeList.tsx
Normal file
233
src/components/Kubernetes/NodeList.tsx
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui";
|
||||||
|
import { AlertCircle, Terminal } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui";
|
||||||
|
import type { NodeInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface NodeListProps {
|
||||||
|
nodes: NodeInfo[];
|
||||||
|
clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeList({ nodes, clusterId }: NodeListProps) {
|
||||||
|
const [selectedNode, setSelectedNode] = useState<NodeInfo | null>(null);
|
||||||
|
const [isCordoning, setIsCordoning] = useState(false);
|
||||||
|
const [isUncordoning, setIsUncordoning] = useState(false);
|
||||||
|
const [isDraining, setIsDraining] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getNodeStatusColor = (status: string) => {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case "ready":
|
||||||
|
return "bg-green-500";
|
||||||
|
case "notready":
|
||||||
|
return "bg-red-500";
|
||||||
|
case "schedulingdisabled":
|
||||||
|
return "bg-yellow-500";
|
||||||
|
default:
|
||||||
|
return "bg-gray-500";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCordon = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
setIsCordoning(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invoke<void>("cordon_node", { clusterId, nodeName: selectedNode.name });
|
||||||
|
setSelectedNode(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to cordon node");
|
||||||
|
} finally {
|
||||||
|
setIsCordoning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUncordon = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
setIsUncordoning(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invoke<void>("uncordon_node", { clusterId, nodeName: selectedNode.name });
|
||||||
|
setSelectedNode(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to uncordon node");
|
||||||
|
} finally {
|
||||||
|
setIsUncordoning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrain = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
setIsDraining(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invoke<void>("drain_node", { clusterId, nodeName: selectedNode.name });
|
||||||
|
setSelectedNode(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to drain node");
|
||||||
|
} finally {
|
||||||
|
setIsDraining(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Roles</TableHead>
|
||||||
|
<TableHead>Version</TableHead>
|
||||||
|
<TableHead>Internal IP</TableHead>
|
||||||
|
<TableHead>OS Image</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{nodes.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center text-muted-foreground">
|
||||||
|
No nodes found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
nodes.map((node) => (
|
||||||
|
<TableRow key={node.name}>
|
||||||
|
<TableCell className="font-medium">{node.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={`${getNodeStatusColor(node.status)} text-white`}>
|
||||||
|
{node.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{node.roles}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{node.version}</TableCell>
|
||||||
|
<TableCell className="text-sm font-mono">{node.internal_ip}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{node.os_image}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{node.age}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedNode(node)}
|
||||||
|
className="text-primary hover:text-primary hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Node Management Dialog */}
|
||||||
|
{selectedNode && (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setSelectedNode(null);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Terminal className="w-5 h-5" />
|
||||||
|
Manage Node: {selectedNode.name}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
{/* Node Details */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 p-4 bg-muted rounded-lg">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Status</p>
|
||||||
|
<p className="font-semibold">{selectedNode.status}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Roles</p>
|
||||||
|
<p className="font-semibold">{selectedNode.roles}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Version</p>
|
||||||
|
<p className="font-semibold">{selectedNode.version}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">OS Image</p>
|
||||||
|
<p className="font-semibold">{selectedNode.os_image}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Kernel</p>
|
||||||
|
<p className="font-semibold">{selectedNode.kernel_version}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Kubelet</p>
|
||||||
|
<p className="font-semibold">{selectedNode.kubelet_version}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Internal IP</p>
|
||||||
|
<p className="font-semibold font-mono">{selectedNode.internal_ip}</p>
|
||||||
|
</div>
|
||||||
|
{selectedNode.external_ip && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">External IP</p>
|
||||||
|
<p className="font-semibold font-mono">{selectedNode.external_ip}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedNode.roles.toLowerCase().includes("schedulingdisabled") ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleUncordon}
|
||||||
|
disabled={isUncordoning}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isUncordoning ? "Uncordoning..." : "Uncordon Node"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleCordon}
|
||||||
|
variant="outline"
|
||||||
|
disabled={isCordoning}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isCordoning ? "Cordoning..." : "Cordon Node"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleDrain}
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isDraining}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isDraining ? "Draining..." : "Drain Node"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/components/Kubernetes/PVCList.tsx
Normal file
50
src/components/Kubernetes/PVCList.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { PersistentVolumeClaimInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface PVCListProps {
|
||||||
|
pvcs: PersistentVolumeClaimInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PVCList({ pvcs, _clusterId, _namespace }: PVCListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Volume</TableHead>
|
||||||
|
<TableHead>Capacity</TableHead>
|
||||||
|
<TableHead>Access Modes</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{pvcs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||||
|
No PVCs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
pvcs.map((pvc) => (
|
||||||
|
<TableRow key={`${pvc.name}-${pvc.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{pvc.name}</TableCell>
|
||||||
|
<TableCell>{pvc.namespace}</TableCell>
|
||||||
|
<TableCell>{pvc.status}</TableCell>
|
||||||
|
<TableCell>{pvc.volume}</TableCell>
|
||||||
|
<TableCell>{pvc.capacity}</TableCell>
|
||||||
|
<TableCell>{pvc.access_modes.join(", ")}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{pvc.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/components/Kubernetes/PVList.tsx
Normal file
49
src/components/Kubernetes/PVList.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { PersistentVolumeInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface PVListProps {
|
||||||
|
pvs: PersistentVolumeInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PVList({ pvs, _clusterId }: PVListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Capacity</TableHead>
|
||||||
|
<TableHead>Access Modes</TableHead>
|
||||||
|
<TableHead>Reclaim Policy</TableHead>
|
||||||
|
<TableHead>Storage Class</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{pvs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||||
|
No PVs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
pvs.map((pv) => (
|
||||||
|
<TableRow key={pv.name}>
|
||||||
|
<TableCell className="font-medium">{pv.name}</TableCell>
|
||||||
|
<TableCell>{pv.status}</TableCell>
|
||||||
|
<TableCell>{pv.capacity}</TableCell>
|
||||||
|
<TableCell>{pv.access_modes.join(", ")}</TableCell>
|
||||||
|
<TableCell>{pv.reclaim_policy}</TableCell>
|
||||||
|
<TableCell>{pv.storage_class}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{pv.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
src/components/Kubernetes/PodDetail.tsx
Normal file
187
src/components/Kubernetes/PodDetail.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Copy, Terminal, X } from "lucide-react";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface PodDetailProps {
|
||||||
|
podName: string;
|
||||||
|
namespace: string;
|
||||||
|
_clusterId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PodDetail({ podName, namespace, _clusterId, onClose }: PodDetailProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("overview");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">Pod: {podName}</h2>
|
||||||
|
<Badge variant="outline">{namespace}</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
<TabsTrigger value="events">Events</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="overview" className="h-full overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pod Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<span className="font-mono">{podName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Namespace</span>
|
||||||
|
<span className="font-mono">{namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Status</span>
|
||||||
|
<Badge variant="default">Running</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">IP</span>
|
||||||
|
<span className="font-mono">10.0.0.1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Node</span>
|
||||||
|
<span className="font-mono">node-1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Restart Count</span>
|
||||||
|
<span>0</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Created</span>
|
||||||
|
<span className="text-sm">2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Containers</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Image</TableHead>
|
||||||
|
<TableHead>State</TableHead>
|
||||||
|
<TableHead>Ready</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>example</TableCell>
|
||||||
|
<TableCell className="font-mono">nginx:latest</TableCell>
|
||||||
|
<TableCell>Running</TableCell>
|
||||||
|
<TableCell>True</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labels</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
<Badge variant="secondary">version=v1</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="logs" className="h-full">
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Container Logs</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Terminal className="w-4 h-4 mr-2" />
|
||||||
|
Execute
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 bg-slate-900 rounded-md p-4 overflow-auto font-mono text-sm">
|
||||||
|
<div className="text-green-400">[INFO] Starting nginx server...</div>
|
||||||
|
<div className="text-green-400">[INFO] Listening on port 80</div>
|
||||||
|
<div className="text-blue-400">[ACCESS] GET / - 200 OK</div>
|
||||||
|
<div className="text-blue-400">[ACCESS] GET /css/style.css - 200 OK</div>
|
||||||
|
<div className="text-blue-400">[ACCESS] GET /js/app.js - 200 OK</div>
|
||||||
|
<div className="text-yellow-400">[WARN] Slow response time detected</div>
|
||||||
|
<div className="text-blue-400">[ACCESS] POST /api/data - 201 Created</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="events" className="h-full overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Time</TableHead>
|
||||||
|
<TableHead>Reason</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Message</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>2 hours ago</TableCell>
|
||||||
|
<TableCell>Pulled</TableCell>
|
||||||
|
<TableCell>Normal</TableCell>
|
||||||
|
<TableCell>Container image "nginx:latest" already present on machine</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>2 hours ago</TableCell>
|
||||||
|
<TableCell>Created</TableCell>
|
||||||
|
<TableCell>Normal</TableCell>
|
||||||
|
<TableCell>Created container example</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>2 hours ago</TableCell>
|
||||||
|
<TableCell>Started</TableCell>
|
||||||
|
<TableCell>Normal</TableCell>
|
||||||
|
<TableCell>Started container example</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/components/Kubernetes/RbacEditor.tsx
Normal file
116
src/components/Kubernetes/RbacEditor.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Plus, X, Check } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui";
|
||||||
|
|
||||||
|
interface RbacEditorProps {
|
||||||
|
_clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RbacEditor({ _clusterId, namespace, onClose }: RbacEditorProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("roles");
|
||||||
|
const [newRoleName, setNewRoleName] = React.useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-semibold">RBAC Editor</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4 mr-2" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="roles">Roles</TabsTrigger>
|
||||||
|
<TabsTrigger value="clusterroles">ClusterRoles</TabsTrigger>
|
||||||
|
<TabsTrigger value="rolebindings">RoleBindings</TabsTrigger>
|
||||||
|
<TabsTrigger value="clusterrolebindings">ClusterRoleBindings</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="roles" className="h-full flex flex-col">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="New role name"
|
||||||
|
value={newRoleName}
|
||||||
|
onChange={(e) => setNewRoleName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button disabled={!newRoleName}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Create Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<div className="bg-card rounded-lg border flex flex-col h-full">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold">Role YAML Editor</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 bg-slate-900 p-4 font-mono text-sm text-green-400 overflow-auto">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">apiVersion:</span> rbac.authorization.k8s.io/v1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">kind:</span> Role
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">metadata:</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-4">
|
||||||
|
<span className="text-blue-400">name:</span> {newRoleName || "role-name"}
|
||||||
|
</div>
|
||||||
|
<div className="pl-4">
|
||||||
|
<span className="text-blue-400">namespace:</span> {namespace}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">rules:</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-4">
|
||||||
|
<span className="text-blue-400">-</span> <span className="text-blue-400">apiGroups:</span> [""]
|
||||||
|
</div>
|
||||||
|
<div className="pl-6">
|
||||||
|
<span className="text-blue-400">resources:</span> ["pods"]
|
||||||
|
</div>
|
||||||
|
<div className="pl-6">
|
||||||
|
<span className="text-blue-400">verbs:</span> ["get", "list", "watch"]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="clusterroles" className="h-full flex flex-col">
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<p>ClusterRole editing would be displayed here</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="rolebindings" className="h-full flex flex-col">
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<p>RoleBinding editing would be displayed here</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="clusterrolebindings" className="h-full flex flex-col">
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<p>ClusterRoleBinding editing would be displayed here</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
196
src/components/Kubernetes/RbacViewer.tsx
Normal file
196
src/components/Kubernetes/RbacViewer.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Plus, Shield, User } from "lucide-react";
|
||||||
|
|
||||||
|
interface RbacViewerProps {
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RbacViewer({ clusterId, namespace }: RbacViewerProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold">RBAC Management</h2>
|
||||||
|
<p className="text-muted-foreground">Cluster ID: {clusterId} | Namespace: {namespace}</p>
|
||||||
|
</div>
|
||||||
|
<Button>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Create Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Shield className="w-5 h-5" />
|
||||||
|
Roles
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Rules</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>pod-reader</TableCell>
|
||||||
|
<TableCell className="font-mono">{namespace}</TableCell>
|
||||||
|
<TableCell>get, list, watch pods</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>secret-viewer</TableCell>
|
||||||
|
<TableCell className="font-mono">{namespace}</TableCell>
|
||||||
|
<TableCell>get, list secrets</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>deployment-manager</TableCell>
|
||||||
|
<TableCell className="font-mono">{namespace}</TableCell>
|
||||||
|
<TableCell>get, list, create, update deployments</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Shield className="w-5 h-5" />
|
||||||
|
ClusterRoles
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Rules</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>admin</TableCell>
|
||||||
|
<TableCell>Full access to all resources</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>edit</TableCell>
|
||||||
|
<TableCell>Modify resources in namespace</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>view</TableCell>
|
||||||
|
<TableCell>Read-only access to resources</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
RoleBindings
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Role</TableHead>
|
||||||
|
<TableHead>Subjects</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>pod-reader-binding</TableCell>
|
||||||
|
<TableCell>pod-reader</TableCell>
|
||||||
|
<TableCell>user:alice</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>deployment-manager-binding</TableCell>
|
||||||
|
<TableCell>deployment-manager</TableCell>
|
||||||
|
<TableCell>group:devs</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg border">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
ClusterRoleBindings
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>ClusterRole</TableHead>
|
||||||
|
<TableHead>Subjects</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>admin-binding</TableCell>
|
||||||
|
<TableCell>admin</TableCell>
|
||||||
|
<TableCell>group:admins</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>view-binding</TableCell>
|
||||||
|
<TableCell>view</TableCell>
|
||||||
|
<TableCell>group:auditors</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm">Edit</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/Kubernetes/ReplicaSetList.tsx
Normal file
52
src/components/Kubernetes/ReplicaSetList.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { ReplicaSetInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface ReplicaSetListProps {
|
||||||
|
replicaSets: ReplicaSetInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReplicaSetList({ replicaSets, _clusterId, _namespace }: ReplicaSetListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Replicas</TableHead>
|
||||||
|
<TableHead>Ready</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead>Labels</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{replicaSets.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||||
|
No replica sets found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
replicaSets.map((replicaSet) => (
|
||||||
|
<TableRow key={`${replicaSet.name}-${replicaSet.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{replicaSet.name}</TableCell>
|
||||||
|
<TableCell>{replicaSet.namespace}</TableCell>
|
||||||
|
<TableCell>{replicaSet.replicas}</TableCell>
|
||||||
|
<TableCell>{replicaSet.ready}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{replicaSet.age}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{Object.entries(replicaSet.labels)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(", ")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/components/Kubernetes/RoleBindingList.tsx
Normal file
44
src/components/Kubernetes/RoleBindingList.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { RoleBindingInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface RoleBindingListProps {
|
||||||
|
roleBindings: RoleBindingInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleBindingList({ roleBindings, _clusterId, _namespace }: RoleBindingListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Role</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{roleBindings.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||||
|
No role bindings found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
roleBindings.map((rb) => (
|
||||||
|
<TableRow key={`${rb.name}-${rb.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{rb.name}</TableCell>
|
||||||
|
<TableCell>{rb.namespace}</TableCell>
|
||||||
|
<TableCell>{rb.role}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{rb.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/Kubernetes/RoleList.tsx
Normal file
42
src/components/Kubernetes/RoleList.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { RoleInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface RoleListProps {
|
||||||
|
roles: RoleInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleList({ roles, _clusterId, _namespace }: RoleListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{roles.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="text-center text-muted-foreground">
|
||||||
|
No roles found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
roles.map((role) => (
|
||||||
|
<TableRow key={`${role.name}-${role.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{role.name}</TableCell>
|
||||||
|
<TableCell>{role.namespace}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{role.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/Kubernetes/SearchBar.tsx
Normal file
41
src/components/Kubernetes/SearchBar.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
query: string;
|
||||||
|
onQueryChange: (query: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
showClear?: boolean;
|
||||||
|
onClear?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchBar({ query, onQueryChange, placeholder = "Search...", showClear = true, onClear }: SearchBarProps) {
|
||||||
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onQueryChange("");
|
||||||
|
onClear?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 px-3 py-2 rounded-md border transition-colors ${isFocused ? "border-primary ring-1 ring-primary" : "border-input"}`}>
|
||||||
|
<Search className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => onQueryChange(e.target.value)}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="border-none shadow-none focus-visible:ring-0 py-0 px-2 flex-1"
|
||||||
|
/>
|
||||||
|
{showClear && query && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleClear} className="h-6 w-6 p-0">
|
||||||
|
<Search className="w-3 h-3 rotate-45" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
src/components/Kubernetes/SecretDetail.tsx
Normal file
122
src/components/Kubernetes/SecretDetail.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface SecretDetailProps {
|
||||||
|
secretName: string;
|
||||||
|
namespace: string;
|
||||||
|
_clusterId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SecretDetail({ secretName, namespace, _clusterId, onClose }: SecretDetailProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("data");
|
||||||
|
const [showValues, setShowValues] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">Secret: {secretName}</h2>
|
||||||
|
<Badge variant="destructive">Secret</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-3 mb-4">
|
||||||
|
<TabsTrigger value="data">Data</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
<TabsTrigger value="metadata">Metadata</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="data" className="h-full overflow-y-auto">
|
||||||
|
<Card className="h-full flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>Secret Data</CardTitle>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setShowValues(!showValues)}>
|
||||||
|
{showValues ? "Hide Values" : "Show Values"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 bg-slate-900 rounded-md p-4 overflow-auto font-mono text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">username:</span>
|
||||||
|
<span className="text-green-400 ml-2">
|
||||||
|
{showValues ? "admin" : "****"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">password:</span>
|
||||||
|
<span className="text-green-400 ml-2">
|
||||||
|
{showValues ? "secret123" : "****"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-blue-400">api-key:</span>
|
||||||
|
<span className="text-green-400 ml-2">
|
||||||
|
{showValues ? "sk-abc123xyz" : "****"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="metadata" className="h-full overflow-y-auto">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Metadata</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<span className="font-mono">{secretName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Namespace</span>
|
||||||
|
<span className="font-mono">{namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Type</span>
|
||||||
|
<Badge variant="secondary">Opaque</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Created</span>
|
||||||
|
<span className="text-sm">2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labels</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/components/Kubernetes/SecretList.tsx
Normal file
50
src/components/Kubernetes/SecretList.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { SecretInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface SecretListProps {
|
||||||
|
secrets: SecretInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SecretList({ secrets, _clusterId, _namespace }: SecretListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Data Keys</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{secrets.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||||
|
No secrets found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
secrets.map((secret) => (
|
||||||
|
<TableRow key={`${secret.name}-${secret.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{secret.name}</TableCell>
|
||||||
|
<TableCell>{secret.namespace}</TableCell>
|
||||||
|
<TableCell>{secret.type}</TableCell>
|
||||||
|
<TableCell>{secret.data_keys}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{secret.age}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<span className="text-sm">View/Edit</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/components/Kubernetes/ServiceAccountList.tsx
Normal file
44
src/components/Kubernetes/ServiceAccountList.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import type { ServiceAccountInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
interface ServiceAccountListProps {
|
||||||
|
serviceAccounts: ServiceAccountInfo[];
|
||||||
|
_clusterId: string;
|
||||||
|
_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServiceAccountList({ serviceAccounts, _clusterId, _namespace }: ServiceAccountListProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Namespace</TableHead>
|
||||||
|
<TableHead>Secrets</TableHead>
|
||||||
|
<TableHead>Age</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{serviceAccounts.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||||
|
No service accounts found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
serviceAccounts.map((sa) => (
|
||||||
|
<TableRow key={`${sa.name}-${sa.namespace}`}>
|
||||||
|
<TableCell className="font-medium">{sa.name}</TableCell>
|
||||||
|
<TableCell>{sa.namespace}</TableCell>
|
||||||
|
<TableCell>{sa.secrets}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{sa.age}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/components/Kubernetes/ServiceDetail.tsx
Normal file
157
src/components/Kubernetes/ServiceDetail.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { YamlEditor } from "./YamlEditor";
|
||||||
|
|
||||||
|
interface ServiceDetailProps {
|
||||||
|
serviceName: string;
|
||||||
|
namespace: string;
|
||||||
|
_clusterId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServiceDetail({ serviceName, namespace, _clusterId, onClose }: ServiceDetailProps) {
|
||||||
|
const [activeTab, setActiveTab] = React.useState("overview");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">Service: {serviceName}</h2>
|
||||||
|
<Badge variant="outline">{namespace}</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="endpoints">Endpoints</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
<TabsTrigger value="events">Events</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<TabsContent value="overview" className="h-full overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Service Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Name</span>
|
||||||
|
<span className="font-mono">{serviceName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Namespace</span>
|
||||||
|
<span className="font-mono">{namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Type</span>
|
||||||
|
<Badge variant="secondary">ClusterIP</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Cluster IP</span>
|
||||||
|
<span className="font-mono">10.96.0.1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">External IP</span>
|
||||||
|
<span className="text-muted-foreground">none</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Port</span>
|
||||||
|
<span>80/TCP</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Selector</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labels</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary">app=web</Badge>
|
||||||
|
<Badge variant="secondary">tier=frontend</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="endpoints" className="h-full overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>IP</TableHead>
|
||||||
|
<TableHead>Port</TableHead>
|
||||||
|
<TableHead>Node</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>10.0.0.1</TableCell>
|
||||||
|
<TableCell>80</TableCell>
|
||||||
|
<TableCell>node-1</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>10.0.0.2</TableCell>
|
||||||
|
<TableCell>80</TableCell>
|
||||||
|
<TableCell>node-2</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>10.0.0.3</TableCell>
|
||||||
|
<TableCell>80</TableCell>
|
||||||
|
<TableCell>node-3</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="yaml" className="h-full">
|
||||||
|
<YamlEditor onChange={() => {}} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="events" className="h-full overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Time</TableHead>
|
||||||
|
<TableHead>Reason</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Message</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>2 hours ago</TableCell>
|
||||||
|
<TableCell>SettingClusterIP</TableCell>
|
||||||
|
<TableCell>Normal</TableCell>
|
||||||
|
<TableCell>Assigned cluster IP 10.96.0.1</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/components/Kubernetes/Terminal.tsx
Normal file
150
src/components/Kubernetes/Terminal.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Terminal as TerminalIcon, X, Plus } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
|
||||||
|
|
||||||
|
interface TerminalSession {
|
||||||
|
id: string;
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
pod: string;
|
||||||
|
container: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TerminalProps {
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Terminal({ clusterId, namespace }: TerminalProps) {
|
||||||
|
const [sessions, setSessions] = React.useState<TerminalSession[]>([]);
|
||||||
|
const [activeSessionId, setActiveSessionId] = React.useState<string | null>(null);
|
||||||
|
const [isCreating, setIsCreating] = React.useState(false);
|
||||||
|
|
||||||
|
const terminalRefs = React.useRef<Record<string, { destroy: () => void }>>({});
|
||||||
|
const containerRefs = React.useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
|
||||||
|
const addSession = React.useCallback(() => {
|
||||||
|
setIsCreating(true);
|
||||||
|
const newSession: TerminalSession = {
|
||||||
|
id: `session-${Date.now()}`,
|
||||||
|
clusterId,
|
||||||
|
namespace: namespace === "all" ? "default" : namespace,
|
||||||
|
pod: "",
|
||||||
|
container: "",
|
||||||
|
command: "bash",
|
||||||
|
};
|
||||||
|
setSessions((prev) => [...prev, newSession]);
|
||||||
|
setActiveSessionId(newSession.id);
|
||||||
|
setIsCreating(false);
|
||||||
|
}, [clusterId, namespace]);
|
||||||
|
|
||||||
|
const removeSession = (sessionId: string) => {
|
||||||
|
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
||||||
|
if (activeSessionId === sessionId) {
|
||||||
|
setActiveSessionId(null);
|
||||||
|
}
|
||||||
|
if (terminalRefs.current[sessionId]) {
|
||||||
|
terminalRefs.current[sessionId].destroy();
|
||||||
|
delete terminalRefs.current[sessionId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeTerminal = (sessionId: string) => {
|
||||||
|
const terminal = terminalRefs.current[sessionId];
|
||||||
|
const container = containerRefs.current[sessionId];
|
||||||
|
if (terminal && container) {
|
||||||
|
// Placeholder for resize logic
|
||||||
|
// Requires xterm-addon-fit dependency
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Initialize with a default session
|
||||||
|
if (sessions.length === 0 && !isCreating) {
|
||||||
|
addSession();
|
||||||
|
}
|
||||||
|
}, [sessions.length, isCreating, addSession]);
|
||||||
|
|
||||||
|
const initTerminal = (sessionId: string, element: HTMLDivElement | null) => {
|
||||||
|
if (!element || terminalRefs.current[sessionId]) return;
|
||||||
|
|
||||||
|
// Placeholder for terminal initialization
|
||||||
|
// Requires xterm, xterm-addon-fit, xterm-addon-web-links dependencies
|
||||||
|
const terminal = { destroy: () => {} };
|
||||||
|
terminalRefs.current[sessionId] = terminal;
|
||||||
|
containerRefs.current[sessionId] = element;
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
window.addEventListener("resize", () => resizeTerminal(sessionId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-hidden flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TerminalIcon className="w-5 h-5" />
|
||||||
|
<h2 className="text-xl font-semibold">Terminal</h2>
|
||||||
|
</div>
|
||||||
|
<Button onClick={addSession} disabled={isCreating}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New Terminal
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sessions.length === 0 ? (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<TerminalIcon className="w-16 h-16 mx-auto text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground">No terminals open</p>
|
||||||
|
<Button onClick={addSession}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Open Terminal
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<Tabs value={activeSessionId || sessions[0]?.id} onValueChange={setActiveSessionId}>
|
||||||
|
<TabsList className="grid grid-cols-10 mb-2">
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={session.id}
|
||||||
|
value={session.id}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="truncate max-w-[100px]">
|
||||||
|
{session.pod || "new"} / {session.container || "bash"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeSession(session.id);
|
||||||
|
}}
|
||||||
|
className="hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<TabsContent
|
||||||
|
key={session.id}
|
||||||
|
value={session.id}
|
||||||
|
className="flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={(el) => initTerminal(session.id, el)}
|
||||||
|
className="w-full h-full bg-slate-900 rounded-md overflow-hidden"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/Kubernetes/Toast.tsx
Normal file
66
src/components/Kubernetes/Toast.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { X, AlertCircle, CheckCircle, Info, AlertTriangle } from "lucide-react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
message: string;
|
||||||
|
type?: "success" | "error" | "info" | "warning";
|
||||||
|
duration?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toast({ message, type = "info", duration = 5000, onClose }: ToastProps) {
|
||||||
|
const [visible, setVisible] = React.useState(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (duration > 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
setTimeout(onClose, 300);
|
||||||
|
}, duration);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [duration, onClose]);
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: <CheckCircle className="w-5 h-5 text-green-500" />,
|
||||||
|
error: <AlertCircle className="w-5 h-5 text-red-500" />,
|
||||||
|
info: <Info className="w-5 h-5 text-blue-500" />,
|
||||||
|
warning: <AlertTriangle className="w-5 h-5 text-yellow-500" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgColors = {
|
||||||
|
success: "bg-green-50 border-green-200",
|
||||||
|
error: "bg-red-50 border-red-200",
|
||||||
|
info: "bg-blue-50 border-blue-200",
|
||||||
|
warning: "bg-yellow-50 border-yellow-200",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed top-4 right-4 z-50 w-full max-w-sm transition-all duration-300 transform ${
|
||||||
|
visible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Card className={`border-l-4 ${bgColors[type]} shadow-lg`}>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icons[type]}
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setVisible(false)}>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">{message}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/components/Kubernetes/YamlEditor.tsx
Normal file
35
src/components/Kubernetes/YamlEditor.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import { Badge } from "@/components/ui";
|
||||||
|
|
||||||
|
interface YamlEditorProps {
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function YamlEditor({ onChange }: YamlEditorProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">YAML Editor</h2>
|
||||||
|
<Badge variant="default" className="bg-green-600">Ready</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => onChange("")}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-primary">
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 rounded-md border overflow-hidden flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">YAML Editor would be displayed here</p>
|
||||||
|
<p className="text-xs mt-2">Requires @monaco-editor/react dependency</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,3 +8,40 @@ export { ServiceList } from "./ServiceList";
|
|||||||
export { DeploymentList } from "./DeploymentList";
|
export { DeploymentList } from "./DeploymentList";
|
||||||
export { StatefulSetList } from "./StatefulSetList";
|
export { StatefulSetList } from "./StatefulSetList";
|
||||||
export { DaemonSetList } from "./DaemonSetList";
|
export { DaemonSetList } from "./DaemonSetList";
|
||||||
|
export { NodeList } from "./NodeList";
|
||||||
|
export { EventList } from "./EventList";
|
||||||
|
export { ConfigMapList } from "./ConfigMapList";
|
||||||
|
export { SecretList } from "./SecretList";
|
||||||
|
export { ReplicaSetList } from "./ReplicaSetList";
|
||||||
|
export { JobList } from "./JobList";
|
||||||
|
export { CronJobList } from "./CronJobList";
|
||||||
|
export { IngressList } from "./IngressList";
|
||||||
|
export { PVCList } from "./PVCList";
|
||||||
|
export { PVList } from "./PVList";
|
||||||
|
export { ServiceAccountList } from "./ServiceAccountList";
|
||||||
|
export { RoleList } from "./RoleList";
|
||||||
|
export { ClusterRoleList } from "./ClusterRoleList";
|
||||||
|
export { RoleBindingList } from "./RoleBindingList";
|
||||||
|
export { ClusterRoleBindingList } from "./ClusterRoleBindingList";
|
||||||
|
export { HPAList } from "./HPAList";
|
||||||
|
export { Terminal } from "./Terminal";
|
||||||
|
export { YamlEditor } from "./YamlEditor";
|
||||||
|
export { MetricsChart } from "./MetricsChart";
|
||||||
|
export { SearchBar } from "./SearchBar";
|
||||||
|
export { ContextSwitcher } from "./ContextSwitcher";
|
||||||
|
export { ApplicationView } from "./ApplicationView";
|
||||||
|
export { PodDetail } from "./PodDetail";
|
||||||
|
export { DeploymentDetail } from "./DeploymentDetail";
|
||||||
|
export { ServiceDetail } from "./ServiceDetail";
|
||||||
|
export { ConfigMapDetail } from "./ConfigMapDetail";
|
||||||
|
export { SecretDetail } from "./SecretDetail";
|
||||||
|
export { ClusterOverview } from "./ClusterOverview";
|
||||||
|
export { ClusterDetails } from "./ClusterDetails";
|
||||||
|
export { Hotbar } from "./Hotbar";
|
||||||
|
export { CommandPalette } from "./CommandPalette";
|
||||||
|
export { Toast } from "./Toast";
|
||||||
|
export { LoadingSpinner } from "./LoadingSpinner";
|
||||||
|
export { CreateResourceModal } from "./CreateResourceModal";
|
||||||
|
export { EditResourceModal } from "./EditResourceModal";
|
||||||
|
export { RbacViewer } from "./RbacViewer";
|
||||||
|
export { RbacEditor } from "./RbacEditor";
|
||||||
|
|||||||
116
src/lib/eventBus.ts
Normal file
116
src/lib/eventBus.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export type EventCallback<T = any> = (data: T) => void;
|
||||||
|
|
||||||
|
export interface EventUnsubscribe {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventBus {
|
||||||
|
on<T = any>(event: string, callback: EventCallback<T>): EventUnsubscribe;
|
||||||
|
off(event: string, callback: EventCallback): void;
|
||||||
|
emit<T = any>(event: string, data?: T): void;
|
||||||
|
once<T = any>(event: string, callback: EventCallback<T>): EventUnsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleEventBus implements EventBus {
|
||||||
|
private events: Record<string, Set<EventCallback>> = {};
|
||||||
|
private onceEvents: Record<string, Set<EventCallback>> = {};
|
||||||
|
|
||||||
|
on<T = any>(event: string, callback: EventCallback<T>): EventUnsubscribe {
|
||||||
|
if (!this.events[event]) {
|
||||||
|
this.events[event] = new Set();
|
||||||
|
}
|
||||||
|
this.events[event].add(callback);
|
||||||
|
|
||||||
|
return () => this.off(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, callback: EventCallback): void {
|
||||||
|
if (this.events[event]) {
|
||||||
|
this.events[event].delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T = any>(event: string, data?: T): void {
|
||||||
|
const callbacks = this.events[event];
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach((callback) => callback(data as T));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onceCallbacks = this.onceEvents[event];
|
||||||
|
if (onceCallbacks) {
|
||||||
|
onceCallbacks.forEach((callback) => callback(data as T));
|
||||||
|
delete this.onceEvents[event];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
once<T = any>(event: string, callback: EventCallback<T>): EventUnsubscribe {
|
||||||
|
if (!this.onceEvents[event]) {
|
||||||
|
this.onceEvents[event] = new Set();
|
||||||
|
}
|
||||||
|
this.onceEvents[event].add(callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (this.onceEvents[event]) {
|
||||||
|
this.onceEvents[event].delete(callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventBus: EventBus = new SimpleEventBus();
|
||||||
|
|
||||||
|
export async function subscribeToK8sEvents(
|
||||||
|
clusterId: string,
|
||||||
|
namespace: string,
|
||||||
|
resourceType: string,
|
||||||
|
callback: EventCallback<any>
|
||||||
|
): Promise<EventUnsubscribe> {
|
||||||
|
try {
|
||||||
|
const unsubscribeId = await invoke<string>("subscribe_to_k8s_events", {
|
||||||
|
clusterId,
|
||||||
|
namespace,
|
||||||
|
resourceType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = (data: any) => {
|
||||||
|
callback(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventBus.on(`k8s:${clusterId}:${namespace}:${resourceType}`, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventBus.off(`k8s:${clusterId}:${namespace}:${resourceType}`, handler);
|
||||||
|
invoke<void>("unsubscribe_from_k8s_events", { unsubscribeId });
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to subscribe to K8s events:", error);
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subscribeToAllEvents(
|
||||||
|
clusterId: string,
|
||||||
|
callback: EventCallback<any>
|
||||||
|
): Promise<EventUnsubscribe> {
|
||||||
|
try {
|
||||||
|
const unsubscribeId = await invoke<string>("subscribe_to_all_k8s_events", {
|
||||||
|
clusterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = (data: any) => {
|
||||||
|
callback(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventBus.on(`k8s:${clusterId}:all`, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventBus.off(`k8s:${clusterId}:all`, handler);
|
||||||
|
invoke<void>("unsubscribe_from_k8s_events", { unsubscribeId });
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to subscribe to all K8s events:", error);
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -748,6 +748,18 @@ export interface ClusterInfo {
|
|||||||
cluster_url: string;
|
cluster_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContextInfo {
|
||||||
|
name: string;
|
||||||
|
cluster: string;
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceInfo {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PortForwardRequest {
|
export interface PortForwardRequest {
|
||||||
cluster_id: string;
|
cluster_id: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useKubernetesStore } from "@/stores/kubernetesStore";
|
||||||
import { ClusterList } from "@/components/Kubernetes/ClusterList";
|
import { ClusterList } from "@/components/Kubernetes/ClusterList";
|
||||||
import { PortForwardList } from "@/components/Kubernetes/PortForwardList";
|
import { PortForwardList } from "@/components/Kubernetes/PortForwardList";
|
||||||
import { AddClusterModal } from "@/components/Kubernetes/AddClusterModal";
|
import { AddClusterModal } from "@/components/Kubernetes/AddClusterModal";
|
||||||
import { PortForwardForm } from "@/components/Kubernetes/PortForwardForm";
|
import { PortForwardForm } from "@/components/Kubernetes/PortForwardForm";
|
||||||
|
import { ResourceBrowser } from "@/components/Kubernetes/ResourceBrowser";
|
||||||
import type { ClusterInfo, PortForwardResponse } from "@/lib/tauriCommands";
|
import type { ClusterInfo, PortForwardResponse } from "@/lib/tauriCommands";
|
||||||
import {
|
import {
|
||||||
listClustersCmd,
|
listClustersCmd,
|
||||||
@ -13,7 +15,7 @@ import {
|
|||||||
} from "@/lib/tauriCommands";
|
} from "@/lib/tauriCommands";
|
||||||
|
|
||||||
export function KubernetesPage() {
|
export function KubernetesPage() {
|
||||||
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
|
const { clusters, addCluster, removeCluster, selectedClusterId } = useKubernetesStore();
|
||||||
const [portForwards, setPortForwards] = useState<PortForwardResponse[]>([]);
|
const [portForwards, setPortForwards] = useState<PortForwardResponse[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isAddClusterOpen, setIsAddClusterOpen] = useState(false);
|
const [isAddClusterOpen, setIsAddClusterOpen] = useState(false);
|
||||||
@ -30,7 +32,8 @@ export function KubernetesPage() {
|
|||||||
listClustersCmd(),
|
listClustersCmd(),
|
||||||
listPortForwardsCmd(),
|
listPortForwardsCmd(),
|
||||||
]);
|
]);
|
||||||
setClusters(clustersData);
|
|
||||||
|
clustersData.forEach(addCluster);
|
||||||
setPortForwards(portForwardsData);
|
setPortForwards(portForwardsData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load data:", err);
|
console.error("Failed to load data:", err);
|
||||||
@ -42,7 +45,7 @@ export function KubernetesPage() {
|
|||||||
const handleRemoveCluster = async (clusterId: string) => {
|
const handleRemoveCluster = async (clusterId: string) => {
|
||||||
try {
|
try {
|
||||||
await removeClusterCmd(clusterId);
|
await removeClusterCmd(clusterId);
|
||||||
setClusters((prev) => prev.filter((c) => c.id !== clusterId));
|
removeCluster(clusterId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to remove cluster:", err);
|
console.error("Failed to remove cluster:", err);
|
||||||
alert("Failed to remove cluster");
|
alert("Failed to remove cluster");
|
||||||
@ -70,7 +73,7 @@ export function KubernetesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddCluster = (cluster: ClusterInfo) => {
|
const handleAddCluster = (cluster: ClusterInfo) => {
|
||||||
setClusters((prev) => [...prev, cluster]);
|
addCluster(cluster);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartPortForward = (portForward: PortForwardResponse) => {
|
const handleStartPortForward = (portForward: PortForwardResponse) => {
|
||||||
@ -93,16 +96,40 @@ export function KubernetesPage() {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Kubernetes Management</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Kubernetes Management</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Manage your Kubernetes clusters and port forwarding sessions
|
Manage your Kubernetes clusters and resources
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8">
|
{/* Cluster Management Section */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold">Clusters</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAddClusterOpen(true)}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Add Cluster
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ClusterList
|
<ClusterList
|
||||||
clusters={clusters}
|
clusters={clusters}
|
||||||
onAdd={() => setIsAddClusterOpen(true)}
|
onAdd={() => setIsAddClusterOpen(true)}
|
||||||
onRemove={handleRemoveCluster}
|
onRemove={handleRemoveCluster}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Port Forwarding Section */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold">Port Forwarding</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsStartPortForwardOpen(true)}
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Start Port Forward
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PortForwardList
|
<PortForwardList
|
||||||
portForwards={portForwards}
|
portForwards={portForwards}
|
||||||
@ -112,12 +139,22 @@ export function KubernetesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Resource Browser Section */}
|
||||||
|
{selectedClusterId && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-semibold">Resource Browser</h2>
|
||||||
|
<ResourceBrowser clusterId={selectedClusterId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Cluster Modal */}
|
||||||
<AddClusterModal
|
<AddClusterModal
|
||||||
isOpen={isAddClusterOpen}
|
isOpen={isAddClusterOpen}
|
||||||
onClose={() => setIsAddClusterOpen(false)}
|
onClose={() => setIsAddClusterOpen(false)}
|
||||||
onAdd={handleAddCluster}
|
onAdd={handleAddCluster}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Port Forward Form */}
|
||||||
<PortForwardForm
|
<PortForwardForm
|
||||||
isOpen={isStartPortForwardOpen}
|
isOpen={isStartPortForwardOpen}
|
||||||
onClose={() => setIsStartPortForwardOpen(false)}
|
onClose={() => setIsStartPortForwardOpen(false)}
|
||||||
|
|||||||
185
src/stores/kubernetesStore.ts
Normal file
185
src/stores/kubernetesStore.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import type { ClusterInfo, ContextInfo, ResourceInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
export type ResourceType =
|
||||||
|
| "pods"
|
||||||
|
| "services"
|
||||||
|
| "deployments"
|
||||||
|
| "statefulsets"
|
||||||
|
| "daemonsets"
|
||||||
|
| "replicasets"
|
||||||
|
| "jobs"
|
||||||
|
| "cronjobs"
|
||||||
|
| "ingresses"
|
||||||
|
| "persistentvolumes"
|
||||||
|
| "persistentvolumeclaims"
|
||||||
|
| "configmaps"
|
||||||
|
| "secrets"
|
||||||
|
| "serviceaccounts"
|
||||||
|
| "roles"
|
||||||
|
| "clusterroles"
|
||||||
|
| "rolebindings"
|
||||||
|
| "clusterrolebindings"
|
||||||
|
| "nodes"
|
||||||
|
| "events"
|
||||||
|
| "hpas";
|
||||||
|
|
||||||
|
interface KubernetesState {
|
||||||
|
// Selection state
|
||||||
|
selectedClusterId: string | null;
|
||||||
|
selectedNamespace: string;
|
||||||
|
|
||||||
|
// Data state
|
||||||
|
clusters: ClusterInfo[];
|
||||||
|
contexts: ContextInfo[];
|
||||||
|
namespaces: Record<string, string[]>; // clusterId -> [namespaces]
|
||||||
|
|
||||||
|
// Loaded resources tracking
|
||||||
|
loadedResources: Set<ResourceType>;
|
||||||
|
|
||||||
|
// Terminal sessions
|
||||||
|
terminalSessions: Record<string, {
|
||||||
|
id: string;
|
||||||
|
clusterId: string;
|
||||||
|
namespace: string;
|
||||||
|
pod: string;
|
||||||
|
container: string;
|
||||||
|
command: string
|
||||||
|
}>;
|
||||||
|
nextTerminalId: number;
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
globalSearchQuery: string;
|
||||||
|
searchResults: Record<ResourceType, ResourceInfo[]>;
|
||||||
|
|
||||||
|
// Bulk selection
|
||||||
|
bulkSelection: Record<ResourceType, string[]>; // resourceType -> [resourceNames]
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setSelectedCluster: (clusterId: string) => void;
|
||||||
|
setSelectedNamespace: (namespace: string) => void;
|
||||||
|
addCluster: (cluster: ClusterInfo) => void;
|
||||||
|
removeCluster: (clusterId: string) => void;
|
||||||
|
updateCluster: (clusterId: string, updates: Partial<ClusterInfo>) => void;
|
||||||
|
addContext: (context: ContextInfo) => void;
|
||||||
|
setNamespaces: (clusterId: string, namespaces: string[]) => void;
|
||||||
|
markResourceLoaded: (type: ResourceType) => void;
|
||||||
|
markResourceUnloaded: (type: ResourceType) => void;
|
||||||
|
isResourceLoaded: (type: ResourceType) => boolean;
|
||||||
|
addTerminalSession: (session: { clusterId: string; namespace: string; pod: string; container: string; command: string }) => string;
|
||||||
|
removeTerminalSession: (sessionId: string) => void;
|
||||||
|
setGlobalSearchQuery: (query: string) => void;
|
||||||
|
setSearchResults: (type: ResourceType, results: ResourceInfo[]) => void;
|
||||||
|
addToBulkSelection: (type: ResourceType, resourceName: string) => void;
|
||||||
|
removeFromBulkSelection: (type: ResourceType, resourceName: string) => void;
|
||||||
|
clearBulkSelection: (type: ResourceType) => void;
|
||||||
|
getBulkSelectionCount: (type: ResourceType) => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKubernetesStore = create<KubernetesState>()((set, get) => ({
|
||||||
|
// Selection state
|
||||||
|
selectedClusterId: null,
|
||||||
|
selectedNamespace: "all",
|
||||||
|
|
||||||
|
// Data state
|
||||||
|
clusters: [],
|
||||||
|
contexts: [],
|
||||||
|
namespaces: {},
|
||||||
|
|
||||||
|
// Loaded resources tracking
|
||||||
|
loadedResources: new Set<ResourceType>(),
|
||||||
|
|
||||||
|
// Terminal sessions
|
||||||
|
terminalSessions: {},
|
||||||
|
nextTerminalId: 1,
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
globalSearchQuery: "",
|
||||||
|
searchResults: {} as Record<ResourceType, ResourceInfo[]>,
|
||||||
|
|
||||||
|
// Bulk selection
|
||||||
|
bulkSelection: {} as Record<ResourceType, string[]>,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setSelectedCluster: (clusterId) => set({ selectedClusterId: clusterId, selectedNamespace: "all" }),
|
||||||
|
|
||||||
|
setSelectedNamespace: (namespace) => set({ selectedNamespace: namespace }),
|
||||||
|
|
||||||
|
addCluster: (cluster) => set((state) => ({
|
||||||
|
clusters: [...state.clusters, cluster],
|
||||||
|
})),
|
||||||
|
|
||||||
|
removeCluster: (clusterId) => set((state) => ({
|
||||||
|
clusters: state.clusters.filter((c) => c.id !== clusterId),
|
||||||
|
selectedClusterId: state.selectedClusterId === clusterId ? null : state.selectedClusterId,
|
||||||
|
})),
|
||||||
|
|
||||||
|
updateCluster: (clusterId, updates) => set((state) => ({
|
||||||
|
clusters: state.clusters.map((c) =>
|
||||||
|
c.id === clusterId ? { ...c, ...updates } : c
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
|
||||||
|
addContext: (context) => set((state) => ({
|
||||||
|
contexts: [...state.contexts, context],
|
||||||
|
})),
|
||||||
|
|
||||||
|
setNamespaces: (clusterId, namespaces) => set((state) => ({
|
||||||
|
namespaces: { ...state.namespaces, [clusterId]: namespaces },
|
||||||
|
})),
|
||||||
|
|
||||||
|
markResourceLoaded: (type) => set((state) => {
|
||||||
|
const newSet = new Set(state.loadedResources);
|
||||||
|
newSet.add(type);
|
||||||
|
return { loadedResources: newSet };
|
||||||
|
}),
|
||||||
|
|
||||||
|
markResourceUnloaded: (type) => set((state) => {
|
||||||
|
const newSet = new Set(state.loadedResources);
|
||||||
|
newSet.delete(type);
|
||||||
|
return { loadedResources: newSet };
|
||||||
|
}),
|
||||||
|
|
||||||
|
isResourceLoaded: (type) => get().loadedResources.has(type),
|
||||||
|
|
||||||
|
addTerminalSession: (session) => {
|
||||||
|
const sessionId = `terminal-${get().nextTerminalId}`;
|
||||||
|
set((state) => ({
|
||||||
|
terminalSessions: { ...state.terminalSessions, [sessionId]: { id: sessionId, ...session } },
|
||||||
|
nextTerminalId: state.nextTerminalId + 1,
|
||||||
|
}));
|
||||||
|
return sessionId;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTerminalSession: (sessionId) => set((state) => ({
|
||||||
|
terminalSessions: Object.fromEntries(
|
||||||
|
Object.entries(state.terminalSessions).filter(([id]) => id !== sessionId)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
|
||||||
|
setGlobalSearchQuery: (query) => set({ globalSearchQuery: query }),
|
||||||
|
|
||||||
|
setSearchResults: (type, results) => set((state) => ({
|
||||||
|
searchResults: { ...state.searchResults, [type]: results },
|
||||||
|
})),
|
||||||
|
|
||||||
|
addToBulkSelection: (type, resourceName) => set((state) => ({
|
||||||
|
bulkSelection: {
|
||||||
|
...state.bulkSelection,
|
||||||
|
[type]: [...(state.bulkSelection[type] || []), resourceName],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
removeFromBulkSelection: (type, resourceName) => set((state) => ({
|
||||||
|
bulkSelection: {
|
||||||
|
...state.bulkSelection,
|
||||||
|
[type]: (state.bulkSelection[type] || []).filter((name) => name !== resourceName),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
clearBulkSelection: (type) => set((state) => ({
|
||||||
|
bulkSelection: { ...state.bulkSelection, [type]: [] },
|
||||||
|
})),
|
||||||
|
|
||||||
|
getBulkSelectionCount: (type) => (get().bulkSelection[type] || []).length,
|
||||||
|
}));
|
||||||
6
src/vite-env.d.ts
vendored
Normal file
6
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
161
tests/unit/kubernetesStore.test.ts
Normal file
161
tests/unit/kubernetesStore.test.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import { useKubernetesStore } from "@/stores/kubernetesStore";
|
||||||
|
import type { ResourceInfo } from "@/lib/tauriCommands";
|
||||||
|
|
||||||
|
describe("Kubernetes Store", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useKubernetesStore.getState().clusters.forEach((c) =>
|
||||||
|
useKubernetesStore.getState().removeCluster(c.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Cluster Management", () => {
|
||||||
|
it("should add a cluster", () => {
|
||||||
|
const cluster = {
|
||||||
|
id: "cluster-1",
|
||||||
|
name: "Production",
|
||||||
|
context: "prod-context",
|
||||||
|
cluster_url: "https://k8s.example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
useKubernetesStore.getState().addCluster(cluster);
|
||||||
|
|
||||||
|
expect(useKubernetesStore.getState().clusters).toHaveLength(1);
|
||||||
|
expect(useKubernetesStore.getState().clusters[0].name).toBe("Production");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a cluster", () => {
|
||||||
|
const cluster = {
|
||||||
|
id: "cluster-1",
|
||||||
|
name: "Production",
|
||||||
|
context: "prod-context",
|
||||||
|
cluster_url: "https://k8s.example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
useKubernetesStore.getState().addCluster(cluster);
|
||||||
|
useKubernetesStore.getState().removeCluster("cluster-1");
|
||||||
|
|
||||||
|
expect(useKubernetesStore.getState().clusters).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update a cluster", () => {
|
||||||
|
const cluster = {
|
||||||
|
id: "cluster-1",
|
||||||
|
name: "Production",
|
||||||
|
context: "prod-context",
|
||||||
|
cluster_url: "https://k8s.example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
useKubernetesStore.getState().addCluster(cluster);
|
||||||
|
useKubernetesStore.getState().updateCluster("cluster-1", { name: "Production New" });
|
||||||
|
|
||||||
|
expect(useKubernetesStore.getState().clusters[0].name).toBe("Production New");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set selected cluster", () => {
|
||||||
|
const cluster = {
|
||||||
|
id: "cluster-1",
|
||||||
|
name: "Production",
|
||||||
|
context: "prod-context",
|
||||||
|
cluster_url: "https://k8s.example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
useKubernetesStore.getState().addCluster(cluster);
|
||||||
|
useKubernetesStore.getState().setSelectedCluster("cluster-1");
|
||||||
|
|
||||||
|
expect(useKubernetesStore.getState().selectedClusterId).toBe("cluster-1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Namespace Management", () => {
|
||||||
|
it("should set selected namespace", () => {
|
||||||
|
useKubernetesStore.getState().setSelectedNamespace("default");
|
||||||
|
expect(useKubernetesStore.getState().selectedNamespace).toBe("default");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set namespaces for a cluster", () => {
|
||||||
|
useKubernetesStore.getState().setNamespaces("cluster-1", ["default", "kube-system", "production"]);
|
||||||
|
expect(useKubernetesStore.getState().namespaces["cluster-1"]).toEqual(["default", "kube-system", "production"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Resource Loading", () => {
|
||||||
|
it("should mark resource as loaded", () => {
|
||||||
|
useKubernetesStore.getState().markResourceLoaded("pods");
|
||||||
|
expect(useKubernetesStore.getState().isResourceLoaded("pods")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should mark resource as unloaded", () => {
|
||||||
|
useKubernetesStore.getState().markResourceLoaded("pods");
|
||||||
|
useKubernetesStore.getState().markResourceUnloaded("pods");
|
||||||
|
expect(useKubernetesStore.getState().isResourceLoaded("pods")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Terminal Sessions", () => {
|
||||||
|
it("should add a terminal session", () => {
|
||||||
|
const sessionId = useKubernetesStore.getState().addTerminalSession({
|
||||||
|
clusterId: "cluster-1",
|
||||||
|
namespace: "default",
|
||||||
|
pod: "nginx",
|
||||||
|
container: "nginx",
|
||||||
|
command: "bash",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sessionId).toBe("terminal-1");
|
||||||
|
expect(useKubernetesStore.getState().terminalSessions[sessionId]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a terminal session", () => {
|
||||||
|
const sessionId = useKubernetesStore.getState().addTerminalSession({
|
||||||
|
clusterId: "cluster-1",
|
||||||
|
namespace: "default",
|
||||||
|
pod: "nginx",
|
||||||
|
container: "nginx",
|
||||||
|
command: "bash",
|
||||||
|
});
|
||||||
|
|
||||||
|
useKubernetesStore.getState().removeTerminalSession(sessionId);
|
||||||
|
expect(useKubernetesStore.getState().terminalSessions[sessionId]).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Search", () => {
|
||||||
|
it("should set global search query", () => {
|
||||||
|
useKubernetesStore.getState().setGlobalSearchQuery("nginx");
|
||||||
|
expect(useKubernetesStore.getState().globalSearchQuery).toBe("nginx");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set search results", () => {
|
||||||
|
const results = [{ name: "nginx-1", namespace: "default" }];
|
||||||
|
useKubernetesStore.getState().setSearchResults("pods", results as ResourceInfo[]);
|
||||||
|
|
||||||
|
expect(useKubernetesStore.getState().searchResults.pods).toEqual(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Bulk Selection", () => {
|
||||||
|
it("should add to bulk selection", () => {
|
||||||
|
useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
|
||||||
|
expect(useKubernetesStore.getState().bulkSelection.pods).toContain("nginx-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove from bulk selection", () => {
|
||||||
|
useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
|
||||||
|
useKubernetesStore.getState().removeFromBulkSelection("pods", "nginx-1");
|
||||||
|
expect(useKubernetesStore.getState().bulkSelection.pods).not.toContain("nginx-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear bulk selection", () => {
|
||||||
|
useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
|
||||||
|
useKubernetesStore.getState().clearBulkSelection("pods");
|
||||||
|
expect(useKubernetesStore.getState().bulkSelection.pods).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get bulk selection count", () => {
|
||||||
|
useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
|
||||||
|
useKubernetesStore.getState().addToBulkSelection("pods", "nginx-2");
|
||||||
|
expect(useKubernetesStore.getState().getBulkSelectionCount("pods")).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -15,8 +15,9 @@
|
|||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": { "@/*": ["src/*"] },
|
"paths": { "@/*": ["./src/*"] },
|
||||||
"types": ["vitest/globals", "@testing-library/jest-dom", "node"]
|
"types": ["vitest/globals", "@testing-library/jest-dom", "node"]
|
||||||
},
|
},
|
||||||
"include": ["src", "tests/unit"],
|
"include": ["src", "tests/unit"],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user