Spaces:
Building
Building
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 |
+
}
|