First commit

This commit is contained in:
dvirlabs 2026-01-19 00:31:27 +02:00
commit bcb87093fa
52 changed files with 6625 additions and 0 deletions

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
# Backend Configuration
WORKSPACE_BASE_PATH=/tmp/helmview_workspaces
MAX_UPLOAD_SIZE=104857600
HELM_TIMEOUT=60
# Frontend Configuration (for production builds)
VITE_API_URL=http://localhost:8000

63
.gitignore vendored Normal file
View File

@ -0,0 +1,63 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
venv/
env/
ENV/
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
dist/
dist-ssr/
*.local
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
logs/
# Docker
docker-compose.override.yml
# HelmView specific
/tmp/helmview_workspaces/
*.tgz
*.tar.gz
*.zip

142
CHANGELOG.md Normal file
View File

@ -0,0 +1,142 @@
# Changelog
All notable changes to HelmView will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2026-01-18
### 🎉 Initial Release
#### Added
- **Project Management**
- Create, list, view, and delete projects
- Project metadata with name and description
- **Chart Upload & Extraction**
- Support for .tgz, .tar.gz, and .zip archives
- Secure extraction with zip-slip protection
- File size validation (100MB limit)
- Automatic Chart.yaml detection
- **Helm Rendering**
- Full helm lint integration
- helm template rendering with custom options
- Configurable release name and namespace
- Values override support (YAML)
- --set and --set-string flag support
- **Visual Resource View**
- Grid layout for all Kubernetes resources
- Resource cards with kind, name, namespace
- Expandable cards with summary and YAML tabs
- Resource modification tracking
- **Advanced Filtering**
- Filter by resource kind
- Filter by resource name (search)
- Filter by namespace (search)
- Clear all filters button
- Real-time filter updates
- **YAML Editor**
- Monaco Editor integration
- YAML syntax highlighting
- Kubernetes-aware autocomplete
- Real-time YAML validation
- Kubernetes schema validation
- Inline error markers
- Format YAML button
- Save functionality with validation
- **Export & Validation**
- Generate modified Helm charts
- Automatic version bumping (semver)
- helm lint validation before export
- helm template validation before export
- Chart packaging as .tgz
- Download exported charts
- Detailed error reporting
- **Docker Support**
- Development docker-compose.yml
- Production docker-compose.prod.yml
- Helm 3 pre-installed in backend
- Health checks for services
- Resource limits
- **Security Features**
- Sandboxed project workspaces
- No arbitrary code execution
- Command timeouts (60s)
- Input validation
- CORS configuration
- Safe path handling
- **Documentation**
- Comprehensive README.md
- Quick start guide (QUICKSTART.md)
- Security policy (SECURITY.md)
- Contributing guidelines (CONTRIBUTING.md)
- API documentation (FastAPI auto-docs)
### Technical Details
#### Backend
- FastAPI 0.109.0
- Python 3.11+
- Helm 3 CLI
- PyYAML for parsing
- Async request handling
- RESTful API design
#### Frontend
- React 18.2
- Vite 5.0
- Monaco Editor 0.45
- React Router 6.21
- Axios for API calls
- Modern ES6+ JavaScript
#### Infrastructure
- Docker containerization
- Docker Compose orchestration
- Volume persistence
- Health monitoring
- Production-ready nginx config
### Known Limitations
- No user authentication (must be added separately)
- No database (in-memory project storage)
- No multi-user support
- No undo/redo functionality
- Limited form-based editing
- No template source tracking
### Future Roadmap
- PostgreSQL integration for persistence
- User authentication & authorization
- Undo/redo per resource
- Template source tracking
- Form editors for common resources
- Diff viewer improvements
- Git integration
- Multi-user collaboration
---
## [Unreleased]
### Planned
- Database persistence layer
- User authentication system
- Enhanced diff viewer
- Form-based editors
- Project history and versioning
---
**Note**: This is the initial release. Future versions will be documented here as they are released.
[1.0.0]: https://github.com/yourusername/helmview/releases/tag/v1.0.0

338
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,338 @@
# Contributing to HelmView
Thank you for your interest in contributing to HelmView! This document provides guidelines and instructions for contributing.
## 🎯 Ways to Contribute
- 🐛 Report bugs
- 💡 Suggest new features
- 📝 Improve documentation
- 🔧 Submit bug fixes
- ✨ Add new features
- 🎨 Improve UI/UX
- 🧪 Write tests
- 🌍 Translate (future)
## 🚀 Getting Started
### Prerequisites
- Git
- Docker & Docker Compose
- Node.js 18+ (for local development)
- Python 3.11+ (for local development)
### Fork and Clone
```bash
# Fork the repository on GitHub
# Then clone your fork
git clone https://github.com/YOUR_USERNAME/helmview.git
cd helmview
```
### Development Setup
```bash
# Start the application
docker-compose up --build
# Or run locally
# Backend
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload
# Frontend (separate terminal)
cd frontend
npm install
npm run dev
```
## 📋 Development Workflow
### 1. Create a Branch
```bash
git checkout -b feature/my-new-feature
# or
git checkout -b fix/bug-description
```
### Branch Naming
- `feature/` - New features
- `fix/` - Bug fixes
- `docs/` - Documentation changes
- `refactor/` - Code refactoring
- `test/` - Test additions
- `chore/` - Maintenance tasks
### 2. Make Changes
#### Code Style
**Backend (Python)**
- Follow PEP 8
- Use type hints
- Add docstrings for functions
- Keep functions focused and small
**Frontend (JavaScript)**
- Use modern ES6+ syntax
- Follow React best practices
- Use functional components with hooks
- Keep components focused and reusable
#### Commit Messages
Follow the conventional commits format:
```
<type>(<scope>): <subject>
<body>
<footer>
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation
- `style`: Formatting
- `refactor`: Code restructuring
- `test`: Tests
- `chore`: Maintenance
Examples:
```bash
git commit -m "feat(editor): add YAML formatting button"
git commit -m "fix(upload): handle large file validation"
git commit -m "docs(readme): update installation steps"
```
### 3. Test Your Changes
#### Backend Tests
```bash
cd backend
pytest
# or with coverage
pytest --cov=. --cov-report=html
```
#### Frontend Tests
```bash
cd frontend
npm run test
# or with coverage
npm run test:coverage
```
#### Manual Testing
- Test all affected features
- Test error cases
- Test on different browsers (frontend)
- Verify Docker build works
### 4. Submit Pull Request
#### Before Submitting
- [ ] Code follows style guidelines
- [ ] Tests pass
- [ ] Documentation updated
- [ ] Commit messages are clear
- [ ] No merge conflicts
- [ ] Docker build succeeds
#### PR Description Template
```markdown
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
How to test these changes
## Screenshots (if applicable)
Add screenshots for UI changes
## Checklist
- [ ] Tests pass
- [ ] Documentation updated
- [ ] No breaking changes (or documented)
```
## 🐛 Reporting Bugs
### Before Reporting
1. Check existing issues
2. Verify it's reproducible
3. Test on latest version
### Bug Report Template
```markdown
**Describe the bug**
Clear description of the bug
**To Reproduce**
Steps to reproduce:
1. Go to '...'
2. Click on '...'
3. See error
**Expected behavior**
What should happen
**Actual behavior**
What actually happens
**Screenshots**
If applicable
**Environment**
- OS: [e.g., Windows 10]
- Browser: [e.g., Chrome 120]
- Version: [e.g., 1.0.0]
**Additional context**
Any other relevant information
```
## 💡 Feature Requests
### Feature Request Template
```markdown
**Is your feature request related to a problem?**
Description of the problem
**Describe the solution you'd like**
Clear description of desired feature
**Describe alternatives you've considered**
Alternative solutions or features
**Additional context**
Mockups, examples, etc.
```
## 📝 Documentation
### Documentation Guidelines
- Use clear, concise language
- Include code examples
- Add screenshots for UI features
- Keep README.md up to date
- Update API documentation
- Add inline code comments
### Documentation Locations
- `README.md` - Main documentation
- `QUICKSTART.md` - Quick start guide
- `SECURITY.md` - Security policy
- `docs/` - Detailed documentation (future)
- Code comments - Inline documentation
## 🎨 UI/UX Guidelines
### Design Principles
- **Simplicity**: Keep interfaces clean
- **Consistency**: Follow existing patterns
- **Accessibility**: WCAG 2.1 AA compliance
- **Responsiveness**: Mobile-friendly
- **Performance**: Fast loading times
### Color Palette
- Primary: `#667eea` (purple)
- Success: `#48bb78` (green)
- Warning: `#ed8936` (orange)
- Error: `#f56565` (red)
- Neutral: Shades of gray
## 🧪 Testing Guidelines
### Test Coverage Goals
- Backend: >80%
- Frontend: >70%
- Critical paths: 100%
### Test Types
1. **Unit Tests**: Individual functions
2. **Integration Tests**: API endpoints
3. **E2E Tests**: Full workflows (future)
4. **Security Tests**: Vulnerability scanning
## 🔒 Security
- Report security issues privately
- See [SECURITY.md](SECURITY.md) for details
- Never commit secrets or credentials
- Use environment variables
## 📦 Release Process
### Versioning
We follow [Semantic Versioning](https://semver.org/):
- MAJOR: Breaking changes
- MINOR: New features
- PATCH: Bug fixes
### Release Checklist
1. Update version numbers
2. Update CHANGELOG.md
3. Run full test suite
4. Update documentation
5. Create release tag
6. Build and test Docker images
7. Publish release notes
## 🤝 Code Review
### Review Process
1. Submit PR
2. Automated checks run
3. Maintainer reviews code
4. Address feedback
5. Approval and merge
### Review Criteria
- Code quality
- Test coverage
- Documentation
- Security considerations
- Performance impact
## 💬 Communication
### Channels
- **GitHub Issues**: Bug reports, features
- **GitHub Discussions**: Questions, ideas
- **Pull Requests**: Code review
- **Email**: Security issues only
### Response Times
- Critical bugs: 24-48 hours
- Other issues: 3-7 days
- Pull requests: 7-14 days
## 📜 License
By contributing, you agree that your contributions will be licensed under the MIT License.
## 🙏 Recognition
Contributors will be:
- Listed in CONTRIBUTORS.md
- Mentioned in release notes
- Credited in documentation
## ❓ Questions?
- Check existing documentation
- Search closed issues
- Open a GitHub Discussion
- Contact maintainers
---
**Thank you for contributing to HelmView! 🎉**

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 HelmView Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

354
PROJECT_STRUCTURE.md Normal file
View File

@ -0,0 +1,354 @@
# HelmView Project Structure
```
helmview/
├── backend/ # FastAPI Backend
│ ├── main.py # Main application file
│ ├── requirements.txt # Python dependencies
│ ├── Dockerfile # Backend Docker image
│ └── .dockerignore # Docker build exclusions
├── frontend/ # React Frontend
│ ├── src/
│ │ ├── components/ # Reusable React components
│ │ │ ├── ResourceCard.jsx # Resource display card
│ │ │ ├── ResourceCard.css
│ │ │ ├── YamlEditor.jsx # Monaco YAML editor
│ │ │ ├── YamlEditor.css
│ │ │ ├── UploadSection.jsx # Chart upload modal
│ │ │ ├── UploadSection.css
│ │ │ ├── RenderSection.jsx # Chart render modal
│ │ │ ├── RenderSection.css
│ │ │ ├── ExportModal.jsx # Chart export modal
│ │ │ └── ExportModal.css
│ │ │
│ │ ├── pages/ # Page components
│ │ │ ├── ProjectDashboard.jsx # Projects list page
│ │ │ ├── ProjectDashboard.css
│ │ │ ├── RenderView.jsx # Resource view page
│ │ │ └── RenderView.css
│ │ │
│ │ ├── api.js # API client (Axios)
│ │ ├── App.jsx # Root component
│ │ ├── App.css # Global styles
│ │ ├── main.jsx # Entry point
│ │ └── index.css # Base styles
│ │
│ ├── public/ # Static assets
│ ├── index.html # HTML template
│ ├── package.json # Node dependencies
│ ├── vite.config.js # Vite configuration
│ ├── Dockerfile # Development Docker image
│ ├── Dockerfile.prod # Production Docker image
│ ├── nginx.conf # Nginx config for production
│ └── .dockerignore # Docker build exclusions
├── docs/ # Documentation (future)
├── tests/ # Tests (future)
│ ├── backend/ # Backend tests
│ └── frontend/ # Frontend tests
├── .gitignore # Git exclusions
├── .env.example # Environment variables template
├── docker-compose.yml # Development orchestration
├── docker-compose.prod.yml # Production orchestration
├── README.md # Main documentation
├── QUICKSTART.md # Quick start guide
├── CONTRIBUTING.md # Contribution guidelines
├── SECURITY.md # Security policy
├── CHANGELOG.md # Version history
├── LICENSE # MIT License
└── PROJECT_STRUCTURE.md # This file
```
## File Descriptions
### Backend (`/backend`)
#### `main.py`
The core FastAPI application containing:
- **Models**: Pydantic models for request/response validation
- **Storage**: In-memory project database
- **Helper Functions**:
- `get_project_path()` - Project directory management
- `safe_extract_archive()` - Secure chart extraction
- `run_helm_command()` - Async Helm execution
- `parse_kubernetes_resources()` - YAML parsing
- `update_chart_version()` - Semver version bumping
- **API Endpoints**:
- Project CRUD operations
- Chart upload and extraction
- Helm rendering with options
- Resource updates
- Chart export and download
- Schema retrieval for autocomplete
#### `requirements.txt`
Python dependencies:
- `fastapi` - Web framework
- `uvicorn` - ASGI server
- `pyyaml` - YAML processing
- `aiofiles` - Async file operations
- `kubernetes` - K8s Python client
- `python-multipart` - File upload support
- `jsonpatch` - JSON patching
#### `Dockerfile`
Multi-stage Docker build:
1. Install system dependencies
2. Install Helm 3
3. Install Python dependencies
4. Copy application code
5. Configure runtime
### Frontend (`/frontend`)
#### Components
**ResourceCard.jsx**
- Displays individual Kubernetes resources
- Expandable card interface
- Tabs for Summary and YAML views
- Integrates YamlEditor component
- Handles resource updates
**YamlEditor.jsx**
- Monaco Editor integration
- Kubernetes autocomplete
- Real-time YAML validation
- Schema-based suggestions
- Format and save functionality
- Kind-specific completions
**UploadSection.jsx**
- Drag-and-drop file upload
- File validation
- Progress indication
- Error handling
**RenderSection.jsx**
- Helm render options form
- Values override editor
- Lint output display
- Error reporting
**ExportModal.jsx**
- Export process management
- Validation status display
- Download functionality
- Error details
#### Pages
**ProjectDashboard.jsx**
- List all projects
- Create new projects
- Delete projects
- Navigate to project view
**RenderView.jsx**
- Main resource view
- Advanced filtering
- Resource grid layout
- Stats display
- Modals orchestration
#### Configuration
**vite.config.js**
- Dev server configuration
- Port settings (5173)
- Hot module replacement
- Build optimizations
**nginx.conf**
- Production web server config
- Gzip compression
- Security headers
- API proxy configuration
- Static asset caching
### Docker Configuration
#### `docker-compose.yml` (Development)
- Hot reload enabled
- Volume mounts for code
- Port exposure for debugging
- Health checks
#### `docker-compose.prod.yml` (Production)
- Optimized builds
- Resource limits
- Health checks
- Restart policies
- Named volumes
### Documentation Files
#### `README.md`
- Project overview
- Feature list
- Installation instructions
- Usage guide
- Architecture details
- API documentation
- Troubleshooting
#### `QUICKSTART.md`
- 3-step quick start
- Sample workflow
- Common use cases
- Troubleshooting tips
#### `CONTRIBUTING.md`
- Contribution guidelines
- Development setup
- Code style guides
- Commit conventions
- PR process
- Testing requirements
#### `SECURITY.md`
- Security features
- Vulnerability reporting
- Best practices
- Production hardening
- Compliance notes
#### `CHANGELOG.md`
- Version history
- Release notes
- Breaking changes
- Migration guides
## Key Design Decisions
### Backend Architecture
- **Async/await**: All I/O operations are async for performance
- **In-memory storage**: Simple start, easy to replace with database
- **Sandboxed execution**: Each project isolated in filesystem
- **No direct Helm API**: Uses CLI for maximum compatibility
### Frontend Architecture
- **Functional components**: Modern React with hooks
- **No TypeScript**: Per requirements, uses JavaScript
- **Component composition**: Small, focused components
- **CSS modules approach**: Separate CSS per component
- **Monaco Editor**: Industry-standard editor with LSP support
### Security Architecture
- **Defense in depth**: Multiple layers of validation
- **Principle of least privilege**: Minimal permissions
- **Input validation**: All inputs validated
- **Safe defaults**: Secure by default configuration
### Scalability Considerations
- **Stateless backend**: Easy horizontal scaling
- **Async operations**: High concurrency support
- **Docker containers**: Easy deployment and scaling
- **Database-ready**: Structure supports easy DB integration
## Development Workflow
### Local Development
1. Start backend: `uvicorn main:app --reload`
2. Start frontend: `npm run dev`
3. Make changes (hot reload active)
4. Test locally
5. Commit and push
### Docker Development
1. Start: `docker-compose up`
2. Make changes (hot reload via volumes)
3. Test in containers
4. Rebuild if needed: `docker-compose up --build`
### Production Build
1. Build images: `docker-compose -f docker-compose.prod.yml build`
2. Test locally: `docker-compose -f docker-compose.prod.yml up`
3. Deploy to production environment
4. Monitor health checks
## Extension Points
### Adding New Features
**New Backend Endpoint**
1. Add route in `main.py`
2. Add Pydantic model if needed
3. Implement handler function
4. Add API client method in `frontend/src/api.js`
5. Update documentation
**New Frontend Component**
1. Create component file in `src/components/`
2. Create corresponding CSS file
3. Import in parent component
4. Add props and state management
5. Test in isolation
**New Validation Rule**
1. Add to `validateYaml()` in `YamlEditor.jsx`
2. Add backend validation in `main.py`
3. Update error messages
4. Add tests
**Database Integration**
1. Add SQLAlchemy models
2. Add Alembic migrations
3. Replace in-memory `projects_db`
4. Update all CRUD operations
5. Add connection pooling
6. Update docker-compose with database service
## Performance Considerations
### Backend
- Async I/O for non-blocking operations
- Command timeouts prevent hanging
- Temporary file cleanup
- Efficient YAML parsing
### Frontend
- Code splitting (Vite handles this)
- Lazy loading of Monaco Editor
- Debounced filter inputs
- Virtual scrolling for large lists (future)
### Docker
- Multi-stage builds reduce image size
- Layer caching optimizes builds
- Resource limits prevent exhaustion
- Health checks ensure availability
## Testing Strategy
### Backend Tests (Future)
- Unit tests for helper functions
- Integration tests for API endpoints
- Security tests for validation
- Performance tests for Helm operations
### Frontend Tests (Future)
- Component unit tests
- Integration tests for pages
- E2E tests for critical flows
- Accessibility tests
### Manual Testing Checklist
- [ ] Upload various chart formats
- [ ] Render with different options
- [ ] Edit and save YAML
- [ ] Export and validate
- [ ] Filter resources
- [ ] Error handling
- [ ] Cross-browser testing
---
**Last Updated**: January 2026
**Version**: 1.0.0

135
QUICKSTART.md Normal file
View File

@ -0,0 +1,135 @@
# HelmView Quick Start Guide
## 🚀 Get Started in 3 Steps
### Step 1: Start the Application
```bash
# Clone and enter directory
cd helmview
# Start with Docker Compose
docker-compose up --build
```
Wait for both services to start (2-3 minutes first time).
### Step 2: Open HelmView
Open your browser and navigate to:
- **Frontend**: http://localhost:5173
- **API Docs**: http://localhost:8000/docs
### Step 3: Create Your First Project
1. **Create a project**
- Click "Create Project"
- Enter name: "my-first-chart"
- Click "Create Project"
2. **Upload a Helm chart**
- Get a sample chart:
```bash
helm create sample-chart
helm package sample-chart
```
- Click "Upload Chart"
- Upload the `sample-chart-0.1.0.tgz` file
3. **Render the chart**
- Click "Render"
- Keep default settings
- Click "Render Chart"
4. **Edit resources**
- Click on any resource card (e.g., Deployment)
- Switch to "YAML" tab
- Edit the YAML (try changing replicas)
- Click "Save Changes"
5. **Export modified chart**
- Click "Export Chart"
- Wait for validation
- Click "Download Chart"
## 📋 What You Get
After following these steps, you'll have:
- ✅ A running HelmView instance
- ✅ A modified Helm chart with your changes
- ✅ A validated, packaged chart ready to deploy
## 🎯 Next Steps
- **Filter Resources**: Use the filters to find specific resources
- **Batch Edits**: Edit multiple resources and export them all
- **Re-render**: Try different values overrides
- **Multiple Projects**: Create more projects for different charts
## 💡 Tips
### Getting a Helm Chart to Test
```bash
# Create a new chart
helm create my-app
# Package it
helm package my-app
# Upload my-app-0.1.0.tgz to HelmView
```
### Using Existing Charts
```bash
# Add Helm repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Pull a chart
helm pull bitnami/nginx --untar
# Package it
helm package nginx
# Upload nginx-*.tgz to HelmView
```
### Common Edits to Try
- Change `spec.replicas` in Deployments
- Add environment variables
- Modify service ports
- Add labels and annotations
- Change resource limits
## 🐛 Troubleshooting
**Application won't start?**
```bash
# Check Docker is running
docker ps
# View logs
docker-compose logs backend
docker-compose logs frontend
# Restart clean
docker-compose down -v
docker-compose up --build
```
**Can't upload chart?**
- Ensure file is `.tgz`, `.tar.gz`, or `.zip`
- Max size is 100MB
- Chart must have valid `Chart.yaml`
**Export fails?**
- Check all YAML edits are saved
- Review helm lint output in the modal
- Ensure YAML syntax is valid
## 📚 Learn More
- Read the full [README.md](README.md)
- Check API docs at http://localhost:8000/docs
- Review example charts in `/examples` (coming soon)
---
**Happy Helm Editing! 🎉**

323
README.md Normal file
View File

@ -0,0 +1,323 @@
# HelmView - Visual Helm Chart Editor & Validator
![HelmView](https://img.shields.io/badge/helm-v3-blue)
![React](https://img.shields.io/badge/react-18-blue)
![FastAPI](https://img.shields.io/badge/fastapi-0.109-green)
![Python](https://img.shields.io/badge/python-3.11+-blue)
**HelmView** is a production-ready web-based visual Helm chart editor and validator. Upload existing Helm charts, render them, visualize all Kubernetes resources, edit them safely with autocomplete + validation, and export fully working Helm charts that pass all validation checks.
## 🚀 Features
### Core Functionality
- **📤 Chart Upload**: Upload Helm charts as `.tgz`, `.tar.gz`, or `.zip` archives
- **🔄 Helm Rendering**: Render charts with custom values, release names, and namespaces
- **👀 Visual Resource View**: Grid view of all Kubernetes resources with filtering
- **✏️ YAML Editor**: Monaco editor with:
- Real-time syntax highlighting
- Kubernetes-aware autocomplete
- Schema-based validation
- Inline error markers
- **📦 Safe Export**: Export modified charts with automatic validation
- **🔍 Advanced Filtering**: Filter resources by kind, name, namespace, and labels
- **📊 Resource Tracking**: Track modifications and view diffs
### Security Features
- ✅ Sandboxed filesystem per project
- ✅ Protection against zip-slip attacks
- ✅ File size limits (100MB default)
- ✅ Execution timeouts for Helm commands
- ✅ No user code execution - only Helm template/lint
- ✅ Input validation and YAML schema checking
## 📋 Tech Stack
### Frontend
- **React 18** with Vite (JavaScript)
- **Monaco Editor** for YAML editing with autocomplete
- **React Router** for navigation
- **Axios** for API communication
- **js-yaml** for YAML parsing
### Backend
- **FastAPI** (Python 3.11+)
- **Helm 3** CLI integration
- **PyYAML** for YAML processing
- **Kubernetes Python Client** for schema validation
### Infrastructure
- **Docker** + **Docker Compose** for containerized deployment
- PostgreSQL support ready (optional, for history/versioning)
## 🛠️ Installation & Setup
### Prerequisites
- Docker 20.10+
- Docker Compose 2.0+
- 4GB RAM minimum
- 10GB free disk space
### Quick Start
1. **Clone the repository**
```bash
git clone <repository-url>
cd helmview
```
2. **Start the application**
```bash
docker-compose up --build
```
3. **Access the application**
- Frontend: http://localhost:5173
- Backend API: http://localhost:8000
- API Documentation: http://localhost:8000/docs
The application will automatically:
- Install Helm 3 in the backend container
- Set up all dependencies
- Create necessary directories
- Start both frontend and backend services
### Environment Variables
Create a `.env` file to customize settings:
```env
# Backend Configuration
WORKSPACE_BASE_PATH=/tmp/helmview_workspaces
MAX_UPLOAD_SIZE=104857600 # 100MB in bytes
HELM_TIMEOUT=60 # seconds
# Frontend Configuration
VITE_API_URL=http://localhost:8000
```
## 📖 Usage Guide
### 1. Create a Project
1. Click "**Create Project**" on the dashboard
2. Enter project name and optional description
3. Click "**Create Project**"
### 2. Upload a Helm Chart
1. Open your project
2. Click "**Upload Chart**"
3. Drag & drop or browse for your chart file (`.tgz`, `.tar.gz`, or `.zip`)
4. Click "**Upload Chart**"
### 3. Render the Chart
1. Click "**Render**" or "**Re-render**"
2. Configure render options:
- **Release Name**: Name for the Helm release
- **Namespace**: Target Kubernetes namespace
- **Values Override**: Optional YAML to override values
3. Click "**Render Chart**"
4. View rendered resources in the grid
### 4. Edit Resources
1. Click on any resource card to expand it
2. Switch to the "**YAML**" tab
3. Edit the YAML with autocomplete support:
- Press `Ctrl+Space` for autocomplete suggestions
- Real-time validation shows errors instantly
4. Click "**Save Changes**" (only enabled when valid)
### 5. Filter Resources
Use the filters section to narrow down resources:
- **Kind**: Filter by resource type (Deployment, Service, etc.)
- **Name**: Search by resource name
- **Namespace**: Filter by namespace
### 6. Export Modified Chart
1. Click "**Export Chart**"
2. Review the export process steps
3. Click "**Export Chart**"
4. Wait for validation (helm lint + helm template)
5. If successful, click "**Download Chart**"
The exported chart includes:
- All your modifications
- Bumped version in Chart.yaml
- Generated overlay templates (_helmview_generated.yaml)
- Full Helm validation passed
## 🏗️ Architecture
### Project Structure
```
helmview/
├── backend/
│ ├── main.py # FastAPI application
│ ├── requirements.txt # Python dependencies
│ └── Dockerfile # Backend container
├── frontend/
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── ResourceCard.jsx
│ │ │ ├── YamlEditor.jsx
│ │ │ ├── UploadSection.jsx
│ │ │ ├── RenderSection.jsx
│ │ │ └── ExportModal.jsx
│ │ ├── pages/ # Page components
│ │ │ ├── ProjectDashboard.jsx
│ │ │ └── RenderView.jsx
│ │ ├── api.js # API client
│ │ ├── App.jsx # Root component
│ │ └── main.jsx # Entry point
│ ├── package.json
│ ├── vite.config.js
│ └── Dockerfile # Frontend container
└── docker-compose.yml # Orchestration
```
### API Endpoints
#### Projects
- `POST /api/projects` - Create a new project
- `GET /api/projects` - List all projects
- `GET /api/projects/{id}` - Get project details
- `DELETE /api/projects/{id}` - Delete a project
#### Charts
- `POST /api/projects/{id}/upload` - Upload a Helm chart
- `POST /api/projects/{id}/render` - Render chart with options
- `GET /api/projects/{id}/resources` - Get all resources
- `PUT /api/projects/{id}/resources/{uid}` - Update a resource
- `POST /api/projects/{id}/export` - Export modified chart
- `GET /api/projects/{id}/download` - Download exported chart
#### Utilities
- `GET /health` - Health check
- `GET /api/kubernetes-schemas/{kind}` - Get K8s schema for autocomplete
## 🔒 Security Notes
### Implemented Protections
1. **File Upload Security**
- Size limits enforced (100MB default)
- Extension validation
- Zip-slip protection during extraction
- Safe path traversal checks
2. **Execution Security**
- No arbitrary code execution
- Helm runs only with `template` and `lint` commands
- No hook execution
- Command timeouts (60s default)
3. **Isolation**
- Each project gets a sandboxed directory
- Temporary files cleaned up after operations
- Docker container isolation
4. **Input Validation**
- YAML syntax validation
- Kubernetes schema validation
- API input sanitization
### Production Recommendations
- [ ] Add authentication/authorization
- [ ] Implement rate limiting
- [ ] Use persistent storage (PostgreSQL) for project metadata
- [ ] Set up HTTPS/TLS
- [ ] Configure resource limits in docker-compose
- [ ] Add audit logging
- [ ] Implement RBAC for multi-user scenarios
- [ ] Regular security updates for dependencies
## 🧪 Development
### Local Development (without Docker)
#### Backend
```bash
cd backend
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
#### Frontend
```bash
cd frontend
npm install
npm run dev
```
### Running Tests
```bash
# Backend tests (add pytest)
cd backend
pytest
# Frontend tests (add vitest)
cd frontend
npm run test
```
## 📊 Future Enhancements
### Planned Features
- [ ] **Database Integration**: PostgreSQL for project history, versioning, and diffs
- [ ] **Undo/Redo**: Per-resource undo/redo functionality
- [ ] **Template Source Tracking**: Show which Helm template produced each resource
- [ ] **Values Highlighting**: Highlight values.yaml keys used by resources
- [ ] **Multi-user Support**: User authentication and project sharing
- [ ] **Diff Viewer**: Visual diff between original and modified resources
- [ ] **Form Editors**: GUI editors for common resource types
- [ ] **Search**: Full-text search across all resources
- [ ] **Export to Git**: Push modified charts directly to Git repositories
## 🐛 Troubleshooting
### Common Issues
**1. Backend fails to start**
```bash
# Check Helm installation
docker-compose exec backend helm version
# Check logs
docker-compose logs backend
```
**2. Frontend can't connect to backend**
- Verify backend is running: `curl http://localhost:8000/health`
- Check CORS settings in backend/main.py
- Ensure `VITE_API_URL` is set correctly
**3. Chart upload fails**
- Verify file size is under 100MB
- Ensure file is valid `.tgz`, `.tar.gz`, or `.zip`
- Check backend logs for detailed error
**4. Export fails validation**
- Review helm lint output in the modal
- Check for YAML syntax errors
- Ensure all resources have required fields
## 📝 License
MIT License - See LICENSE file for details
## 🤝 Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Submit a pull request
## 📧 Support
For issues and questions:
- GitHub Issues: [Create an issue]
- Documentation: [Wiki]
---
**Built with ❤️ for the Kubernetes community**

173
SECURITY.md Normal file
View File

@ -0,0 +1,173 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
## Security Features
HelmView implements several security measures:
### File Upload Security
- File size limits (100MB default)
- Extension validation (`.tgz`, `.tar.gz`, `.zip` only)
- Zip-slip attack protection
- Safe path traversal checks
- Sandboxed extraction directories
### Execution Security
- No arbitrary code execution
- Helm runs only with `template` and `lint` commands
- No hook execution allowed
- Command timeouts (60s default)
- Resource limits in Docker containers
### Input Validation
- YAML syntax validation
- Kubernetes schema validation
- API input sanitization
- SQL injection protection (when using database)
### Isolation
- Per-project sandboxed directories
- Docker container isolation
- Temporary file cleanup
- No network access from Helm commands
## Reporting a Vulnerability
We take security vulnerabilities seriously. If you discover a security issue, please follow these steps:
### DO NOT
- Open a public GitHub issue
- Discuss the vulnerability publicly
- Exploit the vulnerability
### DO
1. **Email**: Send details to security@helmview.io (if available) or create a private security advisory on GitHub
2. **Include**:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
3. **Wait**: Allow up to 48 hours for initial response
### What to Expect
- **Initial Response**: Within 48 hours
- **Status Update**: Within 7 days
- **Fix Timeline**: Depends on severity
- Critical: 1-7 days
- High: 7-30 days
- Medium: 30-90 days
- Low: Next release cycle
## Security Best Practices for Deployment
### Production Deployment
1. **Authentication**: Implement user authentication (not included by default)
2. **HTTPS**: Always use TLS/SSL in production
3. **Firewall**: Restrict backend access to frontend only
4. **Secrets**: Use environment variables or secret management
5. **Updates**: Keep dependencies updated regularly
6. **Monitoring**: Set up security monitoring and logging
7. **Backups**: Regular backups of project data
8. **Resource Limits**: Configure Docker resource limits
### Network Security
```yaml
# Recommended docker-compose.yml additions
services:
backend:
networks:
- internal
frontend:
networks:
- internal
- external
networks:
internal:
internal: true
external:
```
### Environment Hardening
```bash
# Set restrictive file permissions
chmod 600 .env
# Use non-root user in containers
# (already implemented in Dockerfiles)
# Enable Docker security features
docker run --security-opt=no-new-privileges --cap-drop=ALL
```
### Rate Limiting
Consider implementing rate limiting for production:
- File uploads: 10 per hour per IP
- API requests: 100 per minute per IP
- Chart renders: 20 per hour per project
### Database Security (when using PostgreSQL)
- Use strong passwords
- Enable SSL connections
- Regular security updates
- Principle of least privilege for DB users
- Regular backups with encryption
## Known Limitations
### Current Security Limitations
1. **No Authentication**: Users must implement their own auth layer
2. **No Rate Limiting**: Should be added for production
3. **No Audit Logging**: Consider adding for compliance
4. **Shared Resources**: All users share the same backend (multi-tenancy not implemented)
### Helm Security Considerations
- Helm charts may contain malicious templates (mitigated by no hook execution)
- Large charts may consume significant resources (mitigated by timeouts)
- Chart dependencies are not validated (user responsibility)
## Security Checklist for Production
- [ ] Enable HTTPS/TLS
- [ ] Implement authentication & authorization
- [ ] Set up rate limiting
- [ ] Configure firewall rules
- [ ] Set resource limits
- [ ] Enable audit logging
- [ ] Set up monitoring & alerts
- [ ] Regular security updates
- [ ] Backup strategy in place
- [ ] Incident response plan defined
- [ ] Security review completed
- [ ] Penetration testing performed
## Compliance
HelmView does not currently provide built-in compliance features for:
- HIPAA
- PCI-DSS
- SOC 2
- GDPR (data handling must be implemented separately)
Users requiring compliance should implement additional controls.
## References
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [CWE Top 25](https://cwe.mitre.org/top25/)
- [Docker Security Best Practices](https://docs.docker.com/develop/security-best-practices/)
- [Kubernetes Security](https://kubernetes.io/docs/concepts/security/)
- [Helm Security Considerations](https://helm.sh/docs/topics/rbac/)
## License
This security policy is part of the HelmView project and follows the same MIT License.
---
**Last Updated**: January 2026

18
backend/.dockerignore Normal file
View File

@ -0,0 +1,18 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
*.egg-info/
.eggs/
dist/
build/
.pytest_cache/
.coverage
htmlcov/
.git/
.gitignore
README.md
.env

32
backend/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM python:3.11-slim
# Install system dependencies and Helm
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Helm
RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Verify Helm installation
RUN helm version
# Set working directory
WORKDIR /app
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create workspace directory
RUN mkdir -p /tmp/helmview_workspaces
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

1019
backend/main.py Normal file

File diff suppressed because it is too large Load Diff

8
backend/requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
python-multipart==0.0.6
pydantic==2.5.3
pyyaml==6.0.1
aiofiles==23.2.1
kubernetes==29.0.0
jsonpatch==1.33

57
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,57 @@
# Production Docker Compose Configuration
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- helm_workspaces:/tmp/helmview_workspaces
environment:
- PYTHONUNBUFFERED=1
- WORKSPACE_BASE_PATH=/tmp/helmview_workspaces
- MAX_UPLOAD_SIZE=104857600
- HELM_TIMEOUT=60
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 512M
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
args:
- VITE_API_URL=${VITE_API_URL:-http://localhost:8000}
ports:
- "80:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
volumes:
helm_workspaces:
driver: local

41
docker-compose.yml Normal file
View File

@ -0,0 +1,41 @@
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./backend:/app
- helm_workspaces:/tmp/helmview_workspaces
environment:
- PYTHONUNBUFFERED=1
- WORKSPACE_BASE_PATH=/tmp/helmview_workspaces
- MAX_UPLOAD_SIZE=104857600
- HELM_TIMEOUT=60
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- VITE_API_URL=http://localhost:8000
command: npm run dev -- --host
depends_on:
- backend
volumes:
helm_workspaces:

12
frontend/.dockerignore Normal file
View File

@ -0,0 +1,12 @@
node_modules/
dist/
dist-ssr/
*.local
.git/
.gitignore
README.md
.env
.env.*
npm-debug.log*
yarn-debug.log*
yarn-error.log*

18
frontend/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Expose port
EXPOSE 5173
# Run development server
CMD ["npm", "run", "dev", "--", "--host"]

35
frontend/Dockerfile.prod Normal file
View File

@ -0,0 +1,35 @@
# Multi-stage build for production
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source
COPY . .
# Build for production
RUN npm run build
# Production stage with nginx
FROM nginx:alpine
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

13
frontend/index.html Normal file
View File

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

48
frontend/nginx.conf Normal file
View File

@ -0,0 +1,48 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Main location
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

27
frontend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "helmview-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"@monaco-editor/react": "^4.6.0",
"monaco-editor": "^0.45.0",
"js-yaml": "^4.1.0",
"diff": "^5.1.0",
"axios": "^1.6.5"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.11"
}
}

357
frontend/src/App.css Normal file
View File

@ -0,0 +1,357 @@
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
flex: 1;
}
.theme-toggle {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
width: 48px;
height: 48px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: rotate(180deg) scale(1.1);
}
.app-header h1 {
font-size: 2rem;
margin-bottom: 0.25rem;
font-weight: 600;
}
.tagline {
font-size: 0.95rem;
opacity: 0.9;
font-weight: 300;
}
.app-main {
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 2rem;
}
/* Common Styles */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #48bb78;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #38a169;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4);
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #e53e3e;
}
.btn-outline {
background: transparent;
border: 2px solid #667eea;
color: #667eea;
}
.btn-outline:hover:not(:disabled) {
background: #667eea;
color: white;
}
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.input:focus {
outline: none;
border-color: #667eea;
}
.textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 0.95rem;
font-family: 'Monaco', 'Menlo', monospace;
resize: vertical;
min-height: 120px;
}
.textarea:focus {
outline: none;
border-color: #667eea;
}
.label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2d3748;
}
.error {
color: #f56565;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.success {
color: #48bb78;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Dark Theme Styles */
[data-theme="dark"] {
--bg-primary: #1a202c;
--bg-secondary: #2d3748;
--bg-tertiary: #4a5568;
--text-primary: #f7fafc;
--text-secondary: #e2e8f0;
--text-muted: #a0aec0;
--border-color: #4a5568;
--card-bg: #2d3748;
--hover-bg: #374151;
}
[data-theme="dark"] body {
background: var(--bg-primary);
color: var(--text-primary);
}
[data-theme="dark"] .app {
background: var(--bg-primary);
}
[data-theme="dark"] .app-header {
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
}
[data-theme="dark"] .card,
[data-theme="dark"] .resource-card {
background: var(--card-bg);
color: var(--text-primary);
}
[data-theme="dark"] .modal {
background: var(--bg-secondary);
color: var(--text-primary);
}
[data-theme="dark"] .modal-header {
border-bottom-color: var(--border-color);
}
[data-theme="dark"] .modal-footer {
border-top-color: var(--border-color);
}
[data-theme="dark"] .input,
[data-theme="dark"] .textarea {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-color);
}
[data-theme="dark"] .input:focus,
[data-theme="dark"] .textarea:focus {
border-color: #667eea;
background: var(--bg-secondary);
}
[data-theme="dark"] .label {
color: var(--text-secondary);
}
[data-theme="dark"] .resource-header:hover {
background: var(--hover-bg);
}
[data-theme="dark"] .resource-name,
[data-theme="dark"] h2, [data-theme="dark"] h3, [data-theme="dark"] h4 {
color: var(--text-primary);
}
[data-theme="dark"] .resource-namespace,
[data-theme="dark"] .tagline,
[data-theme="dark"] p {
color: var(--text-muted);
}
[data-theme="dark"] .tabs {
background: var(--bg-primary);
border-bottom-color: var(--border-color);
}
[data-theme="dark"] .tab {
color: var(--text-muted);
}
[data-theme="dark"] .tab:hover {
background: var(--hover-bg);
color: var(--text-secondary);
}
[data-theme="dark"] .tab.active {
background: var(--card-bg);
color: #667eea;
}
[data-theme="dark"] .filters-section,
[data-theme="dark"] .resources-stats {
background: var(--card-bg);
}
[data-theme="dark"] .empty-state,
[data-theme="dark"] .loading-state {
color: var(--text-secondary);
}
[data-theme="dark"] .close-btn {
color: var(--text-muted);
}
[data-theme="dark"] .close-btn:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
[data-theme="dark"] .btn-outline {
border-color: var(--border-color);
color: var(--text-secondary);
}
[data-theme="dark"] .btn-outline:hover:not(:disabled) {
background: var(--hover-bg);
border-color: #667eea;
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
}
.badge-primary {
background: #ebf4ff;
color: #2c5282;
}
.badge-success {
background: #f0fff4;
color: #22543d;
}
.badge-warning {
background: #fffaf0;
color: #744210;
}
.badge-danger {
background: #fff5f5;
color: #742a2a;
}

48
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,48 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ProjectDashboard from './pages/ProjectDashboard';
import RenderView from './pages/RenderView';
import { ThemeProvider, useTheme } from './context/ThemeContext';
import './App.css';
function AppContent() {
const { theme, toggleTheme } = useTheme();
return (
<div className="app">
<header className="app-header">
<div className="header-content">
<div className="header-left">
<h1> HelmView</h1>
<p className="tagline">Visual Helm Chart Editor & Validator</p>
</div>
<button
className="theme-toggle"
onClick={toggleTheme}
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
</div>
</header>
<main className="app-main">
<Routes>
<Route path="/" element={<ProjectDashboard />} />
<Route path="/project/:projectId" element={<RenderView />} />
</Routes>
</main>
</div>
);
}
function App() {
return (
<ThemeProvider>
<Router>
<AppContent />
</Router>
</ThemeProvider>
);
}
export default App;

60
frontend/src/api.js Normal file
View File

@ -0,0 +1,60 @@
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
export const projectsAPI = {
list: () => api.get('/api/projects'),
create: (data) => api.post('/api/projects', data),
get: (id) => api.get(`/api/projects/${id}`),
delete: (id) => api.delete(`/api/projects/${id}`),
upload: (id, file) => {
const formData = new FormData();
formData.append('file', file);
return api.post(`/api/projects/${id}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
},
uploadFromGit: (id, gitUrl, chartPath = '') =>
api.post(`/api/projects/${id}/upload-git`, {
git_url: gitUrl,
chart_path: chartPath
}),
render: (id, options) => api.post(`/api/projects/${id}/render`, options),
getChartInfo: (id) => api.get(`/api/projects/${id}/chart-info`),
fixChartYaml: (id) => api.post(`/api/projects/${id}/fix-chart-yaml`),
getTemplates: (id) => api.get(`/api/projects/${id}/templates`),
getValues: (id) => api.get(`/api/projects/${id}/values`),
getResources: (id) => api.get(`/api/projects/${id}/resources`),
updateResource: (projectId, resourceUid, yamlContent) =>
api.put(`/api/projects/${projectId}/resources/${resourceUid}`, {
yaml_content: yamlContent
}),
export: (id) => api.post(`/api/projects/${id}/export`),
download: (id) => api.get(`/api/projects/${id}/download`, {
responseType: 'blob'
}),
};
export const schemasAPI = {
getSchema: (kind) => api.get(`/api/kubernetes-schemas/${kind}`),
};
export default api;

View File

@ -0,0 +1,141 @@
.changes-modal {
max-width: 1200px;
max-height: 95vh;
display: flex;
flex-direction: column;
}
.changes-modal .modal-body {
max-height: calc(95vh - 140px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.no-changes {
text-align: center;
padding: 4rem 2rem;
}
.no-changes-icon {
font-size: 4rem;
color: #48bb78;
margin-bottom: 1rem;
}
.no-changes h4 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 0.5rem;
}
.no-changes p {
color: #718096;
font-size: 1rem;
}
.changes-info {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.changes-info strong {
display: block;
color: #0c4a6e;
margin-bottom: 0.75rem;
}
.modified-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.modified-badge {
background: #e0f2fe;
color: #075985;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
font-family: 'Monaco', 'Menlo', monospace;
border: 1px solid #bae6fd;
}
/* Apply kind-specific colors to modified badges */
.modified-badge[data-kind="Deployment"] {
background: #ede9fe;
color: #5b21b6;
border-color: #c4b5fd;
}
.modified-badge[data-kind="Service"] {
background: #d1fae5;
color: #065f46;
border-color: #6ee7b7;
}
.modified-badge[data-kind="Ingress"] {
background: #fed7aa;
color: #9a3412;
border-color: #fdba74;
}
.modified-badge[data-kind="ConfigMap"] {
background: #dbeafe;
color: #1e40af;
border-color: #93c5fd;
}
.modified-badge[data-kind="Secret"] {
background: #e9d5ff;
color: #6b21a8;
border-color: #d8b4fe;
}
.changes-viewer {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
margin-bottom: 1rem;
}
/* Allow scroll events to propagate when not in editor area */
.changes-viewer .monaco-editor,
.changes-viewer .monaco-editor .overflow-guard,
.changes-viewer .monaco-editor .view-lines {
pointer-events: auto;
}
.changes-viewer .monaco-editor .monaco-mouse-cursor-text {
pointer-events: auto !important;
}
.viewer-header {
background: #f7fafc;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e8f0;
}
.viewer-label {
font-weight: 600;
color: #2d3748;
font-size: 0.9rem;
}
.changes-hint {
background: #fffaf0;
border: 1px solid #fbd38d;
border-radius: 6px;
padding: 1rem;
color: #744210;
font-size: 0.9rem;
}
.changes-hint strong {
color: #975a16;
}

View File

@ -0,0 +1,112 @@
import { useState, useEffect } from 'react';
import { DiffEditor } from '@monaco-editor/react';
import './AllChangesDiff.css';
function AllChangesDiff({ resources, onClose }) {
const [originalYaml, setOriginalYaml] = useState('');
const [modifiedYaml, setModifiedYaml] = useState('');
const [modifiedResources, setModifiedResources] = useState([]);
useEffect(() => {
// Filter modified resources and combine their YAML
const modified = resources.filter(r => r.modified);
setModifiedResources(modified);
if (modified.length > 0) {
// Combine original and modified YAML separately
const combinedOriginal = modified.map(r => {
const yaml = r.originalYaml || r.rawYaml;
return `# ${r.kind}/${r.name}\n---\n${yaml}`;
}).join('\n\n');
const combinedModified = modified.map(r =>
`# ${r.kind}/${r.name}\n---\n${r.rawYaml}`
).join('\n\n');
setOriginalYaml(combinedOriginal);
setModifiedYaml(combinedModified);
}
}, [resources]);
if (modifiedResources.length === 0) {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal changes-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>All Changes</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
<div className="no-changes">
<div className="no-changes-icon"></div>
<h4>No Changes Made</h4>
<p>All resources match the original Helm template output.</p>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={onClose}>
Close
</button>
</div>
</div>
</div>
);
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal changes-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>All Changes ({modifiedResources.length} resources modified)</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
<div className="changes-info">
<strong>Modified Resources:</strong>
<div className="modified-list">
{modifiedResources.map((r, idx) => (
<span key={idx} className="modified-badge" data-kind={r.kind}>
{r.kind}/{r.name}
</span>
))}
</div>
</div>
<div className="changes-viewer">
<div className="viewer-header">
<span className="viewer-label">Changes comparison (Original Modified):</span>
</div>
<DiffEditor
height="600px"
language="yaml"
original={originalYaml}
modified={modifiedYaml}
theme="vs-light"
options={{
readOnly: true,
automaticLayout: true,
renderSideBySide: true,
scrollBeyondLastLine: false,
minimap: { enabled: true },
originalEditable: false,
}}
/>
</div>
<div className="changes-hint">
<strong>💡 Tip:</strong> To see detailed changes for each resource, open the resource card and click "Show Diff" in the YAML editor.
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={onClose}>
Close
</button>
</div>
</div>
</div>
);
}
export default AllChangesDiff;

View File

@ -0,0 +1,196 @@
.export-modal {
max-width: 700px;
}
.export-info {
text-align: center;
padding: 1rem 0;
}
.info-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.export-info h4 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 1rem;
}
.export-info p {
color: #4a5568;
margin-bottom: 1.5rem;
}
.export-steps {
text-align: left;
max-width: 400px;
margin: 0 auto 1.5rem;
list-style: none;
padding: 0;
}
.export-steps li {
padding: 0.5rem 0;
color: #2d3748;
font-size: 0.95rem;
}
.warning-box {
background: #fffaf0;
border: 1px solid #fbd38d;
border-radius: 6px;
padding: 1rem;
text-align: left;
margin-top: 1.5rem;
}
.warning-box strong {
color: #975a16;
display: block;
margin-bottom: 0.5rem;
}
.warning-box p {
color: #744210;
margin: 0;
font-size: 0.9rem;
}
.export-progress {
text-align: center;
padding: 3rem 2rem;
}
.export-progress h4 {
font-size: 1.25rem;
color: #2d3748;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.export-progress p {
color: #718096;
}
.export-success {
text-align: center;
padding: 1rem 0;
}
.success-icon {
font-size: 4rem;
color: #48bb78;
margin-bottom: 1rem;
}
.export-success h4 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 1.5rem;
}
.export-details {
background: #f7fafc;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
}
.detail-label {
font-weight: 600;
color: #4a5568;
}
.detail-value {
font-family: 'Monaco', 'Menlo', monospace;
color: #2d3748;
}
.download-btn {
width: 100%;
justify-content: center;
margin-top: 1rem;
font-size: 1.1rem;
padding: 1rem;
}
.export-failure {
text-align: center;
padding: 1rem 0;
}
.error-icon {
font-size: 4rem;
color: #f56565;
margin-bottom: 1rem;
}
.export-failure h4 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 1.5rem;
}
.error-details {
text-align: left;
background: #fff5f5;
border: 1px solid #feb2b2;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.error-item {
margin-bottom: 1rem;
}
.error-item:last-child {
margin-bottom: 0;
}
.error-item pre {
color: #742a2a;
font-size: 0.85rem;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
.failure-hint {
color: #718096;
font-size: 0.9rem;
margin-top: 1rem;
}
.lint-output {
text-align: left;
background: #f7fafc;
border: 1px solid #cbd5e0;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.lint-output strong {
color: #2d3748;
display: block;
margin-bottom: 0.5rem;
}
.lint-output pre {
color: #4a5568;
font-size: 0.85rem;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
max-height: 200px;
overflow-y: auto;
}

View File

@ -0,0 +1,170 @@
import { useState } from 'react';
import { projectsAPI } from '../api';
import './ExportModal.css';
function ExportModal({ projectId, onClose }) {
const [exporting, setExporting] = useState(false);
const [exportResult, setExportResult] = useState(null);
const [error, setError] = useState('');
const handleExport = async () => {
setExporting(true);
setError('');
setExportResult(null);
try {
const response = await projectsAPI.export(projectId);
const data = response.data;
if (!data.success) {
setError(data.errors.join('\n') || 'Export failed');
setExportResult(data);
return;
}
setExportResult(data);
} catch (err) {
setError(err.response?.data?.detail || 'Failed to export chart');
} finally {
setExporting(false);
}
};
const handleDownload = async () => {
try {
const response = await projectsAPI.download(projectId);
// Create blob and download
const blob = new Blob([response.data], { type: 'application/gzip' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `chart-${exportResult.version}.tgz`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (err) {
setError('Failed to download chart');
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal export-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Export Helm Chart</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{!exportResult && !exporting && (
<div className="export-info">
<div className="info-icon">📦</div>
<h4>Ready to Export</h4>
<p>
This will create a new Helm chart package with all your modifications.
The process includes:
</p>
<ul className="export-steps">
<li> Apply all modified resources</li>
<li> Update Chart.yaml version</li>
<li> Run helm lint validation</li>
<li> Run helm template validation</li>
<li> Package as .tgz archive</li>
</ul>
<div className="warning-box">
<strong> Important:</strong>
<p>
Make sure all YAML changes are saved before exporting.
The export will fail if any validation errors are found.
</p>
</div>
</div>
)}
{exporting && (
<div className="export-progress">
<div className="loading"></div>
<h4>Exporting Chart...</h4>
<p>Running validation and packaging. This may take a moment.</p>
</div>
)}
{exportResult && exportResult.success && (
<div className="export-success">
<div className="success-icon"></div>
<h4>Export Successful!</h4>
<div className="export-details">
<div className="detail-row">
<span className="detail-label">Version:</span>
<span className="detail-value">{exportResult.version}</span>
</div>
</div>
{exportResult.lint_output && (
<div className="lint-output">
<strong>Helm Lint Output:</strong>
<pre>{exportResult.lint_output}</pre>
</div>
)}
<button className="btn btn-secondary download-btn" onClick={handleDownload}>
📥 Download Chart (.tgz)
</button>
</div>
)}
{exportResult && !exportResult.success && (
<div className="export-failure">
<div className="error-icon"></div>
<h4>Export Failed</h4>
<div className="error-details">
{exportResult.errors.map((err, idx) => (
<div key={idx} className="error-item">
<pre>{err}</pre>
</div>
))}
</div>
{exportResult.lint_output && (
<div className="lint-output">
<strong>Helm Lint Output:</strong>
<pre>{exportResult.lint_output}</pre>
</div>
)}
<p className="failure-hint">
Please fix the errors above and try again.
</p>
</div>
)}
{error && !exportResult && (
<div className="error-box">
<strong>Error:</strong>
<pre>{error}</pre>
</div>
)}
</div>
<div className="modal-footer">
<button className="btn btn-outline" onClick={onClose}>
{exportResult && exportResult.success ? 'Close' : 'Cancel'}
</button>
{!exportResult && (
<button
className="btn btn-primary"
onClick={handleExport}
disabled={exporting}
>
{exporting ? 'Exporting...' : '📦 Export Chart'}
</button>
)}
</div>
</div>
</div>
);
}
export default ExportModal;

View File

@ -0,0 +1,177 @@
.render-modal {
max-width: 700px;
max-height: 90vh;
}
.render-modal .modal-body {
max-height: calc(90vh - 140px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
position: relative;
}
.chart-errors-box {
background: #fff5f5;
border: 2px solid #fc8181;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.chart-errors-box h4 {
color: #c53030;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.chart-errors-box ul {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.chart-errors-box li {
color: #742a2a;
padding: 0.5rem 0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
}
.help-text {
color: #744210;
background: #fffaf0;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0 0.5rem 0;
font-size: 0.9rem;
}
.example-yaml {
background: #2d3748;
color: #e2e8f0;
padding: 1rem;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.85rem;
overflow-x: auto;
margin: 0.5rem 0 1rem 0;
}
.fix-btn {
width: 100%;
justify-content: center;
margin-top: 0.5rem;
}
.templates-info {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.templates-info strong {
display: block;
color: #0c4a6e;
margin-bottom: 0.75rem;
}
.template-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.template-badge {
background: #e0f2fe;
color: #075985;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
font-family: 'Monaco', 'Menlo', monospace;
}
.template-more {
color: #64748b;
font-size: 0.85rem;
font-style: italic;
}
.hint-text {
color: #475569;
font-size: 0.9rem;
margin: 0;
line-height: 1.5;
}
.values-textarea {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.85rem;
line-height: 1.5;
min-height: 300px;
max-height: 400px;
overflow-y: auto;
resize: vertical;
}
.label-hint {
margin-left: 0.5rem;
font-weight: 400;
color: #718096;
font-size: 0.85rem;
}
.error-box {
background: #fff5f5;
border: 1px solid #feb2b2;
border-radius: 6px;
padding: 1rem;
margin-top: 1rem;
}
.error-box strong {
color: #c53030;
display: block;
margin-bottom: 0.5rem;
}
.error-box pre {
color: #742a2a;
font-size: 0.85rem;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
.lint-output {
background: #f7fafc;
border: 1px solid #cbd5e0;
border-radius: 6px;
padding: 1rem;
margin-top: 1rem;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.output-header strong {
color: #2d3748;
}
.lint-output pre {
color: #4a5568;
font-size: 0.85rem;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
max-height: 200px;
overflow-y: auto;
}

View File

@ -0,0 +1,254 @@
import { useState, useEffect } from 'react';
import { projectsAPI } from '../api';
import './RenderSection.css';
function RenderSection({ projectId, onClose, onComplete }) {
const [options, setOptions] = useState({
release_name: 'release',
namespace: 'default',
values_override: '',
set_flags: [],
set_string_flags: [],
});
const [rendering, setRendering] = useState(false);
const [loadingValues, setLoadingValues] = useState(false);
const [chartValid, setChartValid] = useState(true);
const [chartErrors, setChartErrors] = useState([]);
const [templates, setTemplates] = useState([]);
const [error, setError] = useState('');
const [lintOutput, setLintOutput] = useState('');
const [showLintOutput, setShowLintOutput] = useState(false);
// Load default values when component mounts
useEffect(() => {
loadChartInfo();
loadTemplates();
loadDefaultValues();
}, [projectId]);
const loadChartInfo = async () => {
try {
const response = await projectsAPI.getChartInfo(projectId);
setChartValid(response.data.valid);
setChartErrors(response.data.errors || []);
} catch (err) {
console.error('Failed to load chart info:', err);
}
};
const loadTemplates = async () => {
try {
const response = await projectsAPI.getTemplates(projectId);
setTemplates(response.data.templates || []);
} catch (err) {
console.error('Failed to load templates:', err);
}
};
const loadDefaultValues = async () => {
setLoadingValues(true);
try {
const response = await projectsAPI.getValues(projectId);
setOptions(prev => ({ ...prev, values_override: response.data.values }));
} catch (err) {
// Silently fail - not critical
console.error('Failed to load default values:', err);
} finally {
setLoadingValues(false);
}
};
const handleFixChartYaml = async () => {
setLoadingValues(true);
try {
await projectsAPI.fixChartYaml(projectId);
// Reload chart info after fixing
await loadChartInfo();
setError('');
} catch (err) {
setError('Failed to fix Chart.yaml');
} finally {
setLoadingValues(false);
}
};
const handleRender = async () => {
setRendering(true);
setError('');
setLintOutput('');
try {
const response = await projectsAPI.render(projectId, options);
const data = response.data;
setLintOutput(data.lint_output);
if (!data.success) {
setError(data.template_errors.join('\n') || 'Failed to render chart');
setShowLintOutput(true);
return;
}
if (data.lint_errors && data.lint_errors.length > 0) {
setShowLintOutput(true);
}
// Show success info
const resourceCount = data.resources.length;
const templateCount = templates.length;
if (resourceCount < templateCount - 1) {
// Fewer resources than templates (minus helpers)
setError(`⚠️ Warning: Found ${templateCount} templates but only rendered ${resourceCount} resources. Some templates may have conditions that aren't met (like 'enabled: false'). Check the Helm Lint Output below for details.`);
setShowLintOutput(true);
}
onComplete(data.resources);
} catch (err) {
setError(err.response?.data?.detail || 'Failed to render chart');
} finally {
setRendering(false);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal render-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Render Helm Chart</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{/* Chart.yaml validation errors */}
{!chartValid && chartErrors.length > 0 && (
<div className="chart-errors-box">
<h4> Chart.yaml has errors - Fix these before rendering:</h4>
<ul>
{chartErrors.map((err, idx) => (
<li key={idx}>{err}</li>
))}
</ul>
<p className="help-text">
<strong>How to fix:</strong> You need to edit the Chart.yaml file manually.
It should have these required fields:
</p>
<pre className="example-yaml">{`apiVersion: v2
name: my-chart-name
version: 0.1.0
description: A Helm chart for Kubernetes`}</pre>
<button
className="btn btn-secondary fix-btn"
onClick={handleFixChartYaml}
disabled={loadingValues}
>
{loadingValues ? 'Fixing...' : '🔧 Auto-Fix Chart.yaml'}
</button>
</div>
)}
{/* Template files info */}
{templates.length > 0 && (
<div className="templates-info">
<strong>📄 Found {templates.count || templates.length} template files:</strong>
<div className="template-list">
{templates.slice(0, 10).map((t, idx) => (
<span key={idx} className="template-badge">{t.name}</span>
))}
{templates.length > 10 && <span className="template-more">+{templates.length - 10} more</span>}
</div>
<p className="hint-text">
💡 Tip: If not all resources appear, check values.yaml for 'enabled: false'
and change to 'true' to enable features like ingress, autoscaling, etc.
</p>
</div>
)}
<div className="form-group">
<label className="label">Release Name</label>
<input
type="text"
className="input"
value={options.release_name}
onChange={(e) => setOptions({ ...options, release_name: e.target.value })}
placeholder="release"
/>
</div>
<div className="form-group">
<label className="label">Namespace</label>
<input
type="text"
className="input"
value={options.namespace}
onChange={(e) => setOptions({ ...options, namespace: e.target.value })}
placeholder="default"
/>
</div>
<div className="form-group">
<label className="label">
Values Override (YAML)
<span className="label-hint">
{loadingValues ? 'Loading default values...' : 'Edit values below - ##OPTIONAL fields can be removed'}
</span>
</label>
<textarea
className="textarea values-textarea"
value={options.values_override}
onChange={(e) => setOptions({ ...options, values_override: e.target.value })}
placeholder="key: value&#10;nested:&#10; key: value"
rows="12"
disabled={loadingValues}
/>
</div>
{error && (
<div className="error-box">
<strong>Error:</strong>
<pre>{error}</pre>
</div>
)}
{showLintOutput && lintOutput && (
<div className="lint-output">
<div className="output-header">
<strong>Helm Lint Output:</strong>
<button
className="btn-small btn-outline"
onClick={() => setShowLintOutput(false)}
>
Hide
</button>
</div>
<pre>{lintOutput}</pre>
</div>
)}
</div>
<div className="modal-footer">
<button className="btn btn-outline" onClick={onClose} disabled={rendering}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleRender}
disabled={rendering || !chartValid}
title={!chartValid ? "Fix Chart.yaml errors first" : ""}
>
{rendering ? (
<>
<div className="loading"></div>
Rendering...
</>
) : (
'🔄 Render Chart'
)}
</button>
</div>
</div>
</div>
);
}
export default RenderSection;

View File

@ -0,0 +1,258 @@
.resource-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.2s;
overflow: hidden;
}
.resource-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.resource-card.expanded {
grid-column: 1 / -1;
}
.resource-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem;
cursor: pointer;
user-select: none;
}
.resource-header:hover {
background: #f7fafc;
}
.resource-info {
flex: 1;
}
.resource-kind {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.kind-badge {
display: inline-block;
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 600;
}
/* Different colors for different resource kinds */
.kind-badge[data-kind="Deployment"],
.resource-card[data-kind="Deployment"] .kind-badge {
background: #667eea;
}
.kind-badge[data-kind="Service"],
.resource-card[data-kind="Service"] .kind-badge {
background: #48bb78;
}
.kind-badge[data-kind="Ingress"],
.resource-card[data-kind="Ingress"] .kind-badge {
background: #ed8936;
}
.kind-badge[data-kind="ConfigMap"],
.resource-card[data-kind="ConfigMap"] .kind-badge {
background: #4299e1;
}
.kind-badge[data-kind="Secret"],
.resource-card[data-kind="Secret"] .kind-badge {
background: #9f7aea;
}
.kind-badge[data-kind="Pod"],
.resource-card[data-kind="Pod"] .kind-badge {
background: #38b2ac;
}
.kind-badge[data-kind="StatefulSet"],
.resource-card[data-kind="StatefulSet"] .kind-badge {
background: #805ad5;
}
.kind-badge[data-kind="DaemonSet"],
.resource-card[data-kind="DaemonSet"] .kind-badge {
background: #d53f8c;
}
.kind-badge[data-kind="Job"],
.resource-card[data-kind="Job"] .kind-badge {
background: #dd6b20;
}
.kind-badge[data-kind="CronJob"],
.resource-card[data-kind="CronJob"] .kind-badge {
background: #c05621;
}
.kind-badge[data-kind="PersistentVolumeClaim"],
.resource-card[data-kind="PersistentVolumeClaim"] .kind-badge {
background: #319795;
}
.kind-badge[data-kind="ServiceAccount"],
.resource-card[data-kind="ServiceAccount"] .kind-badge {
background: #718096;
}
.kind-badge[data-kind="Role"],
.kind-badge[data-kind="RoleBinding"],
.kind-badge[data-kind="ClusterRole"],
.kind-badge[data-kind="ClusterRoleBinding"],
.resource-card[data-kind="Role"] .kind-badge,
.resource-card[data-kind="RoleBinding"] .kind-badge,
.resource-card[data-kind="ClusterRole"] .kind-badge,
.resource-card[data-kind="ClusterRoleBinding"] .kind-badge {
background: #2d3748;
}
.kind-badge[data-kind="HorizontalPodAutoscaler"],
.resource-card[data-kind="HorizontalPodAutoscaler"] .kind-badge {
background: #e53e3e;
}
.resource-name {
font-size: 1.1rem;
color: #2d3748;
margin: 0.25rem 0;
word-break: break-word;
}
.resource-namespace {
color: #718096;
font-size: 0.85rem;
margin: 0.25rem 0 0 0;
font-family: 'Monaco', 'Menlo', monospace;
}
.expand-btn {
background: #ebf4ff;
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.5rem;
color: #667eea;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.expand-btn:hover {
background: #667eea;
color: white;
}
.resource-content {
border-top: 1px solid #e2e8f0;
}
/* Tabs */
.tabs {
display: flex;
background: #f7fafc;
border-bottom: 1px solid #e2e8f0;
}
.tab {
flex: 1;
padding: 0.75rem 1rem;
background: none;
border: none;
font-size: 0.95rem;
font-weight: 500;
color: #718096;
cursor: pointer;
transition: all 0.2s;
border-bottom: 2px solid transparent;
}
.tab:hover {
background: #edf2f7;
color: #2d3748;
}
.tab.active {
color: #667eea;
border-bottom-color: #667eea;
background: white;
}
.tab-content {
padding: 1.5rem;
max-height: 600px;
overflow-y: auto;
overscroll-behavior: contain;
}
/* Summary Tab */
.summary-tab {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.summary-row {
display: flex;
padding: 0.5rem 0;
border-bottom: 1px solid #f7fafc;
}
.summary-label {
font-weight: 600;
color: #4a5568;
min-width: 120px;
}
.summary-value {
color: #2d3748;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
}
.summary-section {
margin-top: 1rem;
}
.summary-section h5 {
font-size: 0.95rem;
color: #4a5568;
margin-bottom: 0.75rem;
font-weight: 600;
}
.label-item {
background: #f7fafc;
padding: 0.5rem 0.75rem;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.label-item code {
font-size: 0.85rem;
color: #2d3748;
}
/* YAML Tab */
.yaml-tab {
padding: 0;
max-height: none;
overflow: visible;
}

View File

@ -0,0 +1,107 @@
import { useState } from 'react';
import YamlEditor from './YamlEditor';
import './ResourceCard.css';
function ResourceCard({ resource, projectId, onUpdate, onClick }) {
const [isExpanded, setIsExpanded] = useState(false);
const [activeTab, setActiveTab] = useState('summary');
const toggleExpand = () => {
setIsExpanded(!isExpanded);
if (!isExpanded) {
onClick && onClick();
}
};
return (
<div className={`resource-card ${isExpanded ? 'expanded' : ''}`} data-kind={resource.kind}>
{/* Card Header */}
<div className="resource-header" onClick={toggleExpand}>
<div className="resource-info">
<div className="resource-kind">
<span className="kind-badge" data-kind={resource.kind}>{resource.kind}</span>
{resource.modified && <span className="badge badge-warning">Modified</span>}
</div>
<h4 className="resource-name">{resource.name}</h4>
{resource.namespace && (
<p className="resource-namespace">ns: {resource.namespace}</p>
)}
</div>
<button className="expand-btn">
{isExpanded ? '' : '+'}
</button>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="resource-content">
{/* Tabs */}
<div className="tabs">
<button
className={`tab ${activeTab === 'summary' ? 'active' : ''}`}
onClick={() => setActiveTab('summary')}
>
Summary
</button>
<button
className={`tab ${activeTab === 'yaml' ? 'active' : ''}`}
onClick={() => setActiveTab('yaml')}
>
YAML
</button>
</div>
{/* Tab Content */}
<div className="tab-content">
{activeTab === 'summary' && (
<div className="summary-tab">
<div className="summary-row">
<span className="summary-label">API Version:</span>
<span className="summary-value">{resource.apiVersion}</span>
</div>
<div className="summary-row">
<span className="summary-label">Kind:</span>
<span className="summary-value">{resource.kind}</span>
</div>
<div className="summary-row">
<span className="summary-label">Name:</span>
<span className="summary-value">{resource.name}</span>
</div>
{resource.namespace && (
<div className="summary-row">
<span className="summary-label">Namespace:</span>
<span className="summary-value">{resource.namespace}</span>
</div>
)}
{resource.labels && Object.keys(resource.labels).length > 0 && (
<div className="summary-section">
<h5>Labels</h5>
{Object.entries(resource.labels).map(([key, value]) => (
<div key={key} className="label-item">
<code>{key}: {value}</code>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'yaml' && (
<div className="yaml-tab">
<YamlEditor
value={resource.rawYaml}
resourceKind={resource.kind}
onChange={(newYaml) => onUpdate(resource.uid, newYaml)}
projectId={projectId}
resourceUid={resource.uid}
/>
</div>
)}
</div>
</div>
)}
</div>
);
}
export default ResourceCard;

View File

@ -0,0 +1,153 @@
.upload-modal {
max-width: 600px;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding: 1rem 1.5rem;
border-top: 1px solid #e2e8f0;
}
.dropzone {
border: 2px dashed #cbd5e0;
border-radius: 8px;
padding: 3rem 2rem;
text-align: center;
transition: all 0.2s;
background: #f7fafc;
cursor: pointer;
}
.dropzone:hover {
border-color: #667eea;
background: #ebf4ff;
}
.dropzone.active {
border-color: #667eea;
background: #ebf4ff;
transform: scale(1.02);
}
.dropzone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.upload-icon {
font-size: 3rem;
}
.dropzone-text {
font-size: 1rem;
color: #2d3748;
font-weight: 500;
margin: 0;
}
.dropzone-hint {
color: #718096;
font-size: 0.9rem;
margin: 0;
}
.file-info {
margin-top: 1.5rem;
padding: 1rem;
background: #f7fafc;
border-radius: 6px;
}
.info-text {
margin: 0.5rem 0;
color: #4a5568;
font-size: 0.9rem;
}
.info-text strong {
color: #2d3748;
}
.upload-mode-selector {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
background: #f7fafc;
padding: 0.5rem;
border-radius: 8px;
}
.mode-btn {
flex: 1;
padding: 0.75rem 1rem;
background: transparent;
border: 2px solid transparent;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
color: #4a5568;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn:hover {
background: white;
color: #2d3748;
}
.mode-btn.active {
background: white;
border-color: #667eea;
color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.git-input-section {
background: #f7fafc;
border-radius: 8px;
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.label-hint {
margin-left: 0.5rem;
font-weight: 400;
color: #718096;
font-size: 0.85rem;
}
.git-info {
margin-top: 1.5rem;
padding: 1rem;
background: white;
border-radius: 6px;
border: 1px solid #e2e8f0;
}
.git-examples {
margin: 0.5rem 0 1rem 0;
padding-left: 1.5rem;
color: #4a5568;
font-size: 0.85rem;
font-family: 'Monaco', 'Menlo', monospace;
}
.git-examples li {
margin: 0.25rem 0;
word-break: break-all;
}

View File

@ -0,0 +1,222 @@
import { useState } from 'react';
import { projectsAPI } from '../api';
import './UploadSection.css';
function UploadSection({ projectId, onClose, onComplete }) {
const [selectedFile, setSelectedFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState('');
const [dragActive, setDragActive] = useState(false);
const [uploadMode, setUploadMode] = useState('file'); // 'file' or 'git'
const [gitUrl, setGitUrl] = useState('');
const [gitPath, setGitPath] = useState(''); // Optional path within repo
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
setSelectedFile(e.dataTransfer.files[0]);
}
};
const handleFileChange = (e) => {
if (e.target.files && e.target.files[0]) {
setSelectedFile(e.target.files[0]);
}
};
const handleUpload = async () => {
if (uploadMode === 'file') {
if (!selectedFile) {
setError('Please select a file');
return;
}
// Validate file type
const validExtensions = ['.tgz', '.tar.gz', '.zip'];
const fileName = selectedFile.name.toLowerCase();
const isValid = validExtensions.some(ext => fileName.endsWith(ext));
if (!isValid) {
setError('Invalid file type. Please upload .tgz, .tar.gz, or .zip file');
return;
}
setUploading(true);
setError('');
try {
await projectsAPI.upload(projectId, selectedFile);
onComplete();
} catch (err) {
setError(err.response?.data?.detail || 'Failed to upload chart');
} finally {
setUploading(false);
}
} else {
// Git mode
if (!gitUrl.trim()) {
setError('Please enter a Git repository URL');
return;
}
setUploading(true);
setError('');
try {
await projectsAPI.uploadFromGit(projectId, gitUrl.trim(), gitPath.trim());
onComplete();
} catch (err) {
setError(err.response?.data?.detail || 'Failed to download chart from Git');
} finally {
setUploading(false);
}
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal upload-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Upload Helm Chart</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{/* Mode Selector */}
<div className="upload-mode-selector">
<button
className={`mode-btn ${uploadMode === 'file' ? 'active' : ''}`}
onClick={() => setUploadMode('file')}
>
📁 Upload File
</button>
<button
className={`mode-btn ${uploadMode === 'git' ? 'active' : ''}`}
onClick={() => setUploadMode('git')}
>
🔗 From Git
</button>
</div>
{uploadMode === 'file' ? (
<>
<div
className={`dropzone ${dragActive ? 'active' : ''}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<div className="dropzone-content">
<div className="upload-icon">📤</div>
<p className="dropzone-text">
{selectedFile ? selectedFile.name : 'Drag and drop your Helm chart here'}
</p>
<p className="dropzone-hint">or</p>
<label className="btn btn-outline">
Browse Files
<input
type="file"
accept=".tgz,.tar.gz,.zip"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</label>
</div>
</div>
<div className="file-info">
<p className="info-text">
<strong>Supported formats:</strong> .tgz, .tar.gz, .zip
</p>
<p className="info-text">
<strong>Max size:</strong> 100 MB
</p>
</div>
</>
) : (
<>
<div className="git-input-section">
<div className="form-group">
<label className="label">Git Repository URL *</label>
<input
type="text"
className="input"
placeholder="https://github.com/user/repo or git@github.com:user/repo.git"
value={gitUrl}
onChange={(e) => setGitUrl(e.target.value)}
/>
</div>
<div className="form-group">
<label className="label">
Chart Path (optional)
<span className="label-hint">Path to chart directory within repo</span>
</label>
<input
type="text"
className="input"
placeholder="charts/my-chart or leave empty if in root"
value={gitPath}
onChange={(e) => setGitPath(e.target.value)}
/>
</div>
<div className="git-info">
<p className="info-text">
<strong>Examples:</strong>
</p>
<ul className="git-examples">
<li>https://github.com/helm/charts/tree/master/stable/nginx</li>
<li>https://gitlab.com/user/helm-charts.git</li>
<li>https://bitbucket.org/user/charts.git</li>
</ul>
<p className="info-text">
<strong>Note:</strong> Public repositories only. Private repos require authentication.
</p>
</div>
</div>
</>
)}
{error && <div className="error">{error}</div>}
</div>
<div className="modal-footer">
<button className="btn btn-outline" onClick={onClose} disabled={uploading}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleUpload}
disabled={(uploadMode === 'file' && !selectedFile) || (uploadMode === 'git' && !gitUrl.trim()) || uploading}
>
{uploading ? (
<>
<div className="loading"></div>
{uploadMode === 'file' ? 'Uploading...' : 'Downloading...'}
</>
) : (
uploadMode === 'file' ? 'Upload Chart' : 'Download from Git'
)}
</button>
</div>
</div>
</div>
);
}
export default UploadSection;

View File

@ -0,0 +1,123 @@
.yaml-editor-container {
background: white;
border-radius: 8px;
overflow: hidden;
}
.editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: #f7fafc;
border-bottom: 1px solid #e2e8f0;
gap: 1rem;
flex-wrap: wrap;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.toolbar-right {
display: flex;
gap: 0.75rem;
}
.editor-title {
font-weight: 600;
color: #2d3748;
font-size: 1rem;
}
.validation-error {
color: #f56565;
font-size: 0.9rem;
font-weight: 500;
}
.validation-success {
color: #48bb78;
font-size: 0.9rem;
font-weight: 500;
}
.btn-small {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-small:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-small.btn-primary {
background: #667eea;
color: white;
}
.btn-small.btn-primary:hover:not(:disabled) {
background: #5568d3;
}
.btn-small.btn-outline {
background: transparent;
border: 1px solid #cbd5e0;
color: #4a5568;
}
.btn-small.btn-outline:hover:not(:disabled) {
background: #f7fafc;
border-color: #667eea;
color: #667eea;
}
.editor-wrapper {
border-bottom: 1px solid #e2e8f0;
}
/* Allow scroll events to propagate to parent when Monaco editor doesn't need them */
.editor-wrapper .monaco-editor,
.editor-wrapper .monaco-editor .overflow-guard,
.editor-wrapper .monaco-editor .view-lines {
pointer-events: auto;
}
/* Ensure Monaco editor area allows scroll propagation */
.editor-wrapper .monaco-editor .monaco-mouse-cursor-text {
pointer-events: auto !important;
}
.editor-footer {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
.editor-footer.error {
background: #fff5f5;
color: #c53030;
border-left: 4px solid #f56565;
}
.editor-footer.warning {
background: #fffaf0;
color: #975a16;
border-left: 4px solid #ed8936;
}
.editor-footer.info {
background: #ebf8ff;
color: #2c5282;
border-left: 4px solid #4299e1;
}
.editor-footer strong {
margin-right: 0.5rem;
}

View File

@ -0,0 +1,280 @@
import { useState, useRef, useEffect } from 'react';
import Editor, { DiffEditor } from '@monaco-editor/react';
import yaml from 'js-yaml';
import { projectsAPI } from '../api';
import './YamlEditor.css';
function YamlEditor({ value, resourceKind, onChange, projectId, resourceUid }) {
const [editorValue, setEditorValue] = useState(value);
const [originalValue] = useState(value); // Store original for diff
const [validationError, setValidationError] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [showDiff, setShowDiff] = useState(false);
const editorRef = useRef(null);
useEffect(() => {
setEditorValue(value);
}, [value]);
const handleEditorDidMount = (editor, monaco) => {
editorRef.current = editor;
// Configure YAML language
monaco.languages.yaml = monaco.languages.yaml || {};
// Register completion provider for autocomplete
monaco.languages.registerCompletionItemProvider('yaml', {
provideCompletionItems: (model, position) => {
const suggestions = getKubernetesSuggestions(resourceKind, monaco);
return { suggestions };
},
});
// Configure editor options
editor.updateOptions({
minimap: { enabled: true },
scrollBeyondLastLine: false,
fontSize: 13,
lineNumbers: 'on',
rulers: [80],
wordWrap: 'on',
folding: true,
});
};
const getKubernetesSuggestions = (kind, monaco) => {
const suggestions = [];
const { CompletionItemKind } = monaco.languages;
// Common Kubernetes fields
const commonFields = [
{ label: 'apiVersion', detail: 'API version', insertText: 'apiVersion: ' },
{ label: 'kind', detail: 'Resource kind', insertText: 'kind: ' },
{ label: 'metadata', detail: 'Resource metadata', insertText: 'metadata:\n name: \n namespace: ' },
{ label: 'name', detail: 'Resource name', insertText: 'name: ' },
{ label: 'namespace', detail: 'Namespace', insertText: 'namespace: ' },
{ label: 'labels', detail: 'Labels', insertText: 'labels:\n ' },
{ label: 'annotations', detail: 'Annotations', insertText: 'annotations:\n ' },
];
// Kind-specific suggestions
const kindSpecificFields = getKindSpecificFields(kind);
[...commonFields, ...kindSpecificFields].forEach(field => {
suggestions.push({
label: field.label,
kind: CompletionItemKind.Property,
detail: field.detail,
insertText: field.insertText,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
});
});
return suggestions;
};
const getKindSpecificFields = (kind) => {
const specs = {
Deployment: [
{ label: 'spec', detail: 'Deployment spec', insertText: 'spec:\n replicas: 1\n selector:\n matchLabels:\n ${1:app}: ${2:myapp}\n template:\n metadata:\n labels:\n ${1:app}: ${2:myapp}\n spec:\n containers:\n - name: ${3:container}\n image: ${4:image:tag}' },
{ label: 'replicas', detail: 'Number of replicas', insertText: 'replicas: ${1:1}' },
{ label: 'selector', detail: 'Label selector', insertText: 'selector:\n matchLabels:\n ${1:app}: ${2:myapp}' },
{ label: 'template', detail: 'Pod template', insertText: 'template:\n metadata:\n labels:\n ${1:app}: ${2:myapp}\n spec:\n containers:\n - name: ${3:container}\n image: ${4:image:tag}' },
],
Service: [
{ label: 'spec', detail: 'Service spec', insertText: 'spec:\n type: ${1:ClusterIP}\n ports:\n - port: ${2:80}\n targetPort: ${3:8080}\n selector:\n ${4:app}: ${5:myapp}' },
{ label: 'type', detail: 'Service type', insertText: 'type: ${1|ClusterIP,NodePort,LoadBalancer|}' },
{ label: 'ports', detail: 'Service ports', insertText: 'ports:\n- port: ${1:80}\n targetPort: ${2:8080}\n protocol: TCP' },
],
ConfigMap: [
{ label: 'data', detail: 'ConfigMap data', insertText: 'data:\n ${1:key}: ${2:value}' },
{ label: 'binaryData', detail: 'Binary data', insertText: 'binaryData:\n ${1:key}: ${2:base64data}' },
],
Secret: [
{ label: 'type', detail: 'Secret type', insertText: 'type: ${1:Opaque}' },
{ label: 'data', detail: 'Secret data (base64)', insertText: 'data:\n ${1:key}: ${2:base64value}' },
{ label: 'stringData', detail: 'Secret data (plain)', insertText: 'stringData:\n ${1:key}: ${2:value}' },
],
Ingress: [
{ label: 'spec', detail: 'Ingress spec', insertText: 'spec:\n rules:\n - host: ${1:example.com}\n http:\n paths:\n - path: ${2:/}\n pathType: ${3:Prefix}\n backend:\n service:\n name: ${4:service}\n port:\n number: ${5:80}' },
],
};
return specs[kind] || [];
};
const validateYaml = (content) => {
try {
// Parse YAML
const parsed = yaml.load(content);
// Basic Kubernetes resource validation
if (!parsed || typeof parsed !== 'object') {
return 'Invalid YAML: must be an object';
}
if (!parsed.apiVersion) {
return 'Missing required field: apiVersion';
}
if (!parsed.kind) {
return 'Missing required field: kind';
}
if (!parsed.metadata || !parsed.metadata.name) {
return 'Missing required field: metadata.name';
}
return null; // Valid
} catch (err) {
return `YAML Syntax Error: ${err.message}`;
}
};
const handleEditorChange = (newValue) => {
setEditorValue(newValue);
setSaveSuccess(false);
// Validate on change
const error = validateYaml(newValue);
setValidationError(error);
};
const handleSave = async () => {
const error = validateYaml(editorValue);
if (error) {
setValidationError(error);
return;
}
setIsSaving(true);
setValidationError(null);
try {
await projectsAPI.updateResource(projectId, resourceUid, editorValue);
onChange(editorValue);
setSaveSuccess(true);
setShowDiff(false); // Switch back to editor after save
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err) {
setValidationError(err.response?.data?.detail || 'Failed to save changes');
} finally {
setIsSaving(false);
}
};
const handleFormat = () => {
try {
const parsed = yaml.load(editorValue);
const formatted = yaml.dump(parsed, {
indent: 2,
lineWidth: -1,
noRefs: true,
});
setEditorValue(formatted);
editorRef.current?.setValue(formatted);
} catch (err) {
setValidationError('Cannot format invalid YAML');
}
};
const hasChanges = editorValue !== value;
return (
<div className="yaml-editor-container">
<div className="editor-toolbar">
<div className="toolbar-left">
<span className="editor-title">Edit YAML</span>
{validationError && (
<span className="validation-error"> {validationError}</span>
)}
{saveSuccess && (
<span className="validation-success"> Saved successfully</span>
)}
</div>
<div className="toolbar-right">
{hasChanges && (
<button
className={`btn-small ${showDiff ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setShowDiff(!showDiff)}
title={showDiff ? "Switch to editor" : "Show changes diff"}
>
{showDiff ? '📝 Editor' : '🔍 Show Diff'}
</button>
)}
<button
className="btn-small btn-outline"
onClick={handleFormat}
disabled={isSaving || showDiff}
>
Format
</button>
<button
className="btn-small btn-primary"
onClick={handleSave}
disabled={!hasChanges || !!validationError || isSaving}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
<div className="editor-wrapper">
{showDiff ? (
<DiffEditor
height="500px"
language="yaml"
original={originalValue}
modified={editorValue}
theme="vs-light"
options={{
readOnly: false,
automaticLayout: true,
renderSideBySide: true,
scrollBeyondLastLine: false,
minimap: { enabled: false },
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
) : (
<Editor
height="500px"
defaultLanguage="yaml"
value={editorValue}
onChange={handleEditorChange}
onMount={handleEditorDidMount}
theme="vs-light"
options={{
readOnly: false,
automaticLayout: true,
}}
/>
)}
</div>
{validationError && (
<div className="editor-footer error">
<strong>Validation Error:</strong> {validationError}
</div>
)}
{!validationError && hasChanges && !showDiff && (
<div className="editor-footer warning">
<strong>Unsaved Changes:</strong> Remember to save your changes before exporting.
</div>
)}
{showDiff && hasChanges && (
<div className="editor-footer info">
<strong>Diff View:</strong> Red = Original (removed), Green = Modified (added). Switch to Editor to make changes.
</div>
)}
</div>
);
}
export default YamlEditor;

View File

@ -0,0 +1,34 @@
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
// Get theme from localStorage or default to 'light'
return localStorage.getItem('helmview-theme') || 'light';
});
useEffect(() => {
// Apply theme to document
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('helmview-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

42
frontend/src/index.css Normal file
View File

@ -0,0 +1,42 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f5f7fa;
color: #2c3e50;
}
code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
#root {
min-height: 100vh;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1,220 @@
.dashboard-container {
width: 100%;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.dashboard-header h2 {
font-size: 1.75rem;
color: #2d3748;
margin-bottom: 0.25rem;
}
.subtitle {
color: #718096;
font-size: 0.95rem;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.project-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.2s;
display: flex;
flex-direction: column;
gap: 1rem;
}
.project-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.project-header h3 {
font-size: 1.25rem;
color: #2d3748;
margin: 0;
word-break: break-word;
}
.project-description {
color: #718096;
font-size: 0.9rem;
line-height: 1.5;
}
.chart-name {
color: #4a5568;
font-size: 0.9rem;
}
.project-meta {
color: #a0aec0;
font-size: 0.85rem;
margin-top: auto;
}
.project-actions {
display: flex;
gap: 0.75rem;
padding-top: 0.5rem;
border-top: 1px solid #e2e8f0;
}
.project-actions .btn {
flex: 1;
justify-content: center;
padding: 0.6rem 1rem;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 0.5rem;
}
.empty-state p {
color: #718096;
margin-bottom: 1.5rem;
}
.loading-state {
text-align: center;
padding: 4rem 2rem;
}
.loading-state p {
margin-top: 1rem;
color: #718096;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h3 {
font-size: 1.25rem;
color: #2d3748;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
color: #a0aec0;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: #f7fafc;
color: #2d3748;
}
.modal-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 1.5rem;
max-height: calc(90vh - 140px);
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
position: relative;
}
.modal form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.modal-footer {
padding: 1.5rem;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 1rem;
justify-content: flex-end;
background: white;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}

View File

@ -0,0 +1,181 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { projectsAPI } from '../api';
import './ProjectDashboard.css';
function ProjectDashboard() {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newProject, setNewProject] = useState({ name: '', description: '' });
const [error, setError] = useState('');
const navigate = useNavigate();
useEffect(() => {
loadProjects();
}, []);
const loadProjects = async () => {
try {
const response = await projectsAPI.list();
setProjects(response.data);
} catch (err) {
setError('Failed to load projects');
} finally {
setLoading(false);
}
};
const handleCreateProject = async (e) => {
e.preventDefault();
setError('');
try {
const response = await projectsAPI.create(newProject);
setProjects([...projects, response.data]);
setShowCreateModal(false);
setNewProject({ name: '', description: '' });
navigate(`/project/${response.data.id}`);
} catch (err) {
setError('Failed to create project');
}
};
const handleDeleteProject = async (id) => {
if (!window.confirm('Are you sure you want to delete this project?')) {
return;
}
try {
await projectsAPI.delete(id);
setProjects(projects.filter(p => p.id !== id));
} catch (err) {
setError('Failed to delete project');
}
};
if (loading) {
return (
<div className="dashboard-container">
<div className="loading-state">
<div className="loading"></div>
<p>Loading projects...</p>
</div>
</div>
);
}
return (
<div className="dashboard-container">
<div className="dashboard-header">
<div>
<h2>Projects</h2>
<p className="subtitle">Manage your Helm chart projects</p>
</div>
<button className="btn btn-primary" onClick={() => setShowCreateModal(true)}>
+ Create Project
</button>
</div>
{error && <div className="error">{error}</div>}
{projects.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">📦</div>
<h3>No projects yet</h3>
<p>Create your first project to start editing Helm charts</p>
<button className="btn btn-primary" onClick={() => setShowCreateModal(true)}>
Create Your First Project
</button>
</div>
) : (
<div className="projects-grid">
{projects.map(project => (
<div key={project.id} className="project-card">
<div className="project-header">
<h3>{project.name}</h3>
{project.chart_uploaded && (
<span className="badge badge-success">Chart Uploaded</span>
)}
</div>
{project.description && (
<p className="project-description">{project.description}</p>
)}
{project.chart_name && (
<p className="chart-name">Chart: <strong>{project.chart_name}</strong></p>
)}
<div className="project-meta">
<small>Created {new Date(project.created_at).toLocaleDateString()}</small>
</div>
<div className="project-actions">
<button
className="btn btn-primary"
onClick={() => navigate(`/project/${project.id}`)}
>
Open
</button>
<button
className="btn btn-danger"
onClick={() => handleDeleteProject(project.id)}
>
Delete
</button>
</div>
</div>
))}
</div>
)}
{showCreateModal && (
<div className="modal-overlay" onClick={() => setShowCreateModal(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Create New Project</h3>
<button className="close-btn" onClick={() => setShowCreateModal(false)}>×</button>
</div>
<form onSubmit={handleCreateProject}>
<div className="form-group">
<label className="label">Project Name *</label>
<input
type="text"
className="input"
value={newProject.name}
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
required
placeholder="my-helm-project"
/>
</div>
<div className="form-group">
<label className="label">Description</label>
<textarea
className="textarea"
value={newProject.description}
onChange={(e) => setNewProject({ ...newProject, description: e.target.value })}
placeholder="Optional project description"
rows="3"
/>
</div>
<div className="modal-actions">
<button type="button" className="btn btn-outline" onClick={() => setShowCreateModal(false)}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Create Project
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
export default ProjectDashboard;

View File

@ -0,0 +1,146 @@
.render-container {
width: 100%;
}
.render-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.back-btn {
background: none;
border: none;
color: #667eea;
font-size: 1rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: all 0.2s;
}
.back-btn:hover {
background: #ebf4ff;
}
.render-header h2 {
font-size: 1.75rem;
color: #2d3748;
margin: 0;
}
.chart-label {
color: #718096;
font-size: 0.9rem;
margin-top: 0.25rem;
}
.header-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
/* Filters */
.filters-section {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.filters-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.filter-group {
display: flex;
flex-direction: column;
}
/* Resources Stats */
.resources-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.stat {
background: white;
border-radius: 8px;
padding: 1.25rem 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex: 1;
text-align: center;
}
.stat-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.25rem;
}
.stat-label {
display: block;
font-size: 0.9rem;
color: #718096;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Resources Grid */
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 0.5rem;
}
.empty-state p {
color: #718096;
margin-bottom: 1.5rem;
}
.loading-state {
text-align: center;
padding: 4rem 2rem;
}
.loading-state p {
margin-top: 1rem;
color: #718096;
}

View File

@ -0,0 +1,319 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { projectsAPI } from '../api';
import ResourceCard from '../components/ResourceCard';
import UploadSection from '../components/UploadSection';
import RenderSection from '../components/RenderSection';
import ExportModal from '../components/ExportModal';
import AllChangesDiff from '../components/AllChangesDiff';
import './RenderView.css';
function RenderView() {
const { projectId } = useParams();
const navigate = useNavigate();
const [project, setProject] = useState(null);
const [resources, setResources] = useState([]);
const [filteredResources, setFilteredResources] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [selectedResource, setSelectedResource] = useState(null);
// Filter states
const [kindFilter, setKindFilter] = useState('');
const [nameFilter, setNameFilter] = useState('');
const [namespaceFilter, setNamespaceFilter] = useState('');
// View states
const [showUpload, setShowUpload] = useState(false);
const [showRender, setShowRender] = useState(false);
const [showExport, setShowExport] = useState(false);
const [showAllChanges, setShowAllChanges] = useState(false);
useEffect(() => {
loadProject();
}, [projectId]);
useEffect(() => {
applyFilters();
}, [resources, kindFilter, nameFilter, namespaceFilter]);
const loadProject = async () => {
try {
const response = await projectsAPI.get(projectId);
setProject(response.data);
if (response.data.chart_uploaded) {
setShowRender(true);
} else {
setShowUpload(true);
}
} catch (err) {
setError('Failed to load project');
} finally {
setLoading(false);
}
};
const handleUploadComplete = () => {
loadProject();
setShowUpload(false);
setShowRender(true);
};
const handleRenderComplete = (renderedResources) => {
// Add originalYaml to each resource for diff comparison
const resourcesWithOriginal = renderedResources.map(r => ({
...r,
originalYaml: r.rawYaml, // Store original for diff
}));
setResources(resourcesWithOriginal);
setShowRender(false);
};
const applyFilters = () => {
let filtered = [...resources];
if (kindFilter) {
filtered = filtered.filter(r => r.kind === kindFilter);
}
if (nameFilter) {
filtered = filtered.filter(r =>
r.name.toLowerCase().includes(nameFilter.toLowerCase())
);
}
if (namespaceFilter) {
filtered = filtered.filter(r =>
r.namespace && r.namespace.toLowerCase().includes(namespaceFilter.toLowerCase())
);
}
setFilteredResources(filtered);
};
const clearFilters = () => {
setKindFilter('');
setNameFilter('');
setNamespaceFilter('');
};
const handleResourceUpdate = (uid, newYaml) => {
setResources(resources.map(r =>
r.uid === uid ? { ...r, rawYaml: newYaml, modified: true } : r
));
};
// Get unique kinds for filter dropdown
const uniqueKinds = [...new Set(resources.map(r => r.kind))].sort();
const hasFilters = kindFilter || nameFilter || namespaceFilter;
if (loading) {
return (
<div className="render-container">
<div className="loading-state">
<div className="loading"></div>
<p>Loading project...</p>
</div>
</div>
);
}
if (!project) {
return (
<div className="render-container">
<div className="error">Project not found</div>
<button className="btn btn-primary" onClick={() => navigate('/')}>
Back to Projects
</button>
</div>
);
}
return (
<div className="render-container">
{/* Header */}
<div className="render-header">
<div className="header-left">
<button className="back-btn" onClick={() => navigate('/')}>
Back
</button>
<div>
<h2>{project.name}</h2>
{project.chart_name && (
<p className="chart-label">Chart: <strong>{project.chart_name}</strong></p>
)}
</div>
</div>
<div className="header-actions">
{project.chart_uploaded && (
<>
<button className="btn btn-outline" onClick={() => setShowRender(true)}>
🔄 Re-render
</button>
{resources.length > 0 && (
<>
{resources.filter(r => r.modified).length > 0 && (
<button className="btn btn-outline" onClick={() => setShowAllChanges(true)}>
🔍 View All Changes
</button>
)}
<button className="btn btn-secondary" onClick={() => setShowExport(true)}>
📦 Export Chart
</button>
</>
)}
</>
)}
{!project.chart_uploaded && (
<button className="btn btn-primary" onClick={() => setShowUpload(true)}>
📤 Upload Chart
</button>
)}
</div>
</div>
{error && <div className="error">{error}</div>}
{/* Resources View */}
{resources.length > 0 && (
<>
{/* Filters */}
<div className="filters-section">
<div className="filters-grid">
<div className="filter-group">
<label className="label">Kind</label>
<select
className="input"
value={kindFilter}
onChange={(e) => setKindFilter(e.target.value)}
>
<option value="">All Kinds</option>
{uniqueKinds.map(kind => (
<option key={kind} value={kind}>{kind}</option>
))}
</select>
</div>
<div className="filter-group">
<label className="label">Name</label>
<input
type="text"
className="input"
placeholder="Filter by name..."
value={nameFilter}
onChange={(e) => setNameFilter(e.target.value)}
/>
</div>
<div className="filter-group">
<label className="label">Namespace</label>
<input
type="text"
className="input"
placeholder="Filter by namespace..."
value={namespaceFilter}
onChange={(e) => setNamespaceFilter(e.target.value)}
/>
</div>
</div>
{hasFilters && (
<button className="btn btn-outline" onClick={clearFilters}>
Clear Filters
</button>
)}
</div>
{/* Resources Stats */}
<div className="resources-stats">
<div className="stat">
<span className="stat-value">{filteredResources.length}</span>
<span className="stat-label">Resources</span>
</div>
<div className="stat">
<span className="stat-value">{uniqueKinds.length}</span>
<span className="stat-label">Kinds</span>
</div>
<div className="stat">
<span className="stat-value">
{resources.filter(r => r.modified).length}
</span>
<span className="stat-label">Modified</span>
</div>
</div>
{/* Resources Grid */}
{filteredResources.length > 0 ? (
<div className="resources-grid">
{filteredResources.map(resource => (
<ResourceCard
key={resource.uid}
resource={resource}
projectId={projectId}
onUpdate={handleResourceUpdate}
onClick={() => setSelectedResource(resource)}
/>
))}
</div>
) : (
<div className="empty-state">
<p>No resources match the current filters</p>
<button className="btn btn-outline" onClick={clearFilters}>
Clear Filters
</button>
</div>
)}
</>
)}
{/* Empty state when no resources */}
{resources.length === 0 && project.chart_uploaded && (
<div className="empty-state">
<div className="empty-icon">📋</div>
<h3>No resources rendered yet</h3>
<p>Click "Re-render" to render your Helm chart</p>
<button className="btn btn-primary" onClick={() => setShowRender(true)}>
🔄 Render Chart
</button>
</div>
)}
{/* Modals */}
{showUpload && (
<UploadSection
projectId={projectId}
onClose={() => setShowUpload(false)}
onComplete={handleUploadComplete}
/>
)}
{showRender && (
<RenderSection
projectId={projectId}
onClose={() => setShowRender(false)}
onComplete={handleRenderComplete}
/>
)}
{showExport && (
<ExportModal
projectId={projectId}
onClose={() => setShowExport(false)}
/>
)}
{showAllChanges && (
<AllChangesDiff
resources={resources}
onClose={() => setShowAllChanges(false)}
/>
)}
</div>
);
}
export default RenderView;

13
frontend/vite.config.js Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 5173,
watch: {
usePolling: true
}
}
})

6
helm-test/Chart.yaml Normal file
View File

@ -0,0 +1,6 @@
apiVersion: v2
name: aws-dvirlabs-nginx
description: Simple nginx with custom HTML + ALB Ingress for Route53
type: application
version: 0.1.0
appVersion: "1.0"

0
helm-test/aws Normal file
View File

View File

@ -0,0 +1,7 @@
{{- define "aws-dvirlabs-nginx.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "aws-dvirlabs-nginx.fullname" -}}
{{- printf "%s" (include "aws-dvirlabs-nginx.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}-html
data:
index.html: |
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>aws-dvirlabs.com</title>
</head>
<body>
<h1>{{ .Values.html.h1 }}</h1>
</body>
</html>

View File

@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "aws-dvirlabs-nginx.fullname" . }}
template:
metadata:
labels:
app: {{ include "aws-dvirlabs-nginx.fullname" . }}
spec:
containers:
- name: nginx
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
resources: {{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: html
configMap:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}-html

View File

@ -0,0 +1,21 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}
annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: {{ .Values.ingress.path }}
pathType: {{ .Values.ingress.pathType }}
backend:
service:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- end }}

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "aws-dvirlabs-nginx.fullname" . }}
spec:
type: ClusterIP
selector:
app: {{ include "aws-dvirlabs-nginx.fullname" . }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: 80

27
helm-test/values.yaml Normal file
View File

@ -0,0 +1,27 @@
replicaCount: 1
image:
repository: nginx
tag: stable-alpine
pullPolicy: IfNotPresent
service:
port: 80
html:
h1: "Hello from EKS + ALB + Route53!"
ingress:
enabled: true
className: alb
host: testapp.aws-dvirlabs.com
path: /
pathType: Prefix
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
resources: {}