Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
401d5a3174 | ||
|
|
cf50d3d36c | ||
|
|
4afcfb3ced | ||
|
|
1cd206adc8 |
@@ -143,6 +143,9 @@ jobs:
|
|||||||
sonarqube:
|
sonarqube:
|
||||||
needs: [build-front, build-back, test-front, test-back, test-ml]
|
needs: [build-front, build-back, test-front, test-back, test-ml]
|
||||||
name: SonarQube
|
name: SonarQube
|
||||||
|
# Pourquoi : GitHub ne fournit pas les secrets aux workflows lancés par dependabot[bot].
|
||||||
|
# Sans SONAR_TOKEN le scan échoue sans rien analyser ; build et tests restent joués.
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ series temporelles energetiques, deployee sur une machine on-premise.
|
|||||||
|-------|----------------------------------------------------------|
|
|-------|----------------------------------------------------------|
|
||||||
| J1 | Valider la préparation de l'environnement et du repo |
|
| J1 | Valider la préparation de l'environnement et du repo |
|
||||||
| J2 | Valider le périmètre retenu et les choix technologiques |
|
| J2 | Valider le périmètre retenu et les choix technologiques |
|
||||||
| J3 | Valider l'architecture et la gestion de la sécurité |
|
| J3 | Ingestion & backend |
|
||||||
| J4 | Valider la robustesse et assurer les livrables |
|
| J4 | Architecture, sécurité & frontend |
|
||||||
|
| J5 | Valider la robustesse et assurer les livrables |
|
||||||
|
| J6 | Amélioration possible |
|
||||||
|
|
||||||
Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.md](docs/architecture/00-vue-ensemble.md).
|
Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.md](docs/architecture/00-vue-ensemble.md).
|
||||||
|
|
||||||
@@ -18,15 +20,15 @@ Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.m
|
|||||||
|
|
||||||
| Domaine | Technologie | Emplacement | Etat |
|
| Domaine | Technologie | Emplacement | Etat |
|
||||||
|------------|-------------------------------------|---------------------|---------------|
|
|------------|-------------------------------------|---------------------|---------------|
|
||||||
| Backend | FastAPI, Python 3.14 | `apps/backend` | Initialise |
|
| Backend | FastAPI, Python 3.14 | `apps/backend` | En place |
|
||||||
| Frontend | Angular 22, Node 24 LTS | `apps/frontend` | Tableau de bord |
|
| Frontend | Angular 22, Node 24 LTS | `apps/frontend` | En place |
|
||||||
| Base | PostgreSQL 17 + TimescaleDB | `db` | Initialise |
|
| Base | PostgreSQL 17 + TimescaleDB | `db` | En place |
|
||||||
| ETL | Apache Airflow | `etl/airflow` | Trois DAGs |
|
| ETL | Apache Airflow | `etl/airflow` | Trois DAGs |
|
||||||
| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise |
|
| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise |
|
||||||
| Reverse proxy | Nginx, TLS | `infra/proxy` | En place |
|
| Reverse proxy | Nginx, TLS | `infra/proxy` | En place |
|
||||||
| CI/CD | GitHub Actions | `.github/workflows` | Backend en place |
|
| CI/CD | GitHub Actions | `.github/workflows` | En place |
|
||||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | A initialiser |
|
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | A initialiser |
|
||||||
| ML | LightGBM, MLflow | `ml` | Entrainement initialise |
|
| ML | LightGBM, MLflow | `ml` | En place |
|
||||||
|
|
||||||
Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Le frontend
|
Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Le frontend
|
||||||
sert un tableau de bord sur `/dashboard`, dont les données proviennent de fixtures : les endpoints
|
sert un tableau de bord sur `/dashboard`, dont les données proviennent de fixtures : les endpoints
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Conventions de tests unitaires : Frontend
|
# Conventions de tests unitaires — Frontend
|
||||||
|
|
||||||
## Outil
|
## Outil
|
||||||
Vitest (intégré nativement à Angular CLI, pas d'installation à faire).
|
Vitest (intégré nativement à Angular CLI, pas d'installation à faire).
|
||||||
@@ -83,6 +83,3 @@ describe('MonComposant', () => {
|
|||||||
## Lancer les tests
|
## Lancer les tests
|
||||||
- Développement (mode watch) : `npm test`
|
- Développement (mode watch) : `npm test`
|
||||||
- Rapport de couverture (CI) : `npm run test:ci -- --coverage`, puis ouvrir `coverage/index.html`
|
- Rapport de couverture (CI) : `npm run test:ci -- --coverage`, puis ouvrir `coverage/index.html`
|
||||||
- Un fichier ou un dossier seulement :
|
|
||||||
`npx ng test --watch=false --coverage=false --include=src/app/core/services/alerts.service.spec.ts`
|
|
||||||
(répéter `--include` pour plusieurs cibles ; un dossier joue tous ses specs)
|
|
||||||
|
|||||||
@@ -2,63 +2,53 @@ import { Alert } from '../../shared/models/alert.model';
|
|||||||
|
|
||||||
export const ALERTS_FIXTURE: Alert[] = [
|
export const ALERTS_FIXTURE: Alert[] = [
|
||||||
{
|
{
|
||||||
alert_id: 5,
|
alert_id: 'ALR-SITE002-1718458320',
|
||||||
|
timestamp: '2026-09-15T11:12:00',
|
||||||
site_id: 'SITE002',
|
site_id: 'SITE002',
|
||||||
timestamp: '2026-09-15T11:12:00Z',
|
|
||||||
type: 'threshold',
|
|
||||||
severity: 'critical',
|
severity: 'critical',
|
||||||
message: 'Puissance appelée 812.5 kW au-dessus de la capacité du site (720.0 kW)',
|
type: 'outage',
|
||||||
|
message: 'Risque de surcharge sur Usine Lyon Vénissieux',
|
||||||
value: 812.5,
|
value: 812.5,
|
||||||
threshold: 720.0,
|
threshold: 720.0,
|
||||||
metric: 'consumption_kw',
|
|
||||||
prediction_id: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
alert_id: 4,
|
alert_id: 'ALR-SITE003-1718458321',
|
||||||
|
timestamp: '2026-09-15T11:05:00',
|
||||||
site_id: 'SITE003',
|
site_id: 'SITE003',
|
||||||
timestamp: '2026-09-15T11:05:00Z',
|
|
||||||
type: 'outage',
|
|
||||||
severity: 'critical',
|
severity: 'critical',
|
||||||
message: 'Aucune lecture depuis 5:00:00 (dernière lecture : 2026-09-15T06:05:00+00:00)',
|
|
||||||
value: null,
|
|
||||||
threshold: null,
|
|
||||||
metric: null,
|
|
||||||
prediction_id: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alert_id: 3,
|
|
||||||
site_id: 'SITE005',
|
|
||||||
timestamp: '2026-09-15T10:47:00Z',
|
|
||||||
type: 'spike',
|
|
||||||
severity: 'high',
|
|
||||||
message: 'Variation brutale entre deux lectures consécutives (260.0 kW -> 410.0 kW)',
|
|
||||||
value: 410.0,
|
|
||||||
threshold: 260.0,
|
|
||||||
metric: 'consumption_kw',
|
|
||||||
prediction_id: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alert_id: 2,
|
|
||||||
site_id: 'SITE006',
|
|
||||||
timestamp: '2026-09-15T10:30:00Z',
|
|
||||||
type: 'sensor',
|
type: 'sensor',
|
||||||
severity: 'medium',
|
message: 'Perte réseau totale sur Data Center Marseille',
|
||||||
message: 'Qualité de mesure degraded (capteur hors ligne, valeur nulle)',
|
value: 0,
|
||||||
value: null,
|
threshold: 0,
|
||||||
threshold: null,
|
|
||||||
metric: null,
|
|
||||||
prediction_id: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
alert_id: 1,
|
alert_id: 'ALR-SITE005-1718458322',
|
||||||
|
timestamp: '2026-09-15T10:47:00',
|
||||||
|
site_id: 'SITE005',
|
||||||
|
severity: 'high',
|
||||||
|
type: 'threshold',
|
||||||
|
message: 'Usine Toulouse approche de son seuil de capacité',
|
||||||
|
value: 410.0,
|
||||||
|
threshold: 480.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alert_id: 'ALR-SITE006-1718458323',
|
||||||
|
timestamp: '2026-09-15T10:30:00',
|
||||||
|
site_id: 'SITE006',
|
||||||
|
severity: 'medium',
|
||||||
|
type: 'sensor',
|
||||||
|
message: 'Capteur de température défaillant sur Bureau Lille',
|
||||||
|
value: 0,
|
||||||
|
threshold: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alert_id: 'ALR-SITE004-1718458324',
|
||||||
|
timestamp: '2026-09-15T09:58:00',
|
||||||
site_id: 'SITE004',
|
site_id: 'SITE004',
|
||||||
timestamp: '2026-09-15T09:58:00Z',
|
|
||||||
type: 'anomaly',
|
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
message: 'Écart de 13% entre la consommation mesurée (62.0 kWh) et la prévision (55.0 kWh)',
|
type: 'anomaly',
|
||||||
|
message: 'Comportement de consommation inhabituel sur Bureau Bordeaux',
|
||||||
value: 62.0,
|
value: 62.0,
|
||||||
threshold: 55.0,
|
threshold: 55.0,
|
||||||
metric: 'consumption_kwh',
|
|
||||||
prediction_id: 42,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,20 +3,6 @@ import { provideHttpClient } from '@angular/common/http';
|
|||||||
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
||||||
import { AlertsService } from './alerts.service';
|
import { AlertsService } from './alerts.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { Alert } from '../../shared/models/alert.model';
|
|
||||||
|
|
||||||
const ALERT_API: Alert = {
|
|
||||||
alert_id: 1,
|
|
||||||
site_id: 'site-1',
|
|
||||||
timestamp: '2026-09-16T00:00:00Z',
|
|
||||||
type: 'threshold',
|
|
||||||
severity: 'high',
|
|
||||||
message: 'Dépassement du seuil configuré',
|
|
||||||
value: 812.5,
|
|
||||||
threshold: 720.0,
|
|
||||||
metric: 'consumption_kw',
|
|
||||||
prediction_id: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AlertsService', () => {
|
describe('AlertsService', () => {
|
||||||
let service: AlertsService;
|
let service: AlertsService;
|
||||||
@@ -32,35 +18,26 @@ describe('AlertsService', () => {
|
|||||||
|
|
||||||
afterEach(() => httpMock.verify());
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
it("appelle le bon endpoint sans paramètre et retourne un tableau d'alertes", () => {
|
it("appelle le bon endpoint et retourne un tableau d'alertes", () => {
|
||||||
let result: Alert[] = [];
|
let result: unknown;
|
||||||
service.getAlerts().subscribe((r) => (result = r));
|
service.getAlerts().subscribe((r) => (result = r));
|
||||||
|
|
||||||
const req = httpMock.expectOne(
|
const req = httpMock.expectOne(`${environment.apiUrl}/alerts`);
|
||||||
(r) => r.url === `${environment.apiUrl}/alerts` && r.method === 'GET',
|
expect(req.request.method).toBe('GET');
|
||||||
);
|
|
||||||
expect(req.request.params.keys()).toEqual([]);
|
|
||||||
req.flush([ALERT_API]);
|
|
||||||
|
|
||||||
expect(result.length).toBe(1);
|
req.flush([
|
||||||
expect(result[0].alert_id).toBe(1);
|
{
|
||||||
expect(result[0].prediction_id).toBeNull();
|
alert_id: 'ALR-TEST-1',
|
||||||
});
|
timestamp: '2026-09-15T12:00:00',
|
||||||
|
site_id: 'SITE001',
|
||||||
|
severity: 'high',
|
||||||
|
type: 'threshold',
|
||||||
|
message: 'Test',
|
||||||
|
value: 100,
|
||||||
|
threshold: 90,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
it('transmet les filtres site_id et severity en paramètres de requête', () => {
|
expect((result as unknown[]).length).toBe(1);
|
||||||
service.getAlerts({ site_id: 'SITE001', severity: 'high' }).subscribe();
|
|
||||||
|
|
||||||
const req = httpMock.expectOne((r) => r.url === `${environment.apiUrl}/alerts`);
|
|
||||||
expect(req.request.params.get('site_id')).toBe('SITE001');
|
|
||||||
expect(req.request.params.get('severity')).toBe('high');
|
|
||||||
req.flush([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ne pose pas de paramètre pour un filtre omis', () => {
|
|
||||||
service.getAlerts({ site_id: 'SITE001' }).subscribe();
|
|
||||||
|
|
||||||
const req = httpMock.expectOne((r) => r.url === `${environment.apiUrl}/alerts`);
|
|
||||||
expect(req.request.params.has('severity')).toBe(false);
|
|
||||||
req.flush([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { Service, inject } from '@angular/core';
|
import { Service, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { Alert, AlertSeverity } from '../../shared/models/alert.model';
|
import { Alert } from '../../shared/models/alert.model';
|
||||||
|
|
||||||
export interface AlertFilters {
|
|
||||||
site_id?: string;
|
|
||||||
severity?: AlertSeverity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class AlertsService {
|
export class AlertsService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
getAlerts(filters: AlertFilters = {}) {
|
getAlerts() {
|
||||||
let params = new HttpParams();
|
return this.http.get<Alert[]>(`${environment.apiUrl}/alerts`);
|
||||||
if (filters.site_id) {
|
|
||||||
params = params.set('site_id', filters.site_id);
|
|
||||||
}
|
|
||||||
if (filters.severity) {
|
|
||||||
params = params.set('severity', filters.severity);
|
|
||||||
}
|
|
||||||
return this.http.get<Alert[]>(`${environment.apiUrl}/alerts`, { params });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import {
|
|
||||||
LIBELLE_PAR_SEVERITE,
|
|
||||||
LIBELLE_PAR_TYPE,
|
|
||||||
SEVERITES,
|
|
||||||
TON_PAR_SEVERITE,
|
|
||||||
TYPES_ALERTE,
|
|
||||||
UNITE_PAR_METRIQUE,
|
|
||||||
} from './alert-presentation';
|
|
||||||
|
|
||||||
describe('alert-presentation', () => {
|
|
||||||
it('distingue le ton des sévérités high et critical', () => {
|
|
||||||
expect(TON_PAR_SEVERITE.high).toBe('danger');
|
|
||||||
expect(TON_PAR_SEVERITE.critical).toBe('critical');
|
|
||||||
expect(TON_PAR_SEVERITE.high).not.toBe(TON_PAR_SEVERITE.critical);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("n'affiche pas une alerte faible avec le ton de succès", () => {
|
|
||||||
expect(TON_PAR_SEVERITE.low).toBe('neutral');
|
|
||||||
expect(TON_PAR_SEVERITE.medium).toBe('warning');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('donne un libellé français à chaque sévérité et à chaque type', () => {
|
|
||||||
for (const severite of SEVERITES) {
|
|
||||||
expect(LIBELLE_PAR_SEVERITE[severite]).toBeTruthy();
|
|
||||||
}
|
|
||||||
for (const type of TYPES_ALERTE) {
|
|
||||||
expect(LIBELLE_PAR_TYPE[type]).toBeTruthy();
|
|
||||||
}
|
|
||||||
expect(SEVERITES.length).toBe(4);
|
|
||||||
expect(TYPES_ALERTE.length).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('associe une unité à chaque métrique du contrat', () => {
|
|
||||||
expect(UNITE_PAR_METRIQUE.consumption_kw).toBe('kW');
|
|
||||||
expect(UNITE_PAR_METRIQUE.consumption_kwh).toBe('kWh');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { BadgeTone } from '../components/ui/badge/badge';
|
|
||||||
import { AlertMetric, AlertSeverity, AlertType } from './alert.model';
|
|
||||||
|
|
||||||
// Pourquoi : `low` en neutre plutôt qu'en vert, une alerte faible reste une alerte ; le vert se
|
|
||||||
// lisait comme « tout va bien » à côté des rouges.
|
|
||||||
export const TON_PAR_SEVERITE: Record<AlertSeverity, BadgeTone> = {
|
|
||||||
low: 'neutral',
|
|
||||||
medium: 'warning',
|
|
||||||
high: 'danger',
|
|
||||||
critical: 'critical',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LIBELLE_PAR_SEVERITE: Record<AlertSeverity, string> = {
|
|
||||||
low: 'Faible',
|
|
||||||
medium: 'Moyenne',
|
|
||||||
high: 'Élevée',
|
|
||||||
critical: 'Critique',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LIBELLE_PAR_TYPE: Record<AlertType, string> = {
|
|
||||||
spike: 'Pic de consommation',
|
|
||||||
threshold: 'Seuil dépassé',
|
|
||||||
anomaly: 'Anomalie',
|
|
||||||
outage: 'Coupure',
|
|
||||||
sensor: 'Capteur',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UNITE_PAR_METRIQUE: Record<AlertMetric, string> = {
|
|
||||||
consumption_kw: 'kW',
|
|
||||||
consumption_kwh: 'kWh',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SEVERITES: readonly AlertSeverity[] = ['low', 'medium', 'high', 'critical'];
|
|
||||||
|
|
||||||
export const TYPES_ALERTE: readonly AlertType[] = [
|
|
||||||
'spike',
|
|
||||||
'threshold',
|
|
||||||
'anomaly',
|
|
||||||
'outage',
|
|
||||||
'sensor',
|
|
||||||
];
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
export type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
|
export type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
|
||||||
export type AlertType = 'spike' | 'threshold' | 'anomaly' | 'outage' | 'sensor';
|
export type AlertType = 'spike' | 'threshold' | 'anomaly' | 'outage' | 'sensor';
|
||||||
export type AlertMetric = 'consumption_kw' | 'consumption_kwh';
|
|
||||||
|
|
||||||
export interface Alert {
|
export interface Alert {
|
||||||
alert_id: number;
|
alert_id: string;
|
||||||
site_id: string;
|
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
type: AlertType;
|
site_id: string;
|
||||||
severity: AlertSeverity;
|
severity: AlertSeverity;
|
||||||
|
type: AlertType;
|
||||||
message: string;
|
message: string;
|
||||||
value: number | null;
|
value: number;
|
||||||
threshold: number | null;
|
threshold: number;
|
||||||
metric: AlertMetric | null;
|
|
||||||
prediction_id: number | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,3 @@
|
|||||||
color: var(--color-disabled);
|
color: var(--color-disabled);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Piège : le chevron est un SVG en data URI, où aucun token CSS n'est lisible ; sa couleur
|
|
||||||
// reprend en dur la valeur de --color-text-muted.
|
|
||||||
.form-select {
|
|
||||||
@extend .form-input;
|
|
||||||
padding-right: 2.25rem;
|
|
||||||
color: var(--color-text);
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.6rem center;
|
|
||||||
background-size: 1rem;
|
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
|
|||||||
@@ -24,13 +24,12 @@ seule fois dans `src/styles.scss`. Disponibles partout sans import supplémentai
|
|||||||
| `--shadow-card` | Ombre portée des cartes |
|
| `--shadow-card` | Ombre portée des cartes |
|
||||||
| `--space-1` à `--space-5` | Échelle d'espacement (0.35rem à 2.5rem) |
|
| `--space-1` à `--space-5` | Échelle d'espacement (0.35rem à 2.5rem) |
|
||||||
|
|
||||||
Les classes de formulaire partagées (`.form-label`, `.form-input`, `.form-select`, `.form-hint`)
|
Les classes de formulaire partagées (`.form-label`, `.form-input`, `.form-hint`) sont dans
|
||||||
sont dans `apps/frontend/src/styles/_forms.scss`, importées globalement de la même façon. Elles
|
`apps/frontend/src/styles/_forms.scss`, importées globalement de la même façon. Elles
|
||||||
s'appliquent directement à des `<label>`/`<input>`/`<select>` natifs, liés par `formControlName` ou
|
s'appliquent directement à des `<label>`/`<input>` natifs liés par `formControlName` : pas de
|
||||||
par un simple `(change)` : pas de composant `ControlValueAccessor` dédié, le gain n'en vaut pas la
|
composant `ControlValueAccessor` dédié, le gain n'en vaut pas la complexité pour des formulaires
|
||||||
complexité pour des formulaires aussi simples que ceux de ce projet. `.form-select` habille un
|
aussi simples que ceux de ce projet. Les erreurs de formulaire, elles, s'affichent via
|
||||||
`<select>` natif avec la bordure et le focus de `.form-input`, plus un chevron. Les erreurs de
|
`<ev-alert severity="danger">`, pas une classe dédiée.
|
||||||
formulaire, elles, s'affichent via `<ev-alert severity="danger">`, pas une classe dédiée.
|
|
||||||
|
|
||||||
La classe `.auth-page` (`apps/frontend/src/styles/_auth-page.scss`, importée globalement) porte
|
La classe `.auth-page` (`apps/frontend/src/styles/_auth-page.scss`, importée globalement) porte
|
||||||
le fond dégradé et le centrage commun aux pages d'authentification (`login`, `change-password`,
|
le fond dégradé et le centrage commun aux pages d'authentification (`login`, `change-password`,
|
||||||
|
|||||||
Reference in New Issue
Block a user