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:**
|
**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
|
- `CHUNKED_UPLOAD_GUIDE.md` - Detailed API documentation
|
||||||
- `TIMEOUT_SOLUTION.md` - Architecture and design details
|
- `TIMEOUT_SOLUTION.md` - Architecture and design details
|
||||||
- `CHUNK_SIZE_GUIDE.md` - Chunk size recommendations
|
- `CHUNK_SIZE_GUIDE.md` - Chunk size recommendations
|
||||||
- `MEMORY_FIX.md` - JVM memory configuration for Tanzu
|
- `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
|
### 2. List Applications
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
#!/bin/bash
|
#!/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
|
# This script deploys a Java application to Cloud Foundry using chunked
|
||||||
# uploads to bypass nginx size restrictions and async deployment to avoid
|
# uploads to bypass nginx size restrictions and async deployment to avoid
|
||||||
# timeout issues.
|
# 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:
|
# Usage:
|
||||||
# ./deploy-chunked-simple.sh
|
# ./deploy-chunked-simple.sh
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/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
|
# This script deploys a Java application to Cloud Foundry using chunked
|
||||||
# uploads to bypass nginx size restrictions and async deployment to avoid
|
# uploads to bypass nginx size restrictions and async deployment to avoid
|
||||||
# timeout issues.
|
# 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:
|
# Usage:
|
||||||
# ./deploy-chunked.sh
|
# ./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