Add frontend
This commit is contained in:
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