Spaces:
Paused
Paused
Upload 18 files
Browse files- flare-ui/ReadMe.md +212 -0
- flare-ui/angular.json +79 -0
- flare-ui/package.json +32 -0
- flare-ui/src/app/app.component.ts +21 -0
- flare-ui/src/app/app.routes.ts +45 -0
- flare-ui/src/app/components/activity-log/activity-log.component.ts +192 -0
- flare-ui/src/app/components/environment/environment.component.ts +210 -0
- flare-ui/src/app/components/login/login.component.ts +115 -0
- flare-ui/src/app/components/main/main.component.ts +164 -0
- flare-ui/src/app/guards/auth.guard.ts +14 -0
- flare-ui/src/app/interceptors/auth.interceptor.ts +35 -0
- flare-ui/src/app/services/api.service.ts +205 -0
- flare-ui/src/app/services/auth.service.ts +57 -0
- flare-ui/src/index.html +14 -0
- flare-ui/src/main.ts +15 -0
- flare-ui/src/styles.scss +260 -0
- flare-ui/tsconfig.app.json +13 -0
- flare-ui/tsconfig.json +32 -0
flare-ui/ReadMe.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flare Admin UI Setup
|
| 2 |
+
|
| 3 |
+
## Quick Start (HuggingFace Deployment)
|
| 4 |
+
|
| 5 |
+
Just push all files to HuggingFace Space. The Dockerfile will handle everything:
|
| 6 |
+
- Build Angular UI
|
| 7 |
+
- Install Python dependencies
|
| 8 |
+
- Serve both UI and API on port 7860
|
| 9 |
+
|
| 10 |
+
## Local Development
|
| 11 |
+
|
| 12 |
+
### Backend Setup
|
| 13 |
+
```bash
|
| 14 |
+
# Install Python dependencies
|
| 15 |
+
pip install -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Set encryption key (if needed)
|
| 18 |
+
export FLARE_TOKEN_KEY="your-32-byte-base64-key"
|
| 19 |
+
|
| 20 |
+
# Run backend
|
| 21 |
+
python app.py
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Frontend Setup (for development only)
|
| 25 |
+
```bash
|
| 26 |
+
# Navigate to UI directory
|
| 27 |
+
cd flare-ui
|
| 28 |
+
|
| 29 |
+
# Install dependencies
|
| 30 |
+
npm install
|
| 31 |
+
|
| 32 |
+
# Run development server (proxies to backend on port 7860)
|
| 33 |
+
npm start
|
| 34 |
+
|
| 35 |
+
# Build for production (creates static/ directory)
|
| 36 |
+
npm run build
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## Docker Deployment
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
# Build and run
|
| 43 |
+
docker build -t flare-admin .
|
| 44 |
+
docker run -p 7860:7860 flare-admin
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Access at `http://localhost:7860`
|
| 48 |
+
|
| 49 |
+
## Default Login
|
| 50 |
+
|
| 51 |
+
- Username: `admin`
|
| 52 |
+
- Password: `admin`
|
| 53 |
+
|
| 54 |
+
## Creating Admin User
|
| 55 |
+
|
| 56 |
+
To create a new admin user with proper password hash:
|
| 57 |
+
|
| 58 |
+
```python
|
| 59 |
+
import hashlib
|
| 60 |
+
|
| 61 |
+
password = "your-password"
|
| 62 |
+
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
| 63 |
+
print(f"Password hash: {password_hash}")
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Add the user to `service_config.jsonc`:
|
| 67 |
+
```json
|
| 68 |
+
{
|
| 69 |
+
"config": {
|
| 70 |
+
"users": [
|
| 71 |
+
{
|
| 72 |
+
"username": "newuser",
|
| 73 |
+
"password_hash": "your-hash-here",
|
| 74 |
+
"salt": "random_salt_string"
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Project Structure
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
/
|
| 85 |
+
├── app.py # Main FastAPI application
|
| 86 |
+
├── admin_routes.py # Admin API endpoints
|
| 87 |
+
├── chat_handler.py # Chat functionality
|
| 88 |
+
├── service_config.jsonc # Configuration file
|
| 89 |
+
├── Dockerfile # Handles everything for deployment
|
| 90 |
+
├── flare-ui/ # Angular UI source
|
| 91 |
+
│ ├── src/
|
| 92 |
+
│ │ ├── app/
|
| 93 |
+
│ │ │ ├── components/
|
| 94 |
+
│ │ │ ├── services/
|
| 95 |
+
│ │ │ └── guards/
|
| 96 |
+
│ │ └── index.html
|
| 97 |
+
│ └── package.json
|
| 98 |
+
└── static/ # Built UI files (auto-generated by Docker)
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Features Implemented
|
| 102 |
+
|
| 103 |
+
- ✅ User authentication with JWT
|
| 104 |
+
- ✅ Environment configuration
|
| 105 |
+
- ✅ Project management
|
| 106 |
+
- ✅ Version control
|
| 107 |
+
- ✅ API management
|
| 108 |
+
- ✅ Activity logging
|
| 109 |
+
- ✅ Race condition handling
|
| 110 |
+
- ✅ Import/Export functionality
|
| 111 |
+
|
| 112 |
+
## TODO
|
| 113 |
+
|
| 114 |
+
- [ ] User Info tab (password change)
|
| 115 |
+
- [ ] APIs tab (CRUD operations)
|
| 116 |
+
- [ ] Projects tab (full CRUD)
|
| 117 |
+
- [ ] Test tab implementation
|
| 118 |
+
- [ ] Intent/Parameter dialogs
|
| 119 |
+
- [ ] Version comparison
|
| 120 |
+
- [ ] Auto-save for drafts
|
| 121 |
+
- [ ] Keyboard shortcuts
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
|
| 125 |
+
## ⚠️ Production Deployment Note
|
| 126 |
+
|
| 127 |
+
**This setup is optimized for HuggingFace Spaces and development environments.** For production on-premise deployment, a more robust architecture will be implemented.
|
| 128 |
+
|
| 129 |
+
### Planned Production Architecture:
|
| 130 |
+
|
| 131 |
+
#### 1. **Web Server Layer**
|
| 132 |
+
- **Nginx** as reverse proxy and static file server
|
| 133 |
+
- SSL/TLS termination
|
| 134 |
+
- Request rate limiting and security headers
|
| 135 |
+
- Gzip compression for static assets
|
| 136 |
+
|
| 137 |
+
#### 2. **Application Layer**
|
| 138 |
+
- **Gunicorn/Uvicorn** workers for Python ASGI
|
| 139 |
+
- Process management with **Supervisor** or **systemd**
|
| 140 |
+
- Horizontal scaling with multiple worker processes
|
| 141 |
+
- Health check endpoints for monitoring
|
| 142 |
+
|
| 143 |
+
#### 3. **Session & Cache Layer**
|
| 144 |
+
- **Redis** for distributed session storage
|
| 145 |
+
- Centralized cache for LLM responses
|
| 146 |
+
- Session affinity for WebSocket connections
|
| 147 |
+
|
| 148 |
+
#### 4. **Database Layer** (Optional)
|
| 149 |
+
- **PostgreSQL** for configuration storage (replacing JSON file)
|
| 150 |
+
- Backup and replication strategies
|
| 151 |
+
- Migration tools for schema updates
|
| 152 |
+
|
| 153 |
+
#### 5. **Monitoring & Logging**
|
| 154 |
+
- **Prometheus** + **Grafana** for metrics
|
| 155 |
+
- **ELK Stack** (Elasticsearch, Logstash, Kibana) for log aggregation
|
| 156 |
+
- Application Performance Monitoring (APM)
|
| 157 |
+
- Alert configuration for critical events
|
| 158 |
+
|
| 159 |
+
#### 6. **Deployment Options**
|
| 160 |
+
|
| 161 |
+
**Option A: Docker Compose** (Small-Medium Scale)
|
| 162 |
+
```yaml
|
| 163 |
+
services:
|
| 164 |
+
nginx:
|
| 165 |
+
image: nginx:alpine
|
| 166 |
+
volumes:
|
| 167 |
+
- ./nginx.conf:/etc/nginx/nginx.conf
|
| 168 |
+
- ./static:/usr/share/nginx/html
|
| 169 |
+
ports:
|
| 170 |
+
- "80:80"
|
| 171 |
+
- "443:443"
|
| 172 |
+
|
| 173 |
+
app:
|
| 174 |
+
image: flare-admin:production
|
| 175 |
+
scale: 3 # 3 instances
|
| 176 |
+
environment:
|
| 177 |
+
- REDIS_URL=redis://redis:6379
|
| 178 |
+
|
| 179 |
+
redis:
|
| 180 |
+
image: redis:alpine
|
| 181 |
+
volumes:
|
| 182 |
+
- redis-data:/data
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
**Option B: Kubernetes** (Large Scale)
|
| 186 |
+
- Helm charts for easy deployment
|
| 187 |
+
- Horizontal Pod Autoscaler (HPA)
|
| 188 |
+
- Ingress controller for routing
|
| 189 |
+
- Persistent Volume Claims for data
|
| 190 |
+
- Secrets management for credentials
|
| 191 |
+
|
| 192 |
+
#### 7. **Security Considerations**
|
| 193 |
+
- JWT token rotation and refresh
|
| 194 |
+
- API rate limiting per user
|
| 195 |
+
- Input validation and sanitization
|
| 196 |
+
- Regular security audits
|
| 197 |
+
- Compliance with data protection regulations
|
| 198 |
+
|
| 199 |
+
#### 8. **Backup & Disaster Recovery**
|
| 200 |
+
- Automated daily backups
|
| 201 |
+
- Point-in-time recovery
|
| 202 |
+
- Geo-redundant storage
|
| 203 |
+
- Disaster recovery procedures
|
| 204 |
+
- RTO/RPO targets defined
|
| 205 |
+
|
| 206 |
+
### Production Deployment Timeline:
|
| 207 |
+
1. **Phase 1**: Current setup (HF Spaces, development)
|
| 208 |
+
2. **Phase 2**: Docker Compose setup for on-premise pilot
|
| 209 |
+
3. **Phase 3**: Full production architecture with monitoring
|
| 210 |
+
4. **Phase 4**: Kubernetes deployment for enterprise scale
|
| 211 |
+
|
| 212 |
+
A comprehensive `DEPLOYMENT.md` guide will be created when transitioning to production architecture.
|
flare-ui/angular.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"newProjectRoot": "projects",
|
| 5 |
+
"projects": {
|
| 6 |
+
"flare-ui": {
|
| 7 |
+
"projectType": "application",
|
| 8 |
+
"schematics": {
|
| 9 |
+
"@schematics/angular:component": {
|
| 10 |
+
"style": "scss"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"root": "",
|
| 14 |
+
"sourceRoot": "src",
|
| 15 |
+
"prefix": "app",
|
| 16 |
+
"architect": {
|
| 17 |
+
"build": {
|
| 18 |
+
"builder": "@angular-devkit/build-angular:browser",
|
| 19 |
+
"options": {
|
| 20 |
+
"outputPath": "dist/flare-ui",
|
| 21 |
+
"index": "src/index.html",
|
| 22 |
+
"main": "src/main.ts",
|
| 23 |
+
"polyfills": [
|
| 24 |
+
"zone.js"
|
| 25 |
+
],
|
| 26 |
+
"tsConfig": "tsconfig.app.json",
|
| 27 |
+
"inlineStyleLanguage": "scss",
|
| 28 |
+
"assets": [
|
| 29 |
+
"src/favicon.ico",
|
| 30 |
+
"src/assets"
|
| 31 |
+
],
|
| 32 |
+
"styles": [
|
| 33 |
+
"src/styles.scss"
|
| 34 |
+
],
|
| 35 |
+
"scripts": []
|
| 36 |
+
},
|
| 37 |
+
"configurations": {
|
| 38 |
+
"production": {
|
| 39 |
+
"budgets": [
|
| 40 |
+
{
|
| 41 |
+
"type": "initial",
|
| 42 |
+
"maximumWarning": "500kb",
|
| 43 |
+
"maximumError": "1mb"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"type": "anyComponentStyle",
|
| 47 |
+
"maximumWarning": "2kb",
|
| 48 |
+
"maximumError": "4kb"
|
| 49 |
+
}
|
| 50 |
+
],
|
| 51 |
+
"outputHashing": "all"
|
| 52 |
+
},
|
| 53 |
+
"development": {
|
| 54 |
+
"buildOptimizer": false,
|
| 55 |
+
"optimization": false,
|
| 56 |
+
"vendorChunk": true,
|
| 57 |
+
"extractLicenses": false,
|
| 58 |
+
"sourceMap": true,
|
| 59 |
+
"namedChunks": true
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"defaultConfiguration": "production"
|
| 63 |
+
},
|
| 64 |
+
"serve": {
|
| 65 |
+
"builder": "@angular-devkit/build-angular:dev-server",
|
| 66 |
+
"configurations": {
|
| 67 |
+
"production": {
|
| 68 |
+
"buildTarget": "flare-ui:build:production"
|
| 69 |
+
},
|
| 70 |
+
"development": {
|
| 71 |
+
"buildTarget": "flare-ui:build:development"
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"defaultConfiguration": "development"
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
flare-ui/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "flare-ui",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"ng": "ng",
|
| 6 |
+
"start": "ng serve",
|
| 7 |
+
"build": "ng build --configuration production --output-path ../static",
|
| 8 |
+
"watch": "ng build --watch --configuration development",
|
| 9 |
+
"test": "ng test"
|
| 10 |
+
},
|
| 11 |
+
"private": true,
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@angular/animations": "^17.0.0",
|
| 14 |
+
"@angular/common": "^17.0.0",
|
| 15 |
+
"@angular/compiler": "^17.0.0",
|
| 16 |
+
"@angular/core": "^17.0.0",
|
| 17 |
+
"@angular/forms": "^17.0.0",
|
| 18 |
+
"@angular/platform-browser": "^17.0.0",
|
| 19 |
+
"@angular/platform-browser-dynamic": "^17.0.0",
|
| 20 |
+
"@angular/router": "^17.0.0",
|
| 21 |
+
"rxjs": "~7.8.0",
|
| 22 |
+
"tslib": "^2.3.0",
|
| 23 |
+
"zone.js": "~0.14.2"
|
| 24 |
+
},
|
| 25 |
+
"devDependencies": {
|
| 26 |
+
"@angular-devkit/build-angular": "^17.0.0",
|
| 27 |
+
"@angular/cli": "^17.0.0",
|
| 28 |
+
"@angular/compiler-cli": "^17.0.0",
|
| 29 |
+
"@types/node": "^20.0.0",
|
| 30 |
+
"typescript": "~5.2.0"
|
| 31 |
+
}
|
| 32 |
+
}
|
flare-ui/src/app/app.component.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { RouterOutlet } from '@angular/router';
|
| 4 |
+
|
| 5 |
+
@Component({
|
| 6 |
+
selector: 'app-root',
|
| 7 |
+
standalone: true,
|
| 8 |
+
imports: [CommonModule, RouterOutlet],
|
| 9 |
+
template: `
|
| 10 |
+
<router-outlet></router-outlet>
|
| 11 |
+
`,
|
| 12 |
+
styles: [`
|
| 13 |
+
:host {
|
| 14 |
+
display: block;
|
| 15 |
+
height: 100vh;
|
| 16 |
+
}
|
| 17 |
+
`]
|
| 18 |
+
})
|
| 19 |
+
export class AppComponent {
|
| 20 |
+
title = 'Flare Administration';
|
| 21 |
+
}
|
flare-ui/src/app/app.routes.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Routes } from '@angular/router';
|
| 2 |
+
import { authGuard } from './guards/auth.guard';
|
| 3 |
+
|
| 4 |
+
export const routes: Routes = [
|
| 5 |
+
{
|
| 6 |
+
path: 'login',
|
| 7 |
+
loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent)
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
path: '',
|
| 11 |
+
loadComponent: () => import('./components/main/main.component').then(m => m.MainComponent),
|
| 12 |
+
canActivate: [authGuard],
|
| 13 |
+
children: [
|
| 14 |
+
{
|
| 15 |
+
path: 'user-info',
|
| 16 |
+
loadComponent: () => import('./components/user-info/user-info.component').then(m => m.UserInfoComponent)
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
path: 'environment',
|
| 20 |
+
loadComponent: () => import('./components/environment/environment.component').then(m => m.EnvironmentComponent)
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
path: 'apis',
|
| 24 |
+
loadComponent: () => import('./components/apis/apis.component').then(m => m.ApisComponent)
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
path: 'projects',
|
| 28 |
+
loadComponent: () => import('./components/projects/projects.component').then(m => m.ProjectsComponent)
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
path: 'test',
|
| 32 |
+
loadComponent: () => import('./components/test/test.component').then(m => m.TestComponent)
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
path: '',
|
| 36 |
+
redirectTo: 'projects',
|
| 37 |
+
pathMatch: 'full'
|
| 38 |
+
}
|
| 39 |
+
]
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
path: '**',
|
| 43 |
+
redirectTo: ''
|
| 44 |
+
}
|
| 45 |
+
];
|
flare-ui/src/app/components/activity-log/activity-log.component.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, EventEmitter, Output, inject, OnInit } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { HttpClient } from '@angular/common/http';
|
| 4 |
+
|
| 5 |
+
interface ActivityLog {
|
| 6 |
+
id: number;
|
| 7 |
+
timestamp: string;
|
| 8 |
+
user: string;
|
| 9 |
+
action: string;
|
| 10 |
+
entity_type: string;
|
| 11 |
+
entity_id: any;
|
| 12 |
+
entity_name: string;
|
| 13 |
+
details?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
@Component({
|
| 17 |
+
selector: 'app-activity-log',
|
| 18 |
+
standalone: true,
|
| 19 |
+
imports: [CommonModule],
|
| 20 |
+
template: `
|
| 21 |
+
<div class="activity-log-dropdown" (click)="$event.stopPropagation()">
|
| 22 |
+
<div class="activity-header">
|
| 23 |
+
<h3>🔔 Recent Activities</h3>
|
| 24 |
+
<button class="close-btn" (click)="close.emit()">×</button>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="activity-list">
|
| 27 |
+
@if (loading) {
|
| 28 |
+
<div class="loading">Loading...</div>
|
| 29 |
+
} @else if (activities.length === 0) {
|
| 30 |
+
<div class="empty">No recent activities</div>
|
| 31 |
+
} @else {
|
| 32 |
+
@for (activity of activities; track activity.id) {
|
| 33 |
+
<div class="activity-item">
|
| 34 |
+
<div class="activity-time">{{ getRelativeTime(activity.timestamp) }}</div>
|
| 35 |
+
<div class="activity-content">
|
| 36 |
+
<strong>{{ activity.user }}</strong> {{ getActionText(activity) }}
|
| 37 |
+
<em>{{ activity.entity_name }}</em>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
</div>
|
| 43 |
+
<div class="activity-footer">
|
| 44 |
+
<button class="btn btn-secondary" (click)="loadMore()">View All Activities</button>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
`,
|
| 48 |
+
styles: [`
|
| 49 |
+
.activity-log-dropdown {
|
| 50 |
+
position: absolute;
|
| 51 |
+
top: 100%;
|
| 52 |
+
right: 0;
|
| 53 |
+
width: 350px;
|
| 54 |
+
background: white;
|
| 55 |
+
border: 1px solid #dee2e6;
|
| 56 |
+
border-radius: 8px;
|
| 57 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 58 |
+
z-index: 1000;
|
| 59 |
+
margin-top: 0.5rem;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.activity-header {
|
| 63 |
+
padding: 1rem;
|
| 64 |
+
border-bottom: 1px solid #dee2e6;
|
| 65 |
+
display: flex;
|
| 66 |
+
justify-content: space-between;
|
| 67 |
+
align-items: center;
|
| 68 |
+
|
| 69 |
+
h3 {
|
| 70 |
+
margin: 0;
|
| 71 |
+
font-size: 1.1rem;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.close-btn {
|
| 75 |
+
background: none;
|
| 76 |
+
border: none;
|
| 77 |
+
font-size: 1.5rem;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
color: #6c757d;
|
| 80 |
+
|
| 81 |
+
&:hover {
|
| 82 |
+
color: #333;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.activity-list {
|
| 88 |
+
max-height: 300px;
|
| 89 |
+
overflow-y: auto;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.activity-item {
|
| 93 |
+
padding: 0.75rem 1rem;
|
| 94 |
+
border-bottom: 1px solid #f0f0f0;
|
| 95 |
+
|
| 96 |
+
&:hover {
|
| 97 |
+
background-color: #f8f9fa;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.activity-time {
|
| 101 |
+
font-size: 0.85rem;
|
| 102 |
+
color: #6c757d;
|
| 103 |
+
margin-bottom: 0.25rem;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.activity-content {
|
| 107 |
+
font-size: 0.9rem;
|
| 108 |
+
|
| 109 |
+
em {
|
| 110 |
+
color: #007bff;
|
| 111 |
+
font-style: normal;
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.activity-footer {
|
| 117 |
+
padding: 0.75rem;
|
| 118 |
+
border-top: 1px solid #dee2e6;
|
| 119 |
+
text-align: center;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.loading, .empty {
|
| 123 |
+
padding: 2rem;
|
| 124 |
+
text-align: center;
|
| 125 |
+
color: #6c757d;
|
| 126 |
+
}
|
| 127 |
+
`]
|
| 128 |
+
})
|
| 129 |
+
export class ActivityLogComponent implements OnInit {
|
| 130 |
+
@Output() close = new EventEmitter<void>();
|
| 131 |
+
|
| 132 |
+
private http = inject(HttpClient);
|
| 133 |
+
|
| 134 |
+
activities: ActivityLog[] = [];
|
| 135 |
+
loading = true;
|
| 136 |
+
|
| 137 |
+
ngOnInit() {
|
| 138 |
+
this.loadActivities();
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
loadActivities() {
|
| 142 |
+
this.loading = true;
|
| 143 |
+
this.http.get<ActivityLog[]>('/api/activity-log?limit=10')
|
| 144 |
+
.subscribe({
|
| 145 |
+
next: (data) => {
|
| 146 |
+
this.activities = data;
|
| 147 |
+
this.loading = false;
|
| 148 |
+
},
|
| 149 |
+
error: () => {
|
| 150 |
+
this.loading = false;
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
loadMore() {
|
| 156 |
+
// TODO: Implement full activity log view
|
| 157 |
+
console.log('Load more activities');
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
getRelativeTime(timestamp: string): string {
|
| 161 |
+
const date = new Date(timestamp);
|
| 162 |
+
const now = new Date();
|
| 163 |
+
const diff = now.getTime() - date.getTime();
|
| 164 |
+
|
| 165 |
+
const minutes = Math.floor(diff / 60000);
|
| 166 |
+
const hours = Math.floor(diff / 3600000);
|
| 167 |
+
const days = Math.floor(diff / 86400000);
|
| 168 |
+
|
| 169 |
+
if (minutes < 1) return 'just now';
|
| 170 |
+
if (minutes < 60) return `${minutes} min ago`;
|
| 171 |
+
if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
| 172 |
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
getActionText(activity: ActivityLog): string {
|
| 176 |
+
const actions: Record<string, string> = {
|
| 177 |
+
'CREATE_PROJECT': 'created project',
|
| 178 |
+
'UPDATE_PROJECT': 'updated project',
|
| 179 |
+
'DELETE_PROJECT': 'deleted project',
|
| 180 |
+
'PUBLISH_VERSION': 'published version',
|
| 181 |
+
'CREATE_VERSION': 'created version',
|
| 182 |
+
'UPDATE_VERSION': 'updated version',
|
| 183 |
+
'CREATE_API': 'created API',
|
| 184 |
+
'UPDATE_API': 'updated API',
|
| 185 |
+
'DELETE_API': 'deleted API',
|
| 186 |
+
'UPDATE_ENVIRONMENT': 'updated environment',
|
| 187 |
+
'IMPORT_PROJECT': 'imported project'
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
return actions[activity.action] || activity.action.toLowerCase().replace('_', ' ');
|
| 191 |
+
}
|
| 192 |
+
}
|
flare-ui/src/app/components/environment/environment.component.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, inject, OnInit } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { FormsModule } from '@angular/forms';
|
| 4 |
+
import { ApiService, Environment } from '../../services/api.service';
|
| 5 |
+
|
| 6 |
+
@Component({
|
| 7 |
+
selector: 'app-environment',
|
| 8 |
+
standalone: true,
|
| 9 |
+
imports: [CommonModule, FormsModule],
|
| 10 |
+
template: `
|
| 11 |
+
<div class="environment-container">
|
| 12 |
+
<h2>Environment Configuration</h2>
|
| 13 |
+
|
| 14 |
+
<div class="card">
|
| 15 |
+
<div class="card-body">
|
| 16 |
+
<form (ngSubmit)="save()" #envForm="ngForm">
|
| 17 |
+
<div class="form-group">
|
| 18 |
+
<label for="workMode">Work Mode *</label>
|
| 19 |
+
<select
|
| 20 |
+
id="workMode"
|
| 21 |
+
name="workMode"
|
| 22 |
+
[(ngModel)]="environment.work_mode"
|
| 23 |
+
(change)="onWorkModeChange()"
|
| 24 |
+
required
|
| 25 |
+
[disabled]="loading"
|
| 26 |
+
>
|
| 27 |
+
<option value="hfcloud">HF Cloud</option>
|
| 28 |
+
<option value="cloud">Cloud</option>
|
| 29 |
+
<option value="on-premise">On-Premise</option>
|
| 30 |
+
</select>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="form-group">
|
| 34 |
+
<label for="cloudToken">Cloud Token</label>
|
| 35 |
+
<input
|
| 36 |
+
type="password"
|
| 37 |
+
id="cloudToken"
|
| 38 |
+
name="cloudToken"
|
| 39 |
+
[(ngModel)]="environment.cloud_token"
|
| 40 |
+
[disabled]="loading || environment.work_mode === 'on-premise'"
|
| 41 |
+
placeholder="Enter cloud token"
|
| 42 |
+
>
|
| 43 |
+
<small class="text-muted">Required for HF Cloud and Cloud modes</small>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="form-group">
|
| 47 |
+
<label for="sparkEndpoint">Spark Endpoint *</label>
|
| 48 |
+
<div class="input-with-button">
|
| 49 |
+
<input
|
| 50 |
+
type="url"
|
| 51 |
+
id="sparkEndpoint"
|
| 52 |
+
name="sparkEndpoint"
|
| 53 |
+
[(ngModel)]="environment.spark_endpoint"
|
| 54 |
+
required
|
| 55 |
+
[disabled]="loading"
|
| 56 |
+
placeholder="https://spark-service.example.com"
|
| 57 |
+
>
|
| 58 |
+
<button
|
| 59 |
+
type="button"
|
| 60 |
+
class="btn btn-secondary"
|
| 61 |
+
(click)="testConnection()"
|
| 62 |
+
[disabled]="loading || !environment.spark_endpoint"
|
| 63 |
+
>
|
| 64 |
+
Test Connection
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
@if (message) {
|
| 70 |
+
<div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError">
|
| 71 |
+
{{ message }}
|
| 72 |
+
</div>
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
<div class="form-actions">
|
| 76 |
+
<button
|
| 77 |
+
type="submit"
|
| 78 |
+
class="btn btn-primary"
|
| 79 |
+
[disabled]="loading || !envForm.valid"
|
| 80 |
+
>
|
| 81 |
+
@if (saving) {
|
| 82 |
+
<span class="spinner"></span> Saving...
|
| 83 |
+
} @else {
|
| 84 |
+
Save
|
| 85 |
+
}
|
| 86 |
+
</button>
|
| 87 |
+
<button
|
| 88 |
+
type="button"
|
| 89 |
+
class="btn btn-secondary"
|
| 90 |
+
(click)="reloadFromSpark()"
|
| 91 |
+
[disabled]="loading"
|
| 92 |
+
>
|
| 93 |
+
Reload from Spark
|
| 94 |
+
</button>
|
| 95 |
+
</div>
|
| 96 |
+
</form>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
`,
|
| 101 |
+
styles: [`
|
| 102 |
+
.environment-container {
|
| 103 |
+
h2 {
|
| 104 |
+
margin-bottom: 1.5rem;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.input-with-button {
|
| 109 |
+
display: flex;
|
| 110 |
+
gap: 0.5rem;
|
| 111 |
+
|
| 112 |
+
input {
|
| 113 |
+
flex: 1;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.form-actions {
|
| 118 |
+
display: flex;
|
| 119 |
+
gap: 0.5rem;
|
| 120 |
+
margin-top: 1.5rem;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.text-muted {
|
| 124 |
+
color: #6c757d;
|
| 125 |
+
font-size: 0.875rem;
|
| 126 |
+
}
|
| 127 |
+
`]
|
| 128 |
+
})
|
| 129 |
+
export class EnvironmentComponent implements OnInit {
|
| 130 |
+
private apiService = inject(ApiService);
|
| 131 |
+
|
| 132 |
+
environment: Environment = {
|
| 133 |
+
work_mode: 'hfcloud',
|
| 134 |
+
cloud_token: '',
|
| 135 |
+
spark_endpoint: ''
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
loading = true;
|
| 139 |
+
saving = false;
|
| 140 |
+
message = '';
|
| 141 |
+
isError = false;
|
| 142 |
+
|
| 143 |
+
ngOnInit() {
|
| 144 |
+
this.loadEnvironment();
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
loadEnvironment() {
|
| 148 |
+
this.loading = true;
|
| 149 |
+
this.apiService.getEnvironment().subscribe({
|
| 150 |
+
next: (env) => {
|
| 151 |
+
this.environment = env;
|
| 152 |
+
this.loading = false;
|
| 153 |
+
},
|
| 154 |
+
error: (err) => {
|
| 155 |
+
this.showMessage('Failed to load environment configuration', true);
|
| 156 |
+
this.loading = false;
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
onWorkModeChange() {
|
| 162 |
+
if (this.environment.work_mode === 'on-premise') {
|
| 163 |
+
this.environment.cloud_token = '';
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
save() {
|
| 168 |
+
this.saving = true;
|
| 169 |
+
this.message = '';
|
| 170 |
+
|
| 171 |
+
this.apiService.updateEnvironment(this.environment).subscribe({
|
| 172 |
+
next: () => {
|
| 173 |
+
this.showMessage('Environment configuration saved successfully', false);
|
| 174 |
+
this.saving = false;
|
| 175 |
+
},
|
| 176 |
+
error: (err) => {
|
| 177 |
+
this.showMessage(err.error?.detail || 'Failed to save configuration', true);
|
| 178 |
+
this.saving = false;
|
| 179 |
+
}
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
testConnection() {
|
| 184 |
+
// TODO: Implement connection test
|
| 185 |
+
this.showMessage('Testing connection to Spark endpoint...', false);
|
| 186 |
+
setTimeout(() => {
|
| 187 |
+
this.showMessage('Connection successful!', false);
|
| 188 |
+
}, 1000);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
reloadFromSpark() {
|
| 192 |
+
// TODO: Implement reload from Spark
|
| 193 |
+
this.showMessage('Reloading configuration from Spark...', false);
|
| 194 |
+
setTimeout(() => {
|
| 195 |
+
this.loadEnvironment();
|
| 196 |
+
this.showMessage('Configuration reloaded', false);
|
| 197 |
+
}, 1000);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
private showMessage(message: string, isError: boolean) {
|
| 201 |
+
this.message = message;
|
| 202 |
+
this.isError = isError;
|
| 203 |
+
|
| 204 |
+
if (!isError) {
|
| 205 |
+
setTimeout(() => {
|
| 206 |
+
this.message = '';
|
| 207 |
+
}, 5000);
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
flare-ui/src/app/components/login/login.component.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, inject } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { FormsModule } from '@angular/forms';
|
| 4 |
+
import { Router } from '@angular/router';
|
| 5 |
+
import { AuthService } from '../../services/auth.service';
|
| 6 |
+
|
| 7 |
+
@Component({
|
| 8 |
+
selector: 'app-login',
|
| 9 |
+
standalone: true,
|
| 10 |
+
imports: [CommonModule, FormsModule],
|
| 11 |
+
template: `
|
| 12 |
+
<div class="login-container">
|
| 13 |
+
<div class="login-card">
|
| 14 |
+
<h1>Flare Administration</h1>
|
| 15 |
+
<form (ngSubmit)="login()" #loginForm="ngForm">
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="username">Username</label>
|
| 18 |
+
<input
|
| 19 |
+
type="text"
|
| 20 |
+
id="username"
|
| 21 |
+
name="username"
|
| 22 |
+
[(ngModel)]="username"
|
| 23 |
+
required
|
| 24 |
+
[disabled]="loading"
|
| 25 |
+
>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="form-group">
|
| 28 |
+
<label for="password">Password</label>
|
| 29 |
+
<input
|
| 30 |
+
type="password"
|
| 31 |
+
id="password"
|
| 32 |
+
name="password"
|
| 33 |
+
[(ngModel)]="password"
|
| 34 |
+
required
|
| 35 |
+
[disabled]="loading"
|
| 36 |
+
>
|
| 37 |
+
</div>
|
| 38 |
+
@if (error) {
|
| 39 |
+
<div class="alert alert-danger">{{ error }}</div>
|
| 40 |
+
}
|
| 41 |
+
<button
|
| 42 |
+
type="submit"
|
| 43 |
+
class="btn btn-primary w-100"
|
| 44 |
+
[disabled]="loading || !loginForm.valid"
|
| 45 |
+
>
|
| 46 |
+
@if (loading) {
|
| 47 |
+
<span class="spinner"></span> Logging in...
|
| 48 |
+
} @else {
|
| 49 |
+
Login
|
| 50 |
+
}
|
| 51 |
+
</button>
|
| 52 |
+
</form>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
`,
|
| 56 |
+
styles: [`
|
| 57 |
+
.login-container {
|
| 58 |
+
min-height: 100vh;
|
| 59 |
+
display: flex;
|
| 60 |
+
align-items: center;
|
| 61 |
+
justify-content: center;
|
| 62 |
+
background-color: #f5f5f5;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.login-card {
|
| 66 |
+
background: white;
|
| 67 |
+
padding: 2rem;
|
| 68 |
+
border-radius: 8px;
|
| 69 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 70 |
+
width: 100%;
|
| 71 |
+
max-width: 400px;
|
| 72 |
+
|
| 73 |
+
h1 {
|
| 74 |
+
text-align: center;
|
| 75 |
+
margin-bottom: 2rem;
|
| 76 |
+
color: #333;
|
| 77 |
+
font-size: 1.5rem;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.w-100 {
|
| 82 |
+
width: 100%;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
button {
|
| 86 |
+
display: flex;
|
| 87 |
+
align-items: center;
|
| 88 |
+
justify-content: center;
|
| 89 |
+
gap: 0.5rem;
|
| 90 |
+
}
|
| 91 |
+
`]
|
| 92 |
+
})
|
| 93 |
+
export class LoginComponent {
|
| 94 |
+
private authService = inject(AuthService);
|
| 95 |
+
private router = inject(Router);
|
| 96 |
+
|
| 97 |
+
username = '';
|
| 98 |
+
password = '';
|
| 99 |
+
loading = false;
|
| 100 |
+
error = '';
|
| 101 |
+
|
| 102 |
+
async login() {
|
| 103 |
+
this.loading = true;
|
| 104 |
+
this.error = '';
|
| 105 |
+
|
| 106 |
+
try {
|
| 107 |
+
await this.authService.login(this.username, this.password).toPromise();
|
| 108 |
+
this.router.navigate(['/']);
|
| 109 |
+
} catch (err: any) {
|
| 110 |
+
this.error = err.error?.detail || 'Invalid credentials';
|
| 111 |
+
} finally {
|
| 112 |
+
this.loading = false;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
flare-ui/src/app/components/main/main.component.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, inject } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
| 4 |
+
import { AuthService } from '../../services/auth.service';
|
| 5 |
+
import { ActivityLogComponent } from '../activity-log/activity-log.component';
|
| 6 |
+
|
| 7 |
+
@Component({
|
| 8 |
+
selector: 'app-main',
|
| 9 |
+
standalone: true,
|
| 10 |
+
imports: [CommonModule, RouterLink, RouterLinkActive, RouterOutlet, ActivityLogComponent],
|
| 11 |
+
template: `
|
| 12 |
+
<div class="main-layout">
|
| 13 |
+
<header class="header">
|
| 14 |
+
<div class="header-content">
|
| 15 |
+
<h1>Flare Administration</h1>
|
| 16 |
+
<div class="header-actions">
|
| 17 |
+
<span class="username">{{ username }}</span>
|
| 18 |
+
<button class="notification-btn" (click)="toggleActivityLog()">
|
| 19 |
+
🔔
|
| 20 |
+
@if (showActivityLog) {
|
| 21 |
+
<app-activity-log (close)="toggleActivityLog()"></app-activity-log>
|
| 22 |
+
}
|
| 23 |
+
</button>
|
| 24 |
+
<button class="btn btn-secondary" (click)="logout()">Logout</button>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
</header>
|
| 28 |
+
|
| 29 |
+
<nav class="tabs">
|
| 30 |
+
<a
|
| 31 |
+
routerLink="/user-info"
|
| 32 |
+
routerLinkActive="active"
|
| 33 |
+
class="tab"
|
| 34 |
+
>
|
| 35 |
+
User Info
|
| 36 |
+
</a>
|
| 37 |
+
<a
|
| 38 |
+
routerLink="/environment"
|
| 39 |
+
routerLinkActive="active"
|
| 40 |
+
class="tab"
|
| 41 |
+
>
|
| 42 |
+
Environment
|
| 43 |
+
</a>
|
| 44 |
+
<a
|
| 45 |
+
routerLink="/apis"
|
| 46 |
+
routerLinkActive="active"
|
| 47 |
+
class="tab"
|
| 48 |
+
>
|
| 49 |
+
APIs
|
| 50 |
+
</a>
|
| 51 |
+
<a
|
| 52 |
+
routerLink="/projects"
|
| 53 |
+
routerLinkActive="active"
|
| 54 |
+
class="tab"
|
| 55 |
+
>
|
| 56 |
+
Projects
|
| 57 |
+
</a>
|
| 58 |
+
<a
|
| 59 |
+
routerLink="/test"
|
| 60 |
+
routerLinkActive="active"
|
| 61 |
+
class="tab"
|
| 62 |
+
>
|
| 63 |
+
Test
|
| 64 |
+
</a>
|
| 65 |
+
</nav>
|
| 66 |
+
|
| 67 |
+
<main class="content">
|
| 68 |
+
<router-outlet></router-outlet>
|
| 69 |
+
</main>
|
| 70 |
+
</div>
|
| 71 |
+
`,
|
| 72 |
+
styles: [`
|
| 73 |
+
.main-layout {
|
| 74 |
+
height: 100vh;
|
| 75 |
+
display: flex;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.header {
|
| 80 |
+
background-color: #fff;
|
| 81 |
+
border-bottom: 1px solid #dee2e6;
|
| 82 |
+
padding: 1rem 0;
|
| 83 |
+
|
| 84 |
+
.header-content {
|
| 85 |
+
max-width: 1200px;
|
| 86 |
+
margin: 0 auto;
|
| 87 |
+
padding: 0 20px;
|
| 88 |
+
display: flex;
|
| 89 |
+
justify-content: space-between;
|
| 90 |
+
align-items: center;
|
| 91 |
+
|
| 92 |
+
h1 {
|
| 93 |
+
font-size: 1.5rem;
|
| 94 |
+
color: #333;
|
| 95 |
+
margin: 0;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.header-actions {
|
| 100 |
+
display: flex;
|
| 101 |
+
align-items: center;
|
| 102 |
+
gap: 1rem;
|
| 103 |
+
|
| 104 |
+
.username {
|
| 105 |
+
font-weight: 500;
|
| 106 |
+
color: #495057;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.notification-btn {
|
| 110 |
+
position: relative;
|
| 111 |
+
background: none;
|
| 112 |
+
border: none;
|
| 113 |
+
font-size: 1.25rem;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
padding: 0.25rem;
|
| 116 |
+
|
| 117 |
+
&:hover {
|
| 118 |
+
opacity: 0.7;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.tabs {
|
| 125 |
+
background-color: #fff;
|
| 126 |
+
border-bottom: 2px solid #dee2e6;
|
| 127 |
+
max-width: 1200px;
|
| 128 |
+
width: 100%;
|
| 129 |
+
margin: 0 auto;
|
| 130 |
+
padding: 0 20px;
|
| 131 |
+
display: flex;
|
| 132 |
+
|
| 133 |
+
a {
|
| 134 |
+
text-decoration: none;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.content {
|
| 139 |
+
flex: 1;
|
| 140 |
+
overflow-y: auto;
|
| 141 |
+
padding: 2rem 0;
|
| 142 |
+
|
| 143 |
+
> * {
|
| 144 |
+
max-width: 1200px;
|
| 145 |
+
margin: 0 auto;
|
| 146 |
+
padding: 0 20px;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
`]
|
| 150 |
+
})
|
| 151 |
+
export class MainComponent {
|
| 152 |
+
private authService = inject(AuthService);
|
| 153 |
+
|
| 154 |
+
username = this.authService.getUsername() || '';
|
| 155 |
+
showActivityLog = false;
|
| 156 |
+
|
| 157 |
+
logout() {
|
| 158 |
+
this.authService.logout();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
toggleActivityLog() {
|
| 162 |
+
this.showActivityLog = !this.showActivityLog;
|
| 163 |
+
}
|
| 164 |
+
}
|
flare-ui/src/app/guards/auth.guard.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { inject } from '@angular/core';
|
| 2 |
+
import { Router } from '@angular/router';
|
| 3 |
+
import { AuthService } from '../services/auth.service';
|
| 4 |
+
|
| 5 |
+
export const authGuard = () => {
|
| 6 |
+
const authService = inject(AuthService);
|
| 7 |
+
const router = inject(Router);
|
| 8 |
+
|
| 9 |
+
if (authService.isLoggedIn()) {
|
| 10 |
+
return true;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
return router.createUrlTree(['/login']);
|
| 14 |
+
};
|
flare-ui/src/app/interceptors/auth.interceptor.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
| 2 |
+
import { inject } from '@angular/core';
|
| 3 |
+
import { Router } from '@angular/router';
|
| 4 |
+
import { catchError, throwError } from 'rxjs';
|
| 5 |
+
import { AuthService } from '../services/auth.service';
|
| 6 |
+
|
| 7 |
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
| 8 |
+
const authService = inject(AuthService);
|
| 9 |
+
const router = inject(Router);
|
| 10 |
+
|
| 11 |
+
// Skip auth for login endpoint
|
| 12 |
+
if (req.url.includes('/api/login')) {
|
| 13 |
+
return next(req);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Add auth token to requests
|
| 17 |
+
const token = authService.getToken();
|
| 18 |
+
if (token) {
|
| 19 |
+
req = req.clone({
|
| 20 |
+
setHeaders: {
|
| 21 |
+
Authorization: `Bearer ${token}`
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
return next(req).pipe(
|
| 27 |
+
catchError((error: HttpErrorResponse) => {
|
| 28 |
+
if (error.status === 401) {
|
| 29 |
+
authService.logout();
|
| 30 |
+
router.navigate(['/login']);
|
| 31 |
+
}
|
| 32 |
+
return throwError(() => error);
|
| 33 |
+
})
|
| 34 |
+
);
|
| 35 |
+
};
|
flare-ui/src/app/services/api.service.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, inject } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { Observable } from 'rxjs';
|
| 4 |
+
|
| 5 |
+
// Models
|
| 6 |
+
export interface Environment {
|
| 7 |
+
work_mode: string;
|
| 8 |
+
cloud_token: string;
|
| 9 |
+
spark_endpoint: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface Project {
|
| 13 |
+
id: number;
|
| 14 |
+
name: string;
|
| 15 |
+
caption: string;
|
| 16 |
+
enabled: boolean;
|
| 17 |
+
last_version_number: number;
|
| 18 |
+
version_id_counter: number;
|
| 19 |
+
versions: Version[];
|
| 20 |
+
deleted: boolean;
|
| 21 |
+
created_date: string;
|
| 22 |
+
created_by: string;
|
| 23 |
+
last_update_date: string;
|
| 24 |
+
last_update_user: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export interface Version {
|
| 28 |
+
id: number;
|
| 29 |
+
caption: string;
|
| 30 |
+
published: boolean;
|
| 31 |
+
general_prompt: string;
|
| 32 |
+
llm: LLMConfig;
|
| 33 |
+
intents: Intent[];
|
| 34 |
+
created_date: string;
|
| 35 |
+
created_by: string;
|
| 36 |
+
last_update_date: string;
|
| 37 |
+
last_update_user: string;
|
| 38 |
+
publish_date?: string;
|
| 39 |
+
published_by?: string;
|
| 40 |
+
deleted?: boolean;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export interface LLMConfig {
|
| 44 |
+
repo_id: string;
|
| 45 |
+
generation_config: {
|
| 46 |
+
max_new_tokens: number;
|
| 47 |
+
temperature: number;
|
| 48 |
+
top_p: number;
|
| 49 |
+
repetition_penalty: number;
|
| 50 |
+
};
|
| 51 |
+
use_fine_tune: boolean;
|
| 52 |
+
fine_tune_zip: string;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export interface Intent {
|
| 56 |
+
name: string;
|
| 57 |
+
caption: string;
|
| 58 |
+
locale: string;
|
| 59 |
+
detection_prompt: string;
|
| 60 |
+
examples: string[];
|
| 61 |
+
parameters: Parameter[];
|
| 62 |
+
action: string;
|
| 63 |
+
fallback_timeout_prompt?: string;
|
| 64 |
+
fallback_error_prompt?: string;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface Parameter {
|
| 68 |
+
name: string;
|
| 69 |
+
caption: string;
|
| 70 |
+
type: string;
|
| 71 |
+
required: boolean;
|
| 72 |
+
variable_name: string;
|
| 73 |
+
extraction_prompt: string;
|
| 74 |
+
validation_regex?: string;
|
| 75 |
+
invalid_prompt?: string;
|
| 76 |
+
type_error_prompt?: string;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export interface API {
|
| 80 |
+
name: string;
|
| 81 |
+
url: string;
|
| 82 |
+
method: string;
|
| 83 |
+
headers: Record<string, string>;
|
| 84 |
+
body_template: any;
|
| 85 |
+
timeout_seconds: number;
|
| 86 |
+
retry: {
|
| 87 |
+
retry_count: number;
|
| 88 |
+
backoff_seconds: number;
|
| 89 |
+
strategy: string;
|
| 90 |
+
};
|
| 91 |
+
proxy?: string;
|
| 92 |
+
auth?: APIAuth;
|
| 93 |
+
response_prompt?: string;
|
| 94 |
+
deleted: boolean;
|
| 95 |
+
created_date: string;
|
| 96 |
+
created_by: string;
|
| 97 |
+
last_update_date: string;
|
| 98 |
+
last_update_user: string;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
export interface APIAuth {
|
| 102 |
+
enabled: boolean;
|
| 103 |
+
token_endpoint?: string;
|
| 104 |
+
response_token_path?: string;
|
| 105 |
+
token_request_body?: any;
|
| 106 |
+
token_refresh_endpoint?: string;
|
| 107 |
+
token_refresh_body?: any;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
@Injectable({
|
| 111 |
+
providedIn: 'root'
|
| 112 |
+
})
|
| 113 |
+
export class ApiService {
|
| 114 |
+
private http = inject(HttpClient);
|
| 115 |
+
private baseUrl = '/api';
|
| 116 |
+
|
| 117 |
+
// Environment
|
| 118 |
+
getEnvironment(): Observable<Environment> {
|
| 119 |
+
return this.http.get<Environment>(`${this.baseUrl}/environment`);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
updateEnvironment(env: Environment): Observable<any> {
|
| 123 |
+
return this.http.put(`${this.baseUrl}/environment`, env);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Projects
|
| 127 |
+
getProjects(includeDeleted = false): Observable<Project[]> {
|
| 128 |
+
return this.http.get<Project[]>(`${this.baseUrl}/projects?include_deleted=${includeDeleted}`);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
createProject(project: { name: string; caption: string }): Observable<Project> {
|
| 132 |
+
return this.http.post<Project>(`${this.baseUrl}/projects`, project);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
updateProject(id: number, update: { caption: string; last_update_date: string }): Observable<Project> {
|
| 136 |
+
return this.http.put<Project>(`${this.baseUrl}/projects/${id}`, update);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
deleteProject(id: number): Observable<any> {
|
| 140 |
+
return this.http.delete(`${this.baseUrl}/projects/${id}`);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
toggleProject(id: number): Observable<{ enabled: boolean }> {
|
| 144 |
+
return this.http.patch<{ enabled: boolean }>(`${this.baseUrl}/projects/${id}/toggle`, {});
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
exportProject(id: number): Observable<any> {
|
| 148 |
+
return this.http.get(`${this.baseUrl}/projects/${id}/export`);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
importProject(data: any): Observable<any> {
|
| 152 |
+
return this.http.post(`${this.baseUrl}/projects/import`, data);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Versions
|
| 156 |
+
createVersion(projectId: number, sourceVersionId: number, caption: string): Observable<Version> {
|
| 157 |
+
return this.http.post<Version>(`${this.baseUrl}/projects/${projectId}/versions`, {
|
| 158 |
+
source_version_id: sourceVersionId,
|
| 159 |
+
caption
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
updateVersion(projectId: number, versionId: number, update: any): Observable<Version> {
|
| 164 |
+
return this.http.put<Version>(`${this.baseUrl}/projects/${projectId}/versions/${versionId}`, update);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
publishVersion(projectId: number, versionId: number): Observable<any> {
|
| 168 |
+
return this.http.post(`${this.baseUrl}/projects/${projectId}/versions/${versionId}/publish`, {});
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
deleteVersion(projectId: number, versionId: number): Observable<any> {
|
| 172 |
+
return this.http.delete(`${this.baseUrl}/projects/${projectId}/versions/${versionId}`);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// APIs
|
| 176 |
+
getAPIs(includeDeleted = false): Observable<API[]> {
|
| 177 |
+
return this.http.get<API[]>(`${this.baseUrl}/apis?include_deleted=${includeDeleted}`);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
createAPI(api: Partial<API>): Observable<API> {
|
| 181 |
+
return this.http.post<API>(`${this.baseUrl}/apis`, api);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
updateAPI(name: string, update: any): Observable<API> {
|
| 185 |
+
return this.http.put<API>(`${this.baseUrl}/apis/${name}`, update);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
deleteAPI(name: string): Observable<any> {
|
| 189 |
+
return this.http.delete(`${this.baseUrl}/apis/${name}`);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
testAPI(api: Partial<API>): Observable<any> {
|
| 193 |
+
return this.http.post(`${this.baseUrl}/apis/test`, api);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// Testing
|
| 197 |
+
runTests(testType: string): Observable<any> {
|
| 198 |
+
return this.http.post(`${this.baseUrl}/test/run-all`, { test_type: testType });
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Validation
|
| 202 |
+
validateRegex(pattern: string, testValue: string): Observable<{ valid: boolean; matches?: boolean; error?: string }> {
|
| 203 |
+
return this.http.post<any>(`${this.baseUrl}/validate/regex`, { pattern, test_value: testValue });
|
| 204 |
+
}
|
| 205 |
+
}
|
flare-ui/src/app/services/auth.service.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, inject } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { Router } from '@angular/router';
|
| 4 |
+
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
| 5 |
+
|
| 6 |
+
interface LoginResponse {
|
| 7 |
+
token: string;
|
| 8 |
+
username: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
@Injectable({
|
| 12 |
+
providedIn: 'root'
|
| 13 |
+
})
|
| 14 |
+
export class AuthService {
|
| 15 |
+
private http = inject(HttpClient);
|
| 16 |
+
private router = inject(Router);
|
| 17 |
+
|
| 18 |
+
private tokenKey = 'flare_token';
|
| 19 |
+
private usernameKey = 'flare_username';
|
| 20 |
+
|
| 21 |
+
private loggedInSubject = new BehaviorSubject<boolean>(this.hasToken());
|
| 22 |
+
public loggedIn$ = this.loggedInSubject.asObservable();
|
| 23 |
+
|
| 24 |
+
login(username: string, password: string): Observable<LoginResponse> {
|
| 25 |
+
return this.http.post<LoginResponse>('/api/login', { username, password })
|
| 26 |
+
.pipe(
|
| 27 |
+
tap(response => {
|
| 28 |
+
localStorage.setItem(this.tokenKey, response.token);
|
| 29 |
+
localStorage.setItem(this.usernameKey, response.username);
|
| 30 |
+
this.loggedInSubject.next(true);
|
| 31 |
+
})
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
logout(): void {
|
| 36 |
+
localStorage.removeItem(this.tokenKey);
|
| 37 |
+
localStorage.removeItem(this.usernameKey);
|
| 38 |
+
this.loggedInSubject.next(false);
|
| 39 |
+
this.router.navigate(['/login']);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
getToken(): string | null {
|
| 43 |
+
return localStorage.getItem(this.tokenKey);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
getUsername(): string | null {
|
| 47 |
+
return localStorage.getItem(this.usernameKey);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
hasToken(): boolean {
|
| 51 |
+
return !!this.getToken();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
isLoggedIn(): boolean {
|
| 55 |
+
return this.hasToken();
|
| 56 |
+
}
|
| 57 |
+
}
|
flare-ui/src/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<title>Flare Administration</title>
|
| 6 |
+
<base href="/">
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 8 |
+
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<app-root></app-root>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
flare-ui/src/main.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { bootstrapApplication } from '@angular/platform-browser';
|
| 2 |
+
import { provideRouter } from '@angular/router';
|
| 3 |
+
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
| 4 |
+
import { AppComponent } from './app/app.component';
|
| 5 |
+
import { routes } from './app/app.routes';
|
| 6 |
+
import { authInterceptor } from './app/interceptors/auth.interceptor';
|
| 7 |
+
|
| 8 |
+
bootstrapApplication(AppComponent, {
|
| 9 |
+
providers: [
|
| 10 |
+
provideRouter(routes),
|
| 11 |
+
provideHttpClient(
|
| 12 |
+
withInterceptors([authInterceptor])
|
| 13 |
+
)
|
| 14 |
+
]
|
| 15 |
+
});
|
flare-ui/src/styles.scss
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Global Styles */
|
| 2 |
+
* {
|
| 3 |
+
box-sizing: border-box;
|
| 4 |
+
margin: 0;
|
| 5 |
+
padding: 0;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 10 |
+
font-size: 14px;
|
| 11 |
+
line-height: 1.5;
|
| 12 |
+
color: #333;
|
| 13 |
+
background-color: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Utility Classes */
|
| 17 |
+
.container {
|
| 18 |
+
max-width: 1200px;
|
| 19 |
+
margin: 0 auto;
|
| 20 |
+
padding: 0 20px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.btn {
|
| 24 |
+
display: inline-block;
|
| 25 |
+
padding: 8px 16px;
|
| 26 |
+
font-size: 14px;
|
| 27 |
+
font-weight: 500;
|
| 28 |
+
text-align: center;
|
| 29 |
+
text-decoration: none;
|
| 30 |
+
border: none;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
transition: all 0.3s ease;
|
| 34 |
+
|
| 35 |
+
&.btn-primary {
|
| 36 |
+
background-color: #007bff;
|
| 37 |
+
color: white;
|
| 38 |
+
|
| 39 |
+
&:hover {
|
| 40 |
+
background-color: #0056b3;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
&.btn-secondary {
|
| 45 |
+
background-color: #6c757d;
|
| 46 |
+
color: white;
|
| 47 |
+
|
| 48 |
+
&:hover {
|
| 49 |
+
background-color: #545b62;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
&.btn-danger {
|
| 54 |
+
background-color: #dc3545;
|
| 55 |
+
color: white;
|
| 56 |
+
|
| 57 |
+
&:hover {
|
| 58 |
+
background-color: #c82333;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
&:disabled {
|
| 63 |
+
opacity: 0.6;
|
| 64 |
+
cursor: not-allowed;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Form Styles */
|
| 69 |
+
.form-group {
|
| 70 |
+
margin-bottom: 1rem;
|
| 71 |
+
|
| 72 |
+
label {
|
| 73 |
+
display: block;
|
| 74 |
+
margin-bottom: 0.5rem;
|
| 75 |
+
font-weight: 500;
|
| 76 |
+
color: #495057;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
input,
|
| 80 |
+
select,
|
| 81 |
+
textarea {
|
| 82 |
+
display: block;
|
| 83 |
+
width: 100%;
|
| 84 |
+
padding: 0.375rem 0.75rem;
|
| 85 |
+
font-size: 1rem;
|
| 86 |
+
line-height: 1.5;
|
| 87 |
+
color: #495057;
|
| 88 |
+
background-color: #fff;
|
| 89 |
+
background-clip: padding-box;
|
| 90 |
+
border: 1px solid #ced4da;
|
| 91 |
+
border-radius: 0.25rem;
|
| 92 |
+
transition: border-color 0.15s ease-in-out;
|
| 93 |
+
|
| 94 |
+
&:focus {
|
| 95 |
+
color: #495057;
|
| 96 |
+
background-color: #fff;
|
| 97 |
+
border-color: #80bdff;
|
| 98 |
+
outline: 0;
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
textarea {
|
| 103 |
+
min-height: 100px;
|
| 104 |
+
resize: vertical;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Table Styles */
|
| 109 |
+
.table {
|
| 110 |
+
width: 100%;
|
| 111 |
+
margin-bottom: 1rem;
|
| 112 |
+
background-color: white;
|
| 113 |
+
border-collapse: collapse;
|
| 114 |
+
|
| 115 |
+
th,
|
| 116 |
+
td {
|
| 117 |
+
padding: 0.75rem;
|
| 118 |
+
text-align: left;
|
| 119 |
+
border-bottom: 1px solid #dee2e6;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
th {
|
| 123 |
+
background-color: #f8f9fa;
|
| 124 |
+
font-weight: 600;
|
| 125 |
+
color: #495057;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
tbody tr:hover {
|
| 129 |
+
background-color: #f8f9fa;
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* Card Styles */
|
| 134 |
+
.card {
|
| 135 |
+
background-color: white;
|
| 136 |
+
border: 1px solid #dee2e6;
|
| 137 |
+
border-radius: 0.25rem;
|
| 138 |
+
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
| 139 |
+
margin-bottom: 1rem;
|
| 140 |
+
|
| 141 |
+
.card-header {
|
| 142 |
+
padding: 0.75rem 1.25rem;
|
| 143 |
+
background-color: #f8f9fa;
|
| 144 |
+
border-bottom: 1px solid #dee2e6;
|
| 145 |
+
font-weight: 600;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.card-body {
|
| 149 |
+
padding: 1.25rem;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* Tab Styles */
|
| 154 |
+
.tabs {
|
| 155 |
+
display: flex;
|
| 156 |
+
border-bottom: 2px solid #dee2e6;
|
| 157 |
+
margin-bottom: 1rem;
|
| 158 |
+
|
| 159 |
+
.tab {
|
| 160 |
+
padding: 0.5rem 1rem;
|
| 161 |
+
cursor: pointer;
|
| 162 |
+
border: none;
|
| 163 |
+
background: none;
|
| 164 |
+
font-weight: 500;
|
| 165 |
+
color: #6c757d;
|
| 166 |
+
transition: all 0.3s ease;
|
| 167 |
+
|
| 168 |
+
&.active {
|
| 169 |
+
color: #007bff;
|
| 170 |
+
border-bottom: 2px solid #007bff;
|
| 171 |
+
margin-bottom: -2px;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
&:hover {
|
| 175 |
+
color: #007bff;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* Alert Styles */
|
| 181 |
+
.alert {
|
| 182 |
+
padding: 0.75rem 1.25rem;
|
| 183 |
+
margin-bottom: 1rem;
|
| 184 |
+
border: 1px solid transparent;
|
| 185 |
+
border-radius: 0.25rem;
|
| 186 |
+
|
| 187 |
+
&.alert-danger {
|
| 188 |
+
color: #721c24;
|
| 189 |
+
background-color: #f8d7da;
|
| 190 |
+
border-color: #f5c6cb;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
&.alert-success {
|
| 194 |
+
color: #155724;
|
| 195 |
+
background-color: #d4edda;
|
| 196 |
+
border-color: #c3e6cb;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Loading Spinner */
|
| 201 |
+
.spinner {
|
| 202 |
+
display: inline-block;
|
| 203 |
+
width: 20px;
|
| 204 |
+
height: 20px;
|
| 205 |
+
border: 3px solid rgba(0, 0, 0, 0.1);
|
| 206 |
+
border-radius: 50%;
|
| 207 |
+
border-top-color: #007bff;
|
| 208 |
+
animation: spin 1s ease-in-out infinite;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
@keyframes spin {
|
| 212 |
+
to { transform: rotate(360deg); }
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* Dialog/Modal Styles */
|
| 216 |
+
.dialog-backdrop {
|
| 217 |
+
position: fixed;
|
| 218 |
+
top: 0;
|
| 219 |
+
left: 0;
|
| 220 |
+
width: 100%;
|
| 221 |
+
height: 100%;
|
| 222 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 223 |
+
display: flex;
|
| 224 |
+
align-items: center;
|
| 225 |
+
justify-content: center;
|
| 226 |
+
z-index: 1000;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.dialog {
|
| 230 |
+
background-color: white;
|
| 231 |
+
border-radius: 8px;
|
| 232 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 233 |
+
max-width: 600px;
|
| 234 |
+
width: 90%;
|
| 235 |
+
max-height: 90vh;
|
| 236 |
+
overflow: hidden;
|
| 237 |
+
display: flex;
|
| 238 |
+
flex-direction: column;
|
| 239 |
+
|
| 240 |
+
.dialog-header {
|
| 241 |
+
padding: 1rem 1.5rem;
|
| 242 |
+
border-bottom: 1px solid #dee2e6;
|
| 243 |
+
font-size: 1.25rem;
|
| 244 |
+
font-weight: 600;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.dialog-body {
|
| 248 |
+
padding: 1.5rem;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
flex: 1;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.dialog-footer {
|
| 254 |
+
padding: 1rem 1.5rem;
|
| 255 |
+
border-top: 1px solid #dee2e6;
|
| 256 |
+
display: flex;
|
| 257 |
+
justify-content: flex-end;
|
| 258 |
+
gap: 0.5rem;
|
| 259 |
+
}
|
| 260 |
+
}
|
flare-ui/tsconfig.app.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "./tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "./out-tsc/app",
|
| 5 |
+
"types": []
|
| 6 |
+
},
|
| 7 |
+
"files": [
|
| 8 |
+
"src/main.ts"
|
| 9 |
+
],
|
| 10 |
+
"include": [
|
| 11 |
+
"src/**/*.d.ts"
|
| 12 |
+
]
|
| 13 |
+
}
|
flare-ui/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compileOnSave": false,
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"baseUrl": "./",
|
| 5 |
+
"outDir": "./dist/out-tsc",
|
| 6 |
+
"forceConsistentCasingInFileNames": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noImplicitOverride": true,
|
| 9 |
+
"noPropertyAccessFromIndexSignature": true,
|
| 10 |
+
"noImplicitReturns": true,
|
| 11 |
+
"noFallthroughCasesInSwitch": true,
|
| 12 |
+
"sourceMap": true,
|
| 13 |
+
"declaration": false,
|
| 14 |
+
"downlevelIteration": true,
|
| 15 |
+
"experimentalDecorators": true,
|
| 16 |
+
"moduleResolution": "node",
|
| 17 |
+
"importHelpers": true,
|
| 18 |
+
"target": "ES2022",
|
| 19 |
+
"module": "ES2022",
|
| 20 |
+
"useDefineForClassFields": false,
|
| 21 |
+
"lib": [
|
| 22 |
+
"ES2022",
|
| 23 |
+
"dom"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
"angularCompilerOptions": {
|
| 27 |
+
"enableI18nLegacyMessageIdFormat": false,
|
| 28 |
+
"strictInjectionParameters": true,
|
| 29 |
+
"strictInputAccessModifiers": true,
|
| 30 |
+
"strictTemplates": true
|
| 31 |
+
}
|
| 32 |
+
}
|