autocomit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, BehaviorSubject, tap } from 'rxjs';
|
||||
import { LoginRequest, RegisterRequest, AuthResponse, User } from '../../shared/models/user.model';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly TOKEN_KEY = 'auth_token';
|
||||
private readonly USER_KEY = 'current_user';
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
|
||||
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
login(credentials: LoginRequest): Observable<AuthResponse> {
|
||||
return this.http.post<AuthResponse>('/api/auth/login', credentials).pipe(
|
||||
tap(response => {
|
||||
this.setSession(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
register(data: RegisterRequest): Observable<AuthResponse> {
|
||||
return this.http.post<AuthResponse>('/api/auth/register', data).pipe(
|
||||
tap(response => {
|
||||
this.setSession(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
localStorage.removeItem(this.TOKEN_KEY);
|
||||
localStorage.removeItem(this.USER_KEY);
|
||||
this.currentUserSubject.next(null);
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.getToken();
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
return this.currentUserSubject.value;
|
||||
}
|
||||
|
||||
private setSession(authResponse: AuthResponse): void {
|
||||
localStorage.setItem(this.TOKEN_KEY, authResponse.token);
|
||||
localStorage.setItem(this.USER_KEY, JSON.stringify(authResponse.user));
|
||||
this.currentUserSubject.next(authResponse.user);
|
||||
}
|
||||
|
||||
private getUserFromStorage(): User | null {
|
||||
const userJson = localStorage.getItem(this.USER_KEY);
|
||||
return userJson ? JSON.parse(userJson) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1>LDPv2 Login</h1>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
formControlName="username"
|
||||
[class.error]="loginForm.get('username')?.invalid && loginForm.get('username')?.touched"
|
||||
/>
|
||||
<div class="error-message" *ngIf="loginForm.get('username')?.invalid && loginForm.get('username')?.touched">
|
||||
Username is required
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
formControlName="password"
|
||||
[class.error]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched"
|
||||
/>
|
||||
<div class="error-message" *ngIf="loginForm.get('password')?.invalid && loginForm.get('password')?.touched">
|
||||
Password is required
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-message" *ngIf="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<button type="submit" [disabled]="loginForm.invalid || loading">
|
||||
{{ loading ? 'Loading...' : 'Login' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: #f44336;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f44336;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent {
|
||||
loginForm: FormGroup;
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.loginForm = this.fb.group({
|
||||
username: ['', [Validators.required]],
|
||||
password: ['', [Validators.required]]
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.loginForm.valid) {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
this.authService.login(this.loginForm.value).subscribe({
|
||||
next: () => {
|
||||
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/environments';
|
||||
this.router.navigate([returnUrl]);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error?.message || 'Login failed';
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, CanActivateFn } from '@angular/router';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
export const authGuard: CanActivateFn = (route, state) => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authService.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { catchError, throwError } from 'rxjs';
|
||||
|
||||
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const router = inject(Router);
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('current_user');
|
||||
router.navigate(['/login']);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(AuthService);
|
||||
const token = authService.getToken();
|
||||
|
||||
if (token && !req.url.includes('/auth/')) {
|
||||
req = req.clone({
|
||||
setHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(req);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<div class="container">
|
||||
<div *ngIf="loading" class="loading">Loading...</div>
|
||||
<div *ngIf="error" class="error">{{ error }}</div>
|
||||
|
||||
<div *ngIf="environment && !loading" class="detail-card">
|
||||
<div class="header">
|
||||
<h1>{{ environment.name }}</h1>
|
||||
<div class="actions">
|
||||
<button (click)="edit()" class="btn-primary">Edit</button>
|
||||
<button (click)="delete()" class="btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div class="detail-row">
|
||||
<label>Description:</label>
|
||||
<span>{{ environment.description || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Production:</label>
|
||||
<span [class.badge-prod]="environment.isProduction" [class.badge-non-prod]="!environment.isProduction">
|
||||
{{ environment.isProduction ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Criticality Level:</label>
|
||||
<span>{{ environment.criticalityLevel || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Created:</label>
|
||||
<span>{{ environment.createdAt | date:'medium' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Updated:</label>
|
||||
<span>{{ environment.updatedAt | date:'medium' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button (click)="back()" class="btn-secondary">Back to List</button>
|
||||
</div>
|
||||
</div>
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f5f5f5;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
width: 200px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-prod {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.badge-non-prod {
|
||||
background-color: #9e9e9e;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary, .btn-danger {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { EnvironmentService } from '../environment.service';
|
||||
import { Environment } from '../../../shared/models/environment.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-environment-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './environment-detail.component.html',
|
||||
styleUrls: ['./environment-detail.component.scss']
|
||||
})
|
||||
export class EnvironmentDetailComponent implements OnInit {
|
||||
environment?: Environment;
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
if (id) {
|
||||
this.loadEnvironment(id);
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvironment(id: string): void {
|
||||
this.loading = true;
|
||||
this.environmentService.getEnvironment(id).subscribe({
|
||||
next: (env) => {
|
||||
this.environment = env;
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to load environment';
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
if (this.environment) {
|
||||
this.router.navigate(['/environments', this.environment.id, 'edit']);
|
||||
}
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
if (this.environment && confirm('Are you sure you want to delete this environment?')) {
|
||||
this.environmentService.deleteEnvironment(this.environment.id).subscribe({
|
||||
next: () => {
|
||||
this.router.navigate(['/environments']);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to delete environment';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(['/environments']);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<div class="container">
|
||||
<h1>{{ isEditMode ? 'Edit Environment' : 'Create New Environment' }}</h1>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<div class="form-group">
|
||||
<label for="name">Name *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
[class.error]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
<div class="error-message" *ngIf="form.get('name')?.invalid && form.get('name')?.touched">
|
||||
Name is required (max 100 characters)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
formControlName="description"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" formControlName="isProduction" />
|
||||
Production Environment
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="criticalityLevel">Criticality Level (1-5)</label>
|
||||
<input
|
||||
id="criticalityLevel"
|
||||
type="number"
|
||||
formControlName="criticalityLevel"
|
||||
min="1"
|
||||
max="5"
|
||||
[class.error]="form.get('criticalityLevel')?.invalid && form.get('criticalityLevel')?.touched"
|
||||
/>
|
||||
<div class="error-message" *ngIf="form.get('criticalityLevel')?.invalid && form.get('criticalityLevel')?.touched">
|
||||
Criticality level must be between 1 and 5
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-message" *ngIf="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" (click)="cancel()" class="btn-secondary">Cancel</button>
|
||||
<button type="submit" [disabled]="form.invalid || loading" class="btn-primary">
|
||||
{{ loading ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: #f44336;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f44336;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { EnvironmentService } from '../environment.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-environment-form',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './environment-form.component.html',
|
||||
styleUrls: ['./environment-form.component.scss']
|
||||
})
|
||||
export class EnvironmentFormComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loading = false;
|
||||
error = '';
|
||||
isEditMode = false;
|
||||
environmentId?: string;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private environmentService: EnvironmentService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
description: [''],
|
||||
isProduction: [false],
|
||||
criticalityLevel: [null, [Validators.min(1), Validators.max(5)]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.environmentId = this.route.snapshot.paramMap.get('id') || undefined;
|
||||
this.isEditMode = !!this.environmentId;
|
||||
|
||||
if (this.isEditMode && this.environmentId) {
|
||||
this.loadEnvironment(this.environmentId);
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvironment(id: string): void {
|
||||
this.loading = true;
|
||||
this.environmentService.getEnvironment(id).subscribe({
|
||||
next: (env) => {
|
||||
this.form.patchValue(env);
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to load environment';
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.form.valid) {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
const request$ = this.isEditMode && this.environmentId
|
||||
? this.environmentService.updateEnvironment(this.environmentId, this.form.value)
|
||||
: this.environmentService.createEnvironment(this.form.value);
|
||||
|
||||
request$.subscribe({
|
||||
next: () => {
|
||||
this.router.navigate(['/environments']);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error?.message || 'Failed to save environment';
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.router.navigate(['/environments']);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Environments</h1>
|
||||
<button (click)="createNew()" class="btn-primary">Create New Environment</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="loading">Loading...</div>
|
||||
<div *ngIf="error" class="error">{{ error }}</div>
|
||||
|
||||
<div *ngIf="!loading && environments.length > 0" class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Production</th>
|
||||
<th>Criticality</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let env of environments">
|
||||
<td>{{ env.name }}</td>
|
||||
<td>{{ env.description || '-' }}</td>
|
||||
<td>
|
||||
<span [class.badge-prod]="env.isProduction" [class.badge-non-prod]="!env.isProduction">
|
||||
{{ env.isProduction ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ env.criticalityLevel || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button (click)="viewDetails(env.id)" class="btn-sm">View</button>
|
||||
<button (click)="edit(env.id)" class="btn-sm">Edit</button>
|
||||
<button (click)="delete(env.id)" class="btn-sm btn-danger">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading && environments.length === 0" class="empty">
|
||||
No environments found. Click "Create New Environment" to get started.
|
||||
</div>
|
||||
|
||||
<div *ngIf="totalPages > 1" class="pagination">
|
||||
<button (click)="previousPage()" [disabled]="page === 0">Previous</button>
|
||||
<span>Page {{ page + 1 }} of {{ totalPages }}</span>
|
||||
<button (click)="nextPage()" [disabled]="page >= totalPages - 1">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
}
|
||||
|
||||
.loading, .error, .empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-prod {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.badge-non-prod {
|
||||
background-color: #9e9e9e;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
background-color: #f44336;
|
||||
|
||||
&:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { EnvironmentService } from '../environment.service';
|
||||
import { Environment, Page } from '../../../shared/models/environment.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-environment-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './environment-list.component.html',
|
||||
styleUrls: ['./environment-list.component.scss']
|
||||
})
|
||||
export class EnvironmentListComponent implements OnInit {
|
||||
environments: Environment[] = [];
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
page = 0;
|
||||
size = 20;
|
||||
totalElements = 0;
|
||||
totalPages = 0;
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEnvironments();
|
||||
}
|
||||
|
||||
loadEnvironments(): void {
|
||||
this.loading = true;
|
||||
this.environmentService.getEnvironments(this.page, this.size).subscribe({
|
||||
next: (data: Page<Environment>) => {
|
||||
this.environments = data.content;
|
||||
this.totalElements = data.totalElements;
|
||||
this.totalPages = data.totalPages;
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to load environments';
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNew(): void {
|
||||
this.router.navigate(['/environments/new']);
|
||||
}
|
||||
|
||||
viewDetails(id: string): void {
|
||||
this.router.navigate(['/environments', id]);
|
||||
}
|
||||
|
||||
edit(id: string): void {
|
||||
this.router.navigate(['/environments', id, 'edit']);
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
if (confirm('Are you sure you want to delete this environment?')) {
|
||||
this.environmentService.deleteEnvironment(id).subscribe({
|
||||
next: () => {
|
||||
this.loadEnvironments();
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to delete environment';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.page < this.totalPages - 1) {
|
||||
this.page++;
|
||||
this.loadEnvironments();
|
||||
}
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.page > 0) {
|
||||
this.page--;
|
||||
this.loadEnvironments();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
Environment,
|
||||
CreateEnvironmentRequest,
|
||||
UpdateEnvironmentRequest,
|
||||
Page
|
||||
} from '../../shared/models/environment.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class EnvironmentService {
|
||||
private readonly API_URL = '/api/environments';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getEnvironments(
|
||||
page: number = 0,
|
||||
size: number = 20,
|
||||
sortBy: string = 'name',
|
||||
sortDirection: string = 'asc'
|
||||
): Observable<Page<Environment>> {
|
||||
const params = new HttpParams()
|
||||
.set('page', page.toString())
|
||||
.set('size', size.toString())
|
||||
.set('sortBy', sortBy)
|
||||
.set('sortDirection', sortDirection);
|
||||
|
||||
return this.http.get<Page<Environment>>(this.API_URL, { params });
|
||||
}
|
||||
|
||||
searchEnvironments(query: string, page: number = 0, size: number = 20): Observable<Page<Environment>> {
|
||||
const params = new HttpParams()
|
||||
.set('query', query)
|
||||
.set('page', page.toString())
|
||||
.set('size', size.toString());
|
||||
|
||||
return this.http.get<Page<Environment>>(`${this.API_URL}/search`, { params });
|
||||
}
|
||||
|
||||
getEnvironment(id: string): Observable<Environment> {
|
||||
return this.http.get<Environment>(`${this.API_URL}/${id}`);
|
||||
}
|
||||
|
||||
createEnvironment(data: CreateEnvironmentRequest): Observable<Environment> {
|
||||
return this.http.post<Environment>(this.API_URL, data);
|
||||
}
|
||||
|
||||
updateEnvironment(id: string, data: UpdateEnvironmentRequest): Observable<Environment> {
|
||||
return this.http.put<Environment>(`${this.API_URL}/${id}`, data);
|
||||
}
|
||||
|
||||
deleteEnvironment(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.API_URL}/${id}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface Environment {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isProduction: boolean;
|
||||
criticalityLevel?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateEnvironmentRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
isProduction?: boolean;
|
||||
criticalityLevel?: number;
|
||||
}
|
||||
|
||||
export interface UpdateEnvironmentRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
isProduction?: boolean;
|
||||
criticalityLevel?: number;
|
||||
}
|
||||
|
||||
export interface Page<T> {
|
||||
content: T[];
|
||||
pageable: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
sort: {
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
};
|
||||
};
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
last: boolean;
|
||||
first: boolean;
|
||||
size: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
empty: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user