Add frontend
This commit is contained in:
298
DEPLOYMENT_SCRIPTS.md
Normal file
298
DEPLOYMENT_SCRIPTS.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# CF Deployer - Deployment Scripts Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains a Spring Boot application for deploying JAR files to Cloud Foundry/Tanzu environments using chunked uploads. There are two deployment scripts designed for different network paths.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ deploy-chunked.sh │──────► nginx ──────► Spring Boot App
|
||||
│ (Direct to nginx) │ (multipart endpoint)
|
||||
└─────────────────────┘
|
||||
|
||||
┌─────────────────────────┐
|
||||
│deploy-chunked-simple.sh │──► Java Proxy ──► nginx ──► Spring Boot App
|
||||
│ (Through Java proxy) │ (adds headers) (base64 endpoint)
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## Deployment Scripts
|
||||
|
||||
### 1. deploy-chunked.sh
|
||||
**Use when**: Direct access to nginx endpoint with cert/key/headers
|
||||
|
||||
**Features**:
|
||||
- Sends chunks as multipart form data (`-F` flags)
|
||||
- Supports client certificates (`--cert`, `--key`)
|
||||
- Supports custom headers (`X-Forwarded-For`, `My-APIM-KEY`)
|
||||
- Uses Spring Boot endpoint: `POST /upload/chunk` (multipart)
|
||||
|
||||
**Configuration**:
|
||||
```bash
|
||||
# Lines 49-55
|
||||
CERT_FILE="/path/to/cert.pem"
|
||||
KEY_FILE="/path/to/key.pem"
|
||||
X_FORWARDED_FOR="192.168.1.100"
|
||||
MY_APIM_KEY="your-api-key"
|
||||
```
|
||||
|
||||
**Curl format**:
|
||||
```bash
|
||||
curl POST /upload/chunk \
|
||||
--cert cert.pem --key key.pem \
|
||||
-H "X-Forwarded-For: ..." \
|
||||
-H "My-APIM-KEY: ..." \
|
||||
-F "uploadSessionId=..." \
|
||||
-F "fileType=..." \
|
||||
-F "chunk=@chunk_file"
|
||||
```
|
||||
|
||||
### 2. deploy-chunked-simple.sh
|
||||
**Use when**: Going through Java proxy that adds cert/headers automatically
|
||||
|
||||
**Features**:
|
||||
- Sends chunks as Base64-encoded text (to work with Java proxy)
|
||||
- Query parameters in URL (for Java proxy's `request.getQueryString()`)
|
||||
- No cert/key/headers needed (Java proxy adds them)
|
||||
- Uses Spring Boot endpoint: `POST /upload/chunk` (text/plain, base64)
|
||||
|
||||
**Configuration**:
|
||||
```bash
|
||||
# Line 24
|
||||
API_BASE="https://myapp.com/v1/utility"
|
||||
```
|
||||
|
||||
**Curl format**:
|
||||
```bash
|
||||
curl POST /upload/chunk?uploadSessionId=...&fileType=... \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "X-Chunk-Encoding: base64" \
|
||||
-d @base64_chunk_file
|
||||
```
|
||||
|
||||
## Why Two Different Scripts?
|
||||
|
||||
### The Java Proxy Problem
|
||||
|
||||
The Java proxy that sits in front of the Spring Boot app reads the request body as a String:
|
||||
|
||||
```java
|
||||
@RequestBody(required = false) String body
|
||||
```
|
||||
|
||||
**Problem**: Binary multipart data gets corrupted when read as String
|
||||
**Solution**: Base64 encode chunks as text before sending through the proxy
|
||||
|
||||
### Deploy Script Comparison
|
||||
|
||||
| Feature | deploy-chunked.sh | deploy-chunked-simple.sh |
|
||||
|---------|-------------------|--------------------------|
|
||||
| Network Path | Direct to nginx | Through Java proxy |
|
||||
| Chunk Format | Multipart binary | Base64 text |
|
||||
| Query Params | No (uses `-F` form fields) | Yes (in URL) |
|
||||
| Cert/Key | Required in script | Added by Java proxy |
|
||||
| Headers | Required in script | Added by Java proxy |
|
||||
| Spring Endpoint | multipart/form-data | text/plain |
|
||||
|
||||
## Spring Boot Endpoints
|
||||
|
||||
The Spring Boot app has **three** chunk upload endpoints:
|
||||
|
||||
### 1. Multipart Endpoint (Original)
|
||||
```java
|
||||
@PostMapping("/upload/chunk")
|
||||
// Consumes: multipart/form-data
|
||||
// Parameters: All as form fields (-F)
|
||||
// File: @RequestPart("chunk") MultipartFile
|
||||
```
|
||||
**Used by**: deploy-chunked.sh (direct to nginx)
|
||||
|
||||
### 2. Raw Binary Endpoint
|
||||
```java
|
||||
@PostMapping(value = "/upload/chunk", consumes = "application/octet-stream")
|
||||
// Consumes: application/octet-stream
|
||||
// Parameters: Query params in URL
|
||||
// File: @RequestBody byte[]
|
||||
```
|
||||
**Used by**: Not currently used (would fail through Java proxy)
|
||||
|
||||
### 3. Base64 Text Endpoint
|
||||
```java
|
||||
@PostMapping(value = "/upload/chunk", consumes = "text/plain")
|
||||
// Consumes: text/plain
|
||||
// Parameters: Query params in URL
|
||||
// File: @RequestBody String (Base64 decoded)
|
||||
// Header: X-Chunk-Encoding: base64
|
||||
```
|
||||
**Used by**: deploy-chunked-simple.sh (through Java proxy)
|
||||
|
||||
Spring Boot routes to the correct endpoint based on the `Content-Type` header!
|
||||
|
||||
## Common Configuration (Both Scripts)
|
||||
|
||||
Both scripts share these configuration options:
|
||||
|
||||
```bash
|
||||
# Files to deploy
|
||||
JAR_FILE="./app.jar"
|
||||
MANIFEST_FILE="./manifest.yml"
|
||||
|
||||
# Chunk size (1MB recommended for Tanzu)
|
||||
CHUNK_SIZE=1048576
|
||||
|
||||
# Cloud Foundry configuration
|
||||
CF_API_ENDPOINT="https://api.cf.example.com"
|
||||
CF_USERNAME="your-username"
|
||||
CF_PASSWORD="your-password"
|
||||
CF_ORGANIZATION="your-org"
|
||||
CF_SPACE="your-space"
|
||||
CF_APP_NAME="your-app"
|
||||
CF_SKIP_SSL="false"
|
||||
|
||||
# Polling configuration
|
||||
POLL_INTERVAL=5
|
||||
MAX_WAIT=600
|
||||
|
||||
# Debug mode
|
||||
DEBUG_MODE="false" # Set to "true" for verbose output
|
||||
```
|
||||
|
||||
## Deployment Flow
|
||||
|
||||
Both scripts follow the same 5-step process:
|
||||
|
||||
1. **Initialize Upload Session**: POST `/upload/init` with CF credentials
|
||||
2. **Upload JAR Chunks**: POST `/upload/chunk` for each chunk
|
||||
3. **Upload Manifest Chunks**: POST `/upload/chunk` for manifest.yml
|
||||
4. **Finalize Upload**: POST `/upload/finalize?uploadSessionId=...&async=true`
|
||||
5. **Poll Deployment Status**: GET `/deployment/status/{uploadSessionId}`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Required part 'chunk' is not present"
|
||||
- **Cause**: Nginx stripped multipart body or wrong Content-Type
|
||||
- **Solution**: Use deploy-chunked-simple.sh with Base64 encoding
|
||||
|
||||
### "504 Gateway Timeout" on chunk upload
|
||||
- **Cause**: Java proxy trying to read binary data as String
|
||||
- **Solution**: Use Base64 encoding (deploy-chunked-simple.sh)
|
||||
|
||||
### "Argument list too long"
|
||||
- **Cause**: Base64 string passed as command argument instead of file
|
||||
- **Solution**: Already fixed - script writes Base64 to temp file and uses `-d @file`
|
||||
|
||||
### "Missing uploadSessionId parameter"
|
||||
- **Cause**: Nginx or proxy stripping query parameters
|
||||
- **For deploy-chunked.sh**: Parameters should be in form fields (`-F`)
|
||||
- **For deploy-chunked-simple.sh**: Parameters should be in query string (`?uploadSessionId=...`)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Why Not Fix the Java Proxy?
|
||||
|
||||
The Java proxy is shared by multiple services, so modifying it could break other applications. Instead, we adapted the deployment script to work with the proxy's limitations.
|
||||
|
||||
### Why Base64 Encoding?
|
||||
|
||||
When the Java proxy reads binary data as `@RequestBody String body`, it:
|
||||
- Corrupts binary data (non-UTF8 bytes)
|
||||
- May hang or timeout on large binary payloads
|
||||
- Cannot properly forward multipart boundaries
|
||||
|
||||
Base64 encoding converts binary to safe ASCII text that the proxy can handle as a String.
|
||||
|
||||
### Why Query Parameters for Simple Script?
|
||||
|
||||
The Java proxy reconstructs the request using:
|
||||
```java
|
||||
String queryParams = request.getQueryString();
|
||||
String completeRequest = WSGURL + req;
|
||||
if (queryParams != null) {
|
||||
completeRequest = completeRequest + "?" + queryParams;
|
||||
}
|
||||
```
|
||||
|
||||
It only forwards query parameters, not form field parameters, so we must use query strings.
|
||||
|
||||
### Performance Impact of Base64
|
||||
|
||||
Base64 encoding increases payload size by ~33%:
|
||||
- 1MB binary chunk → ~1.33MB Base64 text
|
||||
- Adds CPU overhead for encoding/decoding
|
||||
- Acceptable tradeoff for proxy compatibility
|
||||
|
||||
## Testing
|
||||
|
||||
### Test deploy-chunked.sh (Direct to nginx)
|
||||
```bash
|
||||
# Configure cert/key/headers in script
|
||||
vim deploy-chunked.sh
|
||||
|
||||
# Run with debug
|
||||
DEBUG_MODE="true" ./deploy-chunked.sh
|
||||
```
|
||||
|
||||
### Test deploy-chunked-simple.sh (Through proxy)
|
||||
```bash
|
||||
# Configure API base URL
|
||||
vim deploy-chunked-simple.sh
|
||||
|
||||
# Run with debug
|
||||
DEBUG_MODE="true" ./deploy-chunked-simple.sh
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Shell Requirements
|
||||
- `bash` 4.0+
|
||||
- `curl`
|
||||
- `awk` (replaces `bc` for file size calculation)
|
||||
- `base64` (for deploy-chunked-simple.sh)
|
||||
- `mktemp`
|
||||
- `split`
|
||||
- `stat`
|
||||
|
||||
### Backend Requirements
|
||||
- Spring Boot 3.2.0+
|
||||
- Java 17+
|
||||
- Gradle 8.14
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `deploy-chunked.sh` | Direct nginx deployment with cert/headers |
|
||||
| `deploy-chunked-simple.sh` | Java proxy deployment with Base64 |
|
||||
| `CfDeployController.java` | REST endpoints (3 chunk upload variants) |
|
||||
| `ChunkedUploadService.java` | Chunk processing (multipart + raw bytes) |
|
||||
| `AsyncDeploymentService.java` | Background deployment execution |
|
||||
|
||||
## Quick Start
|
||||
|
||||
**For direct nginx access**:
|
||||
```bash
|
||||
cp deploy-chunked.sh my-deploy.sh
|
||||
# Edit configuration
|
||||
vim my-deploy.sh
|
||||
# Run
|
||||
./my-deploy.sh
|
||||
```
|
||||
|
||||
**For Java proxy access**:
|
||||
```bash
|
||||
cp deploy-chunked-simple.sh my-deploy.sh
|
||||
# Edit API_BASE
|
||||
vim my-deploy.sh
|
||||
# Run
|
||||
./my-deploy.sh
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Enable `DEBUG_MODE="true"` in the script
|
||||
2. Check the curl commands and responses
|
||||
3. Review Spring Boot application logs
|
||||
4. Verify nginx/proxy logs for request forwarding
|
||||
19
README.md
19
README.md
@@ -207,12 +207,29 @@ done
|
||||
```
|
||||
|
||||
**For more details, see:**
|
||||
- `deploy-chunked.sh` - Complete working script
|
||||
- `deploy-chunked.sh` - Direct nginx deployment with cert/headers
|
||||
- `deploy-chunked-simple.sh` - Java proxy deployment with Base64 encoding
|
||||
- **`DEPLOYMENT_SCRIPTS.md`** - **Comprehensive guide for both deployment scripts** ⭐
|
||||
- `CHUNKED_UPLOAD_GUIDE.md` - Detailed API documentation
|
||||
- `TIMEOUT_SOLUTION.md` - Architecture and design details
|
||||
- `CHUNK_SIZE_GUIDE.md` - Chunk size recommendations
|
||||
- `MEMORY_FIX.md` - JVM memory configuration for Tanzu
|
||||
|
||||
### Two Deployment Scripts: Which One to Use?
|
||||
|
||||
We provide **two deployment scripts** for different network configurations:
|
||||
|
||||
| Script | Use When | Features |
|
||||
|--------|----------|----------|
|
||||
| **deploy-chunked.sh** | Direct access to nginx | Multipart uploads, cert/key support, custom headers |
|
||||
| **deploy-chunked-simple.sh** | Through Java proxy | Base64 encoded uploads, proxy adds cert/headers |
|
||||
|
||||
**📖 See [DEPLOYMENT_SCRIPTS.md](DEPLOYMENT_SCRIPTS.md) for detailed comparison, troubleshooting, and technical explanations.**
|
||||
|
||||
**Quick Decision Guide:**
|
||||
- ✅ Use `deploy-chunked.sh` if you have direct nginx access and need to provide certificates/headers
|
||||
- ✅ Use `deploy-chunked-simple.sh` if you go through a Java proxy that adds authentication automatically
|
||||
|
||||
---
|
||||
|
||||
### 2. List Applications
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# CF Deployer - Simple Chunked Upload Deployment Script
|
||||
# CF Deployer - Simple Chunked Upload Deployment Script (Through Java proxy)
|
||||
#
|
||||
# This script deploys a Java application to Cloud Foundry using chunked
|
||||
# uploads to bypass nginx size restrictions and async deployment to avoid
|
||||
# timeout issues.
|
||||
#
|
||||
# This is a simplified version without cert/key/header authentication.
|
||||
# USE THIS SCRIPT WHEN:
|
||||
# - You deploy through a Java proxy that adds certificates/headers
|
||||
# - The proxy reads request body as @RequestBody String
|
||||
# - You need to send chunks as Base64-encoded text
|
||||
#
|
||||
# For direct nginx deployments with cert/key, use: deploy-chunked.sh
|
||||
# For detailed documentation, see: DEPLOYMENT_SCRIPTS.md
|
||||
#
|
||||
# TECHNICAL NOTES:
|
||||
# - Chunks are Base64-encoded to prevent corruption in the Java proxy
|
||||
# - Query parameters are sent in URL (not form fields)
|
||||
# - Uses Spring Boot endpoint: POST /upload/chunk (Content-Type: text/plain)
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy-chunked-simple.sh
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# CF Deployer - Chunked Upload Deployment Script
|
||||
# CF Deployer - Chunked Upload Deployment Script (Direct to nginx)
|
||||
#
|
||||
# This script deploys a Java application to Cloud Foundry using chunked
|
||||
# uploads to bypass nginx size restrictions and async deployment to avoid
|
||||
# timeout issues.
|
||||
#
|
||||
# USE THIS SCRIPT WHEN:
|
||||
# - You have direct access to nginx endpoint
|
||||
# - You need to provide client certificates and custom headers
|
||||
# - You can send multipart/form-data directly
|
||||
#
|
||||
# For deployments through a Java proxy, use: deploy-chunked-simple.sh
|
||||
# For detailed documentation, see: DEPLOYMENT_SCRIPTS.md
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy-chunked.sh
|
||||
#
|
||||
|
||||
7
frontend/.dockerignore
Normal file
7
frontend/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
helm
|
||||
25
frontend/.gitignore
vendored
Normal file
25
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Angular
|
||||
dist/
|
||||
.angular/
|
||||
.ng_build/
|
||||
.ng_pkg_build/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
33
frontend/Dockerfile
Normal file
33
frontend/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Stage 1: Build Angular app
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build Angular app for production
|
||||
RUN npm run build:prod
|
||||
|
||||
# Stage 2: Serve with nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.conf /etc/nginx/nginx.conf.template
|
||||
|
||||
# Copy built Angular app
|
||||
COPY --from=builder /app/dist/cf-deployer-ui/browser /usr/share/nginx/html
|
||||
|
||||
# Set default backend URL
|
||||
ENV BACKEND_URL=http://localhost:8080
|
||||
|
||||
# Replace env variables and start nginx
|
||||
CMD envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'
|
||||
|
||||
EXPOSE 80
|
||||
348
frontend/README.md
Normal file
348
frontend/README.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Tanzu Deployer UI - Angular Frontend
|
||||
|
||||
A modern, dark-themed Angular 19.1 frontend for deploying applications to Tanzu/Cloud Foundry environments.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Modern Dark Theme**: Sleek GitHub-inspired dark UI with blue/purple gradients
|
||||
- ✅ **Simple Form Interface**: Easy-to-use deployment form with all Tanzu configuration fields
|
||||
- ✅ **Modern File Upload**: Beautiful drag-and-drop style file upload buttons with SVG icons
|
||||
- ✅ **Chunked File Upload**: Handles large JAR files using Base64-encoded chunks (compatible with Java proxy)
|
||||
- ✅ **Real-time Progress**: Visual progress bar showing upload and deployment status
|
||||
- ✅ **Live Logs**: Collapsible terminal-style output window displaying deployment logs
|
||||
- ✅ **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
|
||||
- ✅ **Secure**: Password fields are masked, follows Angular security best practices
|
||||
- ✅ **Configurable**: Environment-based configuration for all settings
|
||||
- ✅ **Production Ready**: Includes nginx configuration and Helm chart for K8s deployment
|
||||
|
||||
## Configuration
|
||||
|
||||
All application settings are managed via environment files:
|
||||
|
||||
**`src/environments/environment.ts`** (Development):
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBase: '/api/cf', // API endpoint base URL
|
||||
chunkSize: 1048576, // Chunk size in bytes (1MB)
|
||||
enableSslValidation: false, // Show/hide SSL validation checkbox
|
||||
pollInterval: 5000, // Poll interval in milliseconds
|
||||
maxPollAttempts: 120, // Max polling attempts (10 minutes)
|
||||
defaultLogsExpanded: true, // Logs expanded by default
|
||||
showDebugInfo: false // Show debug information
|
||||
};
|
||||
```
|
||||
|
||||
**`src/environments/environment.prod.ts`** (Production):
|
||||
- Same structure as development
|
||||
- Used when building with `--configuration production`
|
||||
|
||||
### Customizing Configuration
|
||||
|
||||
To customize the application behavior:
|
||||
|
||||
1. Edit `src/environments/environment.ts` for development
|
||||
2. Edit `src/environments/environment.prod.ts` for production
|
||||
3. Rebuild the application
|
||||
|
||||
**Common Customizations:**
|
||||
|
||||
- **Enable SSL Validation Checkbox**: Set `enableSslValidation: true`
|
||||
- **Change Chunk Size**: Set `chunkSize: 2097152` (2MB)
|
||||
- **Increase Poll Time**: Set `maxPollAttempts: 240` (20 minutes)
|
||||
- **Change API Endpoint**: Set `apiBase: 'https://api.example.com/cf'`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Start development server:**
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
3. **Open browser:**
|
||||
Navigate to `http://localhost:4200`
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Built files will be in `dist/cf-deployer-ui/browser/`
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### Build Docker Image
|
||||
|
||||
```bash
|
||||
docker build -t cf-deployer-ui:latest .
|
||||
```
|
||||
|
||||
### Run Docker Container
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 8080:80 \
|
||||
-e BACKEND_URL=http://cf-deployer-backend:8080 \
|
||||
--name cf-deployer-ui \
|
||||
cf-deployer-ui:latest
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
- `BACKEND_URL`: URL of the CF Deployer backend API (default: `http://localhost:8080`)
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Kubernetes cluster
|
||||
- Helm 3.x installed
|
||||
- Docker image pushed to registry
|
||||
|
||||
### Deploy with Helm
|
||||
|
||||
1. **Update values.yaml:**
|
||||
```yaml
|
||||
image:
|
||||
repository: your-registry/cf-deployer-ui
|
||||
tag: "1.0.0"
|
||||
|
||||
backend:
|
||||
url: "http://cf-deployer-backend:8080"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: cf-deployer.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
2. **Install the chart:**
|
||||
```bash
|
||||
helm install cf-deployer-ui ./helm
|
||||
```
|
||||
|
||||
3. **Upgrade the chart:**
|
||||
```bash
|
||||
helm upgrade cf-deployer-ui ./helm
|
||||
```
|
||||
|
||||
4. **Uninstall:**
|
||||
```bash
|
||||
helm uninstall cf-deployer-ui
|
||||
```
|
||||
|
||||
### Helm Configuration Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `replicaCount` | Number of replicas | `2` |
|
||||
| `image.repository` | Docker image repository | `cf-deployer-ui` |
|
||||
| `image.tag` | Docker image tag | `latest` |
|
||||
| `service.type` | Kubernetes service type | `ClusterIP` |
|
||||
| `service.port` | Service port | `80` |
|
||||
| `backend.url` | Backend API URL | `http://cf-deployer-backend:8080` |
|
||||
| `ingress.enabled` | Enable ingress | `false` |
|
||||
| `resources.limits.cpu` | CPU limit | `500m` |
|
||||
| `resources.limits.memory` | Memory limit | `512Mi` |
|
||||
|
||||
## Usage
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
1. **Fill in Cloud Foundry Details:**
|
||||
- API Endpoint (e.g., `https://api.cf.example.com`)
|
||||
- Username and Password
|
||||
- Organization and Space
|
||||
- Application Name
|
||||
- Skip SSL Validation (if needed)
|
||||
|
||||
2. **Select Files:**
|
||||
- JAR File: Your application JAR
|
||||
- Manifest File: Cloud Foundry manifest.yml
|
||||
|
||||
3. **Deploy:**
|
||||
- Click "Deploy to Cloud Foundry"
|
||||
- Watch progress bar and logs
|
||||
- Wait for completion
|
||||
|
||||
### Screenshots
|
||||
|
||||
**Main Form:**
|
||||
- Clean, responsive form with all required fields
|
||||
- File upload with size display
|
||||
- SSL validation checkbox
|
||||
|
||||
**Progress Tracking:**
|
||||
- Visual progress bar (0-100%)
|
||||
- Current step indicator
|
||||
- Status badges (IN_PROGRESS, COMPLETED, FAILED)
|
||||
|
||||
**Logs Output:**
|
||||
- Collapsible terminal-style output
|
||||
- Timestamped log entries
|
||||
- Auto-scroll to latest logs
|
||||
|
||||
## Architecture
|
||||
|
||||
### How It Works
|
||||
|
||||
The frontend mimics the behavior of `deploy-chunked-simple.sh`:
|
||||
|
||||
1. **Initialize Upload Session:**
|
||||
```
|
||||
POST /api/cf/upload/init
|
||||
→ Returns uploadSessionId
|
||||
```
|
||||
|
||||
2. **Upload Files in Chunks:**
|
||||
- Splits files into 1MB chunks
|
||||
- Base64 encodes each chunk (for Java proxy compatibility)
|
||||
- Uploads via:
|
||||
```
|
||||
POST /api/cf/upload/chunk?uploadSessionId=...&fileType=...&chunkIndex=...&totalChunks=...&fileName=...
|
||||
Headers: Content-Type: text/plain, X-Chunk-Encoding: base64
|
||||
Body: Base64 chunk data
|
||||
```
|
||||
|
||||
3. **Finalize Upload:**
|
||||
```
|
||||
POST /api/cf/upload/finalize?uploadSessionId=...&async=true
|
||||
```
|
||||
|
||||
4. **Poll Deployment Status:**
|
||||
```
|
||||
GET /api/cf/deployment/status/{uploadSessionId}
|
||||
(Every 5 seconds until COMPLETED or FAILED)
|
||||
```
|
||||
|
||||
### Why Base64 Encoding?
|
||||
|
||||
The frontend sends chunks as Base64-encoded text because:
|
||||
- It goes through a Java proxy that reads `@RequestBody String`
|
||||
- Binary data gets corrupted when read as String
|
||||
- Base64 ensures safe text transport through the proxy
|
||||
- Backend automatically decodes Base64 back to binary
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── app.component.ts # Main component with form logic
|
||||
│ │ ├── app.component.html # Template with form UI
|
||||
│ │ ├── app.component.css # Component styles
|
||||
│ │ └── deploy.service.ts # API service
|
||||
│ ├── index.html # Main HTML file
|
||||
│ ├── main.ts # Bootstrap file
|
||||
│ └── styles.css # Global styles
|
||||
├── helm/ # Helm chart
|
||||
│ ├── Chart.yaml
|
||||
│ ├── values.yaml
|
||||
│ └── templates/
|
||||
│ ├── deployment.yaml
|
||||
│ ├── service.yaml
|
||||
│ ├── ingress.yaml
|
||||
│ └── _helpers.tpl
|
||||
├── nginx.conf # nginx configuration
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── angular.json # Angular CLI configuration
|
||||
├── package.json # NPM dependencies
|
||||
└── tsconfig.json # TypeScript configuration
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20.x or higher
|
||||
- npm 10.x or higher
|
||||
- Angular CLI 19.x
|
||||
|
||||
### Install Angular CLI
|
||||
|
||||
```bash
|
||||
npm install -g @angular/cli@19
|
||||
```
|
||||
|
||||
### Code Structure
|
||||
|
||||
**app.component.ts:**
|
||||
- Handles form state and validation
|
||||
- Manages file uploads and chunking
|
||||
- Polls deployment status
|
||||
- Updates progress and logs
|
||||
|
||||
**deploy.service.ts:**
|
||||
- Encapsulates all HTTP API calls
|
||||
- Returns RxJS Observables converted to Promises
|
||||
- Handles Base64 encoding headers
|
||||
|
||||
**Styling:**
|
||||
- Responsive grid layout
|
||||
- Mobile-first design
|
||||
- Terminal-style logs with custom scrollbar
|
||||
- Gradient progress bar
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CORS Errors
|
||||
|
||||
If you see CORS errors in the browser console:
|
||||
|
||||
1. **Development:** Configure proxy in `angular.json`:
|
||||
```json
|
||||
{
|
||||
"serve": {
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `proxy.conf.json`:
|
||||
```json
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Production:** nginx handles proxying (already configured)
|
||||
|
||||
### File Upload Fails
|
||||
|
||||
- Check that backend is running and accessible
|
||||
- Verify `BACKEND_URL` environment variable
|
||||
- Check browser console for error messages
|
||||
- Enable DEBUG_MODE in backend to see detailed logs
|
||||
|
||||
### Deployment Timeout
|
||||
|
||||
- Default timeout is 10 minutes (120 attempts × 5 seconds)
|
||||
- Increase `maxAttempts` in `pollDeploymentStatus()` if needed
|
||||
- Check backend logs for actual deployment status
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Mobile browsers (iOS Safari, Chrome Android)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see main project README
|
||||
76
frontend/angular.json
Normal file
76
frontend/angular.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"cf-deployer-ui": {
|
||||
"projectType": "application",
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/cf-deployer-ui",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.json",
|
||||
"assets": [],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kB",
|
||||
"maximumError": "4kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "cf-deployer-ui:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "cf-deployer-ui:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
6
frontend/helm/Chart.yaml
Normal file
6
frontend/helm/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: cf-deployer-ui
|
||||
description: Cloud Foundry Deployer UI - Angular frontend
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "1.0.0"
|
||||
29
frontend/helm/templates/_helpers.tpl
Normal file
29
frontend/helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "cf-deployer-ui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "cf-deployer-ui.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "cf-deployer-ui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
64
frontend/helm/templates/deployment.yaml
Normal file
64
frontend/helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cf-deployer-ui.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
chart: {{ include "cf-deployer-ui.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: BACKEND_URL
|
||||
value: {{ .Values.backend.url | quote }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
44
frontend/helm/templates/ingress.yaml
Normal file
44
frontend/helm/templates/ingress.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "cf-deployer-ui.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
chart: {{ include "cf-deployer-ui.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "cf-deployer-ui.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
19
frontend/helm/templates/service.yaml
Normal file
19
frontend/helm/templates/service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cf-deployer-ui.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
chart: {{ include "cf-deployer-ui.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: {{ include "cf-deployer-ui.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
61
frontend/helm/values.yaml
Normal file
61
frontend/helm/values.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
# Default values for cf-deployer-ui
|
||||
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: cf-deployer-ui
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: false
|
||||
name: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: cf-deployer.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: cf-deployer-tls
|
||||
# hosts:
|
||||
# - cf-deployer.example.com
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
# Backend service URL
|
||||
backend:
|
||||
url: "http://cf-deployer-backend:8080"
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
45
frontend/nginx.conf
Normal file
45
frontend/nginx.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Angular app
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy API requests to backend
|
||||
location /api/ {
|
||||
proxy_pass ${BACKEND_URL};
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
|
||||
# Increase timeout for long-running deployments
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
|
||||
# Allow large file uploads
|
||||
client_max_body_size 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "cf-deployer-ui",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --configuration production"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.1.0",
|
||||
"@angular/common": "^19.1.0",
|
||||
"@angular/compiler": "^19.1.0",
|
||||
"@angular/core": "^19.1.0",
|
||||
"@angular/forms": "^19.1.0",
|
||||
"@angular/platform-browser": "^19.1.0",
|
||||
"@angular/platform-browser-dynamic": "^19.1.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"tslib": "^2.6.0",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.1.0",
|
||||
"@angular/cli": "^19.1.0",
|
||||
"@angular/compiler-cli": "^19.1.0",
|
||||
"typescript": "~5.6.0"
|
||||
}
|
||||
}
|
||||
38
frontend/quick-start.sh
Normal file
38
frontend/quick-start.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "CF Deployer UI - Quick Start"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
# Check if Node.js is installed
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "Error: Node.js is not installed"
|
||||
echo "Please install Node.js 20.x or higher from https://nodejs.org"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node.js version: $(node --version)"
|
||||
echo "npm version: $(npm --version)"
|
||||
echo ""
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "Installing dependencies..."
|
||||
npm install
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Ask for backend URL
|
||||
read -p "Enter backend URL (default: http://localhost:8080): " BACKEND_URL
|
||||
BACKEND_URL=${BACKEND_URL:-http://localhost:8080}
|
||||
|
||||
echo ""
|
||||
echo "Starting development server..."
|
||||
echo "Backend URL: $BACKEND_URL"
|
||||
echo ""
|
||||
echo "The app will be available at: http://localhost:4200"
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
# Start dev server
|
||||
npm start
|
||||
442
frontend/src/app/app.component.css
Normal file
442
frontend/src/app/app.component.css
Normal file
@@ -0,0 +1,442 @@
|
||||
/* Dark Theme Variables */
|
||||
:host {
|
||||
--bg-primary: #0d1117;
|
||||
--bg-secondary: #161b22;
|
||||
--bg-tertiary: #21262d;
|
||||
--bg-hover: #30363d;
|
||||
--border-color: #30363d;
|
||||
--text-primary: #c9d1d9;
|
||||
--text-secondary: #8b949e;
|
||||
--accent-primary: #58a6ff;
|
||||
--accent-secondary: #1f6feb;
|
||||
--success: #3fb950;
|
||||
--error: #f85149;
|
||||
--warning: #d29922;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 30px;
|
||||
background: var(--bg-secondary);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 25%, #ffaa00 50%, #ff6b35 75%, #f7931e 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 107, 53, 0.3));
|
||||
}
|
||||
|
||||
header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus,
|
||||
.form-group input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background-color: var(--bg-primary);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Modern File Upload */
|
||||
.file-upload-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.file-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 12px 24px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-upload-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.file-upload-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-upload-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-info svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.form-group.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-group.checkbox label {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group.checkbox input {
|
||||
width: auto;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
color: var(--accent-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 32px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #1f6feb 0%, #8957e5 100%);
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(63, 185, 80, 0.15);
|
||||
color: var(--success);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
color: var(--error);
|
||||
border: 1px solid var(--error);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: rgba(88, 166, 255, 0.15);
|
||||
color: var(--accent-primary);
|
||||
border: 1px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.logs-card {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 5px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.logs-header:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.logs-header h2 {
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 1.2em;
|
||||
color: var(--text-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.logs-output {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 4px 0;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid rgba(48, 54, 61, 0.3);
|
||||
}
|
||||
|
||||
.log-line:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.logs-output::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.logs-output::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logs-output::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-hover);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logs-output::-webkit-scrollbar-thumb:hover {
|
||||
background: #484f58;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logs-output {
|
||||
max-height: 300px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
217
frontend/src/app/app.component.html
Normal file
217
frontend/src/app/app.component.html
Normal file
@@ -0,0 +1,217 @@
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Tanzu Deployer</h1>
|
||||
<p>Deploy your applications to Tanzu Platform</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<form (ngSubmit)="deploy()" #deployForm="ngForm">
|
||||
<!-- Cloud Foundry Configuration -->
|
||||
<div class="form-section">
|
||||
<h3>Tanzu Configuration</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiEndpoint">API Endpoint *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiEndpoint"
|
||||
name="apiEndpoint"
|
||||
[(ngModel)]="apiEndpoint"
|
||||
placeholder="https://api.cf.example.com"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="username">Username *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
[(ngModel)]="username"
|
||||
placeholder="your-username"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
[(ngModel)]="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="organization">Organization *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="organization"
|
||||
name="organization"
|
||||
[(ngModel)]="organization"
|
||||
placeholder="your-org"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="space">Space *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="space"
|
||||
name="space"
|
||||
[(ngModel)]="space"
|
||||
placeholder="dev"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="appName">Application Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="appName"
|
||||
name="appName"
|
||||
[(ngModel)]="appName"
|
||||
placeholder="my-app"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox" *ngIf="env.enableSslValidation">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="skipSsl"
|
||||
[(ngModel)]="skipSsl"
|
||||
[disabled]="uploading">
|
||||
Skip SSL Validation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Upload -->
|
||||
<div class="form-section">
|
||||
<h3>Application Files</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="jarFile">JAR File *</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input
|
||||
type="file"
|
||||
id="jarFile"
|
||||
name="jarFile"
|
||||
class="file-input"
|
||||
(change)="onJarFileChange($event)"
|
||||
accept=".jar"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
<label for="jarFile" class="file-upload-btn" [class.disabled]="uploading">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span *ngIf="!jarFile">Choose JAR File</span>
|
||||
<span *ngIf="jarFile">Change File</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="file-info" *ngIf="jarFile">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
|
||||
<polyline points="13 2 13 9 20 9"></polyline>
|
||||
</svg>
|
||||
<span>{{ jarFile.name }}</span>
|
||||
<span class="file-size">({{ (jarFile.size / 1024 / 1024).toFixed(2) }}MB)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="manifestFile">Manifest File *</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input
|
||||
type="file"
|
||||
id="manifestFile"
|
||||
name="manifestFile"
|
||||
class="file-input"
|
||||
(change)="onManifestFileChange($event)"
|
||||
accept=".yml,.yaml"
|
||||
required
|
||||
[disabled]="uploading">
|
||||
<label for="manifestFile" class="file-upload-btn" [class.disabled]="uploading">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span *ngIf="!manifestFile">Choose Manifest File</span>
|
||||
<span *ngIf="manifestFile">Change File</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="file-info" *ngIf="manifestFile">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
|
||||
<polyline points="13 2 13 9 20 9"></polyline>
|
||||
</svg>
|
||||
<span>{{ manifestFile.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!deployForm.form.valid || uploading">
|
||||
<span *ngIf="!uploading">Deploy to Tanzu</span>
|
||||
<span *ngIf="uploading">Deploying...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="clearForm()" [disabled]="uploading">
|
||||
Clear Form
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Progress Section -->
|
||||
<div class="card" *ngIf="uploading || deploymentStatus">
|
||||
<h2>Deployment Progress</h2>
|
||||
|
||||
<div class="progress-section">
|
||||
<div class="progress-label">
|
||||
<span>{{ currentStep }}</span>
|
||||
<span class="progress-percent">{{ uploadProgress }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" [style.width.%]="uploadProgress"></div>
|
||||
</div>
|
||||
|
||||
<div class="status-badge" *ngIf="deploymentStatus">
|
||||
<span class="badge" [class.badge-success]="deploymentStatus === 'COMPLETED'"
|
||||
[class.badge-error]="deploymentStatus === 'FAILED'"
|
||||
[class.badge-info]="deploymentStatus === 'IN_PROGRESS'">
|
||||
{{ deploymentStatus }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Section -->
|
||||
<div class="card logs-card" *ngIf="logs.length > 0">
|
||||
<div class="logs-header" (click)="toggleLogs()">
|
||||
<h2>Deployment Logs</h2>
|
||||
<span class="toggle-icon">{{ logsExpanded ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="logs-output" *ngIf="logsExpanded">
|
||||
<div class="log-line" *ngFor="let log of logs">{{ log }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
217
frontend/src/app/app.component.ts
Normal file
217
frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DeployService } from './deploy.service';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
// Environment config
|
||||
env = environment;
|
||||
|
||||
// Form fields
|
||||
apiEndpoint = '';
|
||||
username = '';
|
||||
password = '';
|
||||
organization = '';
|
||||
space = '';
|
||||
appName = '';
|
||||
skipSsl = false;
|
||||
jarFile: File | null = null;
|
||||
manifestFile: File | null = null;
|
||||
|
||||
// Upload state
|
||||
uploading = false;
|
||||
uploadProgress = 0;
|
||||
currentStep = '';
|
||||
logs: string[] = [];
|
||||
logsExpanded = environment.defaultLogsExpanded;
|
||||
deploymentStatus = '';
|
||||
sessionId = '';
|
||||
|
||||
constructor(private deployService: DeployService) {}
|
||||
|
||||
onJarFileChange(event: any) {
|
||||
this.jarFile = event.target.files[0];
|
||||
}
|
||||
|
||||
onManifestFileChange(event: any) {
|
||||
this.manifestFile = event.target.files[0];
|
||||
}
|
||||
|
||||
addLog(message: string) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.logs.push(`[${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
if (!this.jarFile || !this.manifestFile) {
|
||||
alert('Please select both JAR and Manifest files');
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.uploadProgress = 0;
|
||||
this.logs = [];
|
||||
this.deploymentStatus = '';
|
||||
|
||||
try {
|
||||
// Step 1: Initialize upload session
|
||||
this.currentStep = 'Initializing upload session...';
|
||||
this.addLog('Step 1/5: Initializing upload session');
|
||||
|
||||
const config = {
|
||||
apiEndpoint: this.apiEndpoint,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
organization: this.organization,
|
||||
space: this.space,
|
||||
appName: this.appName,
|
||||
skipSslValidation: this.skipSsl
|
||||
};
|
||||
|
||||
const initResponse = await this.deployService.initUpload(config);
|
||||
this.sessionId = initResponse.uploadSessionId;
|
||||
this.addLog(`✓ Session created: ${this.sessionId}`);
|
||||
this.uploadProgress = 10;
|
||||
|
||||
// Step 2: Upload JAR file
|
||||
this.currentStep = 'Uploading JAR file...';
|
||||
this.addLog(`Step 2/5: Uploading JAR file (${(this.jarFile.size / 1024 / 1024).toFixed(2)}MB)`);
|
||||
|
||||
await this.uploadFileInChunks(this.jarFile, 'jarFile', 10, 50);
|
||||
this.addLog('✓ JAR file upload completed');
|
||||
|
||||
// Step 3: Upload manifest file
|
||||
this.currentStep = 'Uploading manifest file...';
|
||||
this.addLog('Step 3/5: Uploading manifest file');
|
||||
|
||||
await this.uploadFileInChunks(this.manifestFile, 'manifest', 50, 60);
|
||||
this.addLog('✓ Manifest file upload completed');
|
||||
|
||||
// Step 4: Finalize and start deployment
|
||||
this.currentStep = 'Starting deployment...';
|
||||
this.addLog('Step 4/5: Starting async deployment');
|
||||
|
||||
const finalizeResponse = await this.deployService.finalizeUpload(this.sessionId);
|
||||
this.addLog(`✓ Deployment started (Status: ${finalizeResponse.status})`);
|
||||
this.uploadProgress = 70;
|
||||
|
||||
// Step 5: Poll deployment status
|
||||
this.currentStep = 'Deploying application...';
|
||||
this.addLog('Step 5/5: Polling deployment status');
|
||||
|
||||
await this.pollDeploymentStatus();
|
||||
|
||||
} catch (error: any) {
|
||||
this.addLog(`✗ ERROR: ${error.message || error}`);
|
||||
this.currentStep = 'Deployment failed';
|
||||
this.deploymentStatus = 'FAILED';
|
||||
this.uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFileInChunks(file: File, fileType: string, startProgress: number, endProgress: number) {
|
||||
const CHUNK_SIZE = environment.chunkSize;
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
// Convert chunk to base64
|
||||
const base64Chunk = await this.fileToBase64(chunk);
|
||||
|
||||
await this.deployService.uploadChunk(
|
||||
this.sessionId,
|
||||
fileType,
|
||||
file.name,
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
base64Chunk
|
||||
);
|
||||
|
||||
const progress = startProgress + ((chunkIndex + 1) / totalChunks) * (endProgress - startProgress);
|
||||
this.uploadProgress = Math.round(progress);
|
||||
this.addLog(` Chunk ${chunkIndex + 1}/${totalChunks} uploaded`);
|
||||
}
|
||||
}
|
||||
|
||||
async fileToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = (reader.result as string).split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
async pollDeploymentStatus() {
|
||||
const maxAttempts = environment.maxPollAttempts;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, environment.pollInterval));
|
||||
|
||||
const status = await this.deployService.getDeploymentStatus(this.sessionId);
|
||||
this.deploymentStatus = status.status;
|
||||
|
||||
const progressPercent = 70 + (status.progress || 0) * 0.3;
|
||||
this.uploadProgress = Math.round(progressPercent);
|
||||
|
||||
this.addLog(` Status: ${status.status} - ${status.message || ''}`);
|
||||
|
||||
if (status.status === 'COMPLETED') {
|
||||
this.currentStep = 'Deployment completed successfully!';
|
||||
this.uploadProgress = 100;
|
||||
this.uploading = false;
|
||||
this.addLog('✓ Deployment completed successfully!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.status === 'FAILED') {
|
||||
this.currentStep = 'Deployment failed';
|
||||
this.uploading = false;
|
||||
this.addLog(`✗ Deployment failed: ${status.message}`);
|
||||
if (status.error) {
|
||||
this.addLog(` Error: ${status.error}`);
|
||||
}
|
||||
throw new Error('Deployment failed');
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
throw new Error('Deployment timeout after 10 minutes');
|
||||
}
|
||||
|
||||
toggleLogs() {
|
||||
this.logsExpanded = !this.logsExpanded;
|
||||
}
|
||||
|
||||
clearForm() {
|
||||
this.apiEndpoint = '';
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
this.organization = '';
|
||||
this.space = '';
|
||||
this.appName = '';
|
||||
this.skipSsl = false;
|
||||
this.jarFile = null;
|
||||
this.manifestFile = null;
|
||||
this.logs = [];
|
||||
this.uploadProgress = 0;
|
||||
this.currentStep = '';
|
||||
this.deploymentStatus = '';
|
||||
}
|
||||
}
|
||||
97
frontend/src/app/deploy.service.ts
Normal file
97
frontend/src/app/deploy.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export interface CfConfig {
|
||||
apiEndpoint: string;
|
||||
username: string;
|
||||
password: string;
|
||||
organization: string;
|
||||
space: string;
|
||||
appName: string;
|
||||
skipSslValidation: boolean;
|
||||
}
|
||||
|
||||
export interface InitUploadResponse {
|
||||
success: boolean;
|
||||
uploadSessionId: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ChunkUploadResponse {
|
||||
success: boolean;
|
||||
uploadSessionId: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface FinalizeResponse {
|
||||
uploadSessionId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface DeploymentStatus {
|
||||
uploadSessionId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
progress: number;
|
||||
output?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeployService {
|
||||
private apiBase = environment.apiBase;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
async initUpload(config: CfConfig): Promise<InitUploadResponse> {
|
||||
const url = `${this.apiBase}/upload/init`;
|
||||
return firstValueFrom(
|
||||
this.http.post<InitUploadResponse>(url, config)
|
||||
);
|
||||
}
|
||||
|
||||
async uploadChunk(
|
||||
sessionId: string,
|
||||
fileType: string,
|
||||
fileName: string,
|
||||
chunkIndex: number,
|
||||
totalChunks: number,
|
||||
base64Data: string
|
||||
): Promise<ChunkUploadResponse> {
|
||||
const url = `${this.apiBase}/upload/chunk?uploadSessionId=${sessionId}&fileType=${fileType}&chunkIndex=${chunkIndex}&totalChunks=${totalChunks}&fileName=${encodeURIComponent(fileName)}`;
|
||||
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'text/plain',
|
||||
'X-Chunk-Encoding': 'base64'
|
||||
});
|
||||
|
||||
return firstValueFrom(
|
||||
this.http.post<ChunkUploadResponse>(url, base64Data, { headers })
|
||||
);
|
||||
}
|
||||
|
||||
async finalizeUpload(sessionId: string): Promise<FinalizeResponse> {
|
||||
const url = `${this.apiBase}/upload/finalize?uploadSessionId=${sessionId}&async=true`;
|
||||
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Length': '0'
|
||||
});
|
||||
|
||||
return firstValueFrom(
|
||||
this.http.post<FinalizeResponse>(url, null, { headers })
|
||||
);
|
||||
}
|
||||
|
||||
async getDeploymentStatus(sessionId: string): Promise<DeploymentStatus> {
|
||||
const url = `${this.apiBase}/deployment/status/${sessionId}`;
|
||||
return firstValueFrom(
|
||||
this.http.get<DeploymentStatus>(url)
|
||||
);
|
||||
}
|
||||
}
|
||||
20
frontend/src/environments/environment.prod.ts
Normal file
20
frontend/src/environments/environment.prod.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
|
||||
// API Configuration
|
||||
apiBase: '/api/cf',
|
||||
|
||||
// Upload Configuration
|
||||
chunkSize: 1048576, // 1MB in bytes
|
||||
|
||||
// Feature Flags
|
||||
enableSslValidation: false, // Show/hide SSL validation checkbox
|
||||
|
||||
// Polling Configuration
|
||||
pollInterval: 5000, // 5 seconds in milliseconds
|
||||
maxPollAttempts: 120, // 10 minutes total (120 * 5 seconds)
|
||||
|
||||
// UI Configuration
|
||||
defaultLogsExpanded: true,
|
||||
showDebugInfo: false
|
||||
};
|
||||
20
frontend/src/environments/environment.ts
Normal file
20
frontend/src/environments/environment.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
|
||||
// API Configuration
|
||||
apiBase: '/api/cf',
|
||||
|
||||
// Upload Configuration
|
||||
chunkSize: 1048576, // 1MB in bytes
|
||||
|
||||
// Feature Flags
|
||||
enableSslValidation: false, // Show/hide SSL validation checkbox
|
||||
|
||||
// Polling Configuration
|
||||
pollInterval: 5000, // 5 seconds in milliseconds
|
||||
maxPollAttempts: 120, // 10 minutes total (120 * 5 seconds)
|
||||
|
||||
// UI Configuration
|
||||
defaultLogsExpanded: true,
|
||||
showDebugInfo: false
|
||||
};
|
||||
12
frontend/src/index.html
Normal file
12
frontend/src/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CF Deployer</title>
|
||||
<base href="/">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
9
frontend/src/main.ts
Normal file
9
frontend/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideHttpClient()
|
||||
]
|
||||
}).catch(err => console.error(err));
|
||||
17
frontend/src/styles.css
Normal file
17
frontend/src/styles.css
Normal file
@@ -0,0 +1,17 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #010409;
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
30
frontend/tsconfig.json
Normal file
30
frontend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user