First commit
This commit is contained in:
commit
bcb87093fa
7
.env.example
Normal file
7
.env.example
Normal 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
63
.gitignore
vendored
Normal 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
142
CHANGELOG.md
Normal 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
338
CONTRIBUTING.md
Normal 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
21
LICENSE
Normal 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
354
PROJECT_STRUCTURE.md
Normal 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
135
QUICKSTART.md
Normal 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
323
README.md
Normal file
@ -0,0 +1,323 @@
|
||||
# HelmView - Visual Helm Chart Editor & Validator
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**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
173
SECURITY.md
Normal 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
18
backend/.dockerignore
Normal 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
32
backend/Dockerfile
Normal 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
1019
backend/main.py
Normal file
File diff suppressed because it is too large
Load Diff
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal 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
57
docker-compose.prod.yml
Normal 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
41
docker-compose.yml
Normal 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
12
frontend/.dockerignore
Normal 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
18
frontend/Dockerfile
Normal 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
35
frontend/Dockerfile.prod
Normal 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
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>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
48
frontend/nginx.conf
Normal 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
27
frontend/package.json
Normal 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
357
frontend/src/App.css
Normal 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
48
frontend/src/App.jsx
Normal 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
60
frontend/src/api.js
Normal 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;
|
||||
141
frontend/src/components/AllChangesDiff.css
Normal file
141
frontend/src/components/AllChangesDiff.css
Normal 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;
|
||||
}
|
||||
112
frontend/src/components/AllChangesDiff.jsx
Normal file
112
frontend/src/components/AllChangesDiff.jsx
Normal 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;
|
||||
196
frontend/src/components/ExportModal.css
Normal file
196
frontend/src/components/ExportModal.css
Normal 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;
|
||||
}
|
||||
170
frontend/src/components/ExportModal.jsx
Normal file
170
frontend/src/components/ExportModal.jsx
Normal 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;
|
||||
177
frontend/src/components/RenderSection.css
Normal file
177
frontend/src/components/RenderSection.css
Normal 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;
|
||||
}
|
||||
254
frontend/src/components/RenderSection.jsx
Normal file
254
frontend/src/components/RenderSection.jsx
Normal 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 nested: 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;
|
||||
258
frontend/src/components/ResourceCard.css
Normal file
258
frontend/src/components/ResourceCard.css
Normal 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;
|
||||
}
|
||||
107
frontend/src/components/ResourceCard.jsx
Normal file
107
frontend/src/components/ResourceCard.jsx
Normal 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;
|
||||
153
frontend/src/components/UploadSection.css
Normal file
153
frontend/src/components/UploadSection.css
Normal 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;
|
||||
}
|
||||
222
frontend/src/components/UploadSection.jsx
Normal file
222
frontend/src/components/UploadSection.jsx
Normal 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;
|
||||
123
frontend/src/components/YamlEditor.css
Normal file
123
frontend/src/components/YamlEditor.css
Normal 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;
|
||||
}
|
||||
280
frontend/src/components/YamlEditor.jsx
Normal file
280
frontend/src/components/YamlEditor.jsx
Normal 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;
|
||||
34
frontend/src/context/ThemeContext.jsx
Normal file
34
frontend/src/context/ThemeContext.jsx
Normal 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
42
frontend/src/index.css
Normal 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
10
frontend/src/main.jsx
Normal 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>,
|
||||
)
|
||||
220
frontend/src/pages/ProjectDashboard.css
Normal file
220
frontend/src/pages/ProjectDashboard.css
Normal 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;
|
||||
}
|
||||
181
frontend/src/pages/ProjectDashboard.jsx
Normal file
181
frontend/src/pages/ProjectDashboard.jsx
Normal 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;
|
||||
146
frontend/src/pages/RenderView.css
Normal file
146
frontend/src/pages/RenderView.css
Normal 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;
|
||||
}
|
||||
319
frontend/src/pages/RenderView.jsx
Normal file
319
frontend/src/pages/RenderView.jsx
Normal 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
13
frontend/vite.config.js
Normal 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
6
helm-test/Chart.yaml
Normal 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
0
helm-test/aws
Normal file
7
helm-test/templates/_helpers.tpl
Normal file
7
helm-test/templates/_helpers.tpl
Normal 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 -}}
|
||||
16
helm-test/templates/configmap.yaml
Normal file
16
helm-test/templates/configmap.yaml
Normal 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>
|
||||
29
helm-test/templates/deployment.yaml
Normal file
29
helm-test/templates/deployment.yaml
Normal 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
|
||||
21
helm-test/templates/ingress.yaml
Normal file
21
helm-test/templates/ingress.yaml
Normal 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 }}
|
||||
12
helm-test/templates/service.yaml
Normal file
12
helm-test/templates/service.yaml
Normal 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
27
helm-test/values.yaml
Normal 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: {}
|
||||
Loading…
x
Reference in New Issue
Block a user