diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 90e14c2675b7895a8ebe5e771e00dd3e6124ce01..b18301408c0ff2a0f37f2e9fb2e15dc36c6081dd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -88,6 +88,7 @@ import { DoughnutchartComponent } from './landing/metrics/doughnutchart/doughnut import { StatusCardComponent } from './landing/metrics/statuscard/statuscard.component'; import { PortalCardComponent } from './landing/portalcard/portalcard.component'; import { SkeletonComponent } from './landing/metrics/skeleton.component'; +import { CdkTableModule } from "@angular/cdk/table"; registerLocaleData(enGB); export function initializeApp(bootstrap: BootstrapService) { @@ -116,23 +117,24 @@ export function initializeAppTheme(bootstrap: BootstrapService) { SkeletonComponent, ], imports: [ - BrowserModule, - AppRoutingModule, - HttpClientModule, - BrowserAnimationsModule, - CollapseModule.forRoot(), - BsDropdownModule.forRoot(), - FormsModule, - ReactiveFormsModule, - NgProgressModule, - NgProgressHttpModule, - MatInputModule, - MatDialogModule, - MatCheckboxModule, - OAuthModule.forRoot(), - ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }), - NgChartsModule - ], + BrowserModule, + AppRoutingModule, + HttpClientModule, + BrowserAnimationsModule, + CollapseModule.forRoot(), + BsDropdownModule.forRoot(), + FormsModule, + ReactiveFormsModule, + NgProgressModule, + NgProgressHttpModule, + MatInputModule, + MatDialogModule, + MatCheckboxModule, + OAuthModule.forRoot(), + ToastrModule.forRoot({ progressBar: true, preventDuplicates: true }), + NgChartsModule, + CdkTableModule +], providers: [ AppService, AuthService, diff --git a/src/app/landing/landing.component.html b/src/app/landing/landing.component.html index 424753450d3c64873c5c13cd772120b1ea6aeeac..639a491357a78cbe6a6bb4d0e025cda16bd72516 100644 --- a/src/app/landing/landing.component.html +++ b/src/app/landing/landing.component.html @@ -46,7 +46,13 @@
-
{{animatedCounts.registeredUsers}}
+
+
+
+ + +
{{animatedCounts.registeredUsers}}
+
Registered Users
@@ -61,7 +67,13 @@
-
{{animatedCounts.publishedServiceSpecs}}
+
+
+
+ + +
{{animatedCounts.publishedServiceSpecs}}
+
Published Service Specifications
@@ -77,7 +89,12 @@
-
{{animatedCounts.registeredResourceSpecs}}
+
+
+
+ +
{{animatedCounts.registeredResourceSpecs}}
+
Registered Resource Specifications
diff --git a/src/app/landing/landing.component.scss b/src/app/landing/landing.component.scss index 944ec75065ccb442f386495617a48f37eefc3647..42340365c65be34f78f18074623cbeafb4930a5b 100644 --- a/src/app/landing/landing.component.scss +++ b/src/app/landing/landing.component.scss @@ -95,6 +95,8 @@ .card-title { padding-top: 1.5rem; transition: all 0.3s ease-in-out; + display: flex; + justify-content: center; } diff --git a/src/app/landing/landing.component.ts b/src/app/landing/landing.component.ts index 1cc25fd52eebb0adaec80fd170f47301a2b7f8a6..5ecfc724c925f8066d79fb04775ec69e56f9a90d 100644 --- a/src/app/landing/landing.component.ts +++ b/src/app/landing/landing.component.ts @@ -23,25 +23,23 @@ export class LandingComponent implements OnInit { config: IAppConfig loggedIn: boolean - // Video section variables private player: Player; videoLoaded: boolean = false; + isLoading: boolean = true; + @ViewChild('videoRef', { static: true }) videoElement!: ElementRef; // Metrics section variables - registeredUsers = 9999; - publishedServiceSpecs = 9999; - registeredResourceSpecs = 9999; animatedCounts = { - registeredUsers: 0, - publishedServiceSpecs: 0, - registeredResourceSpecs: 0 + registeredUsers: NaN, + publishedServiceSpecs: NaN, + registeredResourceSpecs: NaN }; targetCounts = { - registeredUsers: 0, - publishedServiceSpecs: 0, - registeredResourceSpecs: 0 + registeredUsers: NaN, + publishedServiceSpecs: NaN, + registeredResourceSpecs: NaN }; private intervalId: any; @@ -49,41 +47,49 @@ export class LandingComponent implements OnInit { this.config = this.appService.config this.authService.isAuthenticated$.subscribe( isAuthenticated => this.loggedIn = isAuthenticated - ) + ) this.initializePlayer() - // Initialize animated counts - this.startAnimation(); forkJoin({ registeredUsers: this.generalMetricsService.getRegisteredIndividuals().pipe( catchError(error => { console.error('Failed to load registered users', error); - return scheduled([{ registeredIndividuals: this.registeredUsers }], asyncScheduler); + return scheduled([{ registeredIndividuals: NaN }], asyncScheduler); }) ), publishedServiceSpecs: this.generalMetricsService.getPublishedServiceSpecifications().pipe( catchError(error => { console.error('Failed to load published specs', error); - return scheduled([{ publishedServiceSpecifications: this.publishedServiceSpecs }], asyncScheduler); + return scheduled([{ publishedServiceSpecifications: NaN }], asyncScheduler); }) ), registeredResourceSpecs: this.generalMetricsService.getRegisteredResourceSpecifications().pipe( catchError(error => { console.error('Failed to load resource specs', error); - return scheduled([{ registeredResourceSpecifications: this.registeredResourceSpecs }], asyncScheduler); + return scheduled([{ registeredResourceSpecifications: NaN }], asyncScheduler); }) ) - }).subscribe( - results => { - this.targetCounts.registeredUsers = results.registeredUsers.registeredIndividuals; - this.targetCounts.publishedServiceSpecs = results.publishedServiceSpecs.publishedServiceSpecifications; - this.targetCounts.registeredResourceSpecs = results.registeredResourceSpecs.registeredResourceSpecifications; - } - ) + }) + // .pipe(delay(500)) + .subscribe( + results => { + + this.targetCounts.registeredUsers = results.registeredUsers.registeredIndividuals; + this.targetCounts.publishedServiceSpecs = results.publishedServiceSpecs.publishedServiceSpecifications; + this.targetCounts.registeredResourceSpecs = results.registeredResourceSpecs.registeredResourceSpecifications; + this.isLoading = false; + // Initialize animated counts + this.startAnimation(); + + } + ); } + isNaN(value: any): boolean { + return isNaN(value); + } initializePlayer() { this.player = new Player(this.videoElement.nativeElement, { @@ -94,8 +100,8 @@ export class LandingComponent implements OnInit { muted: true, playsinline: true, // For mobile devices }); - - this.player.ready().then(() => { + + this.player.ready().then(() => { this.player.on('play', () => { this.videoLoaded = true; }); @@ -110,7 +116,7 @@ export class LandingComponent implements OnInit { // console.error('Player not initialized'); // return; // } - + // try { // if (this.isPlaying) { // await this.player.pause(); @@ -140,7 +146,7 @@ export class LandingComponent implements OnInit { // toggleFullscreen() { // const elem = this.videoElement.nativeElement.parentElement; - + // if (!document.fullscreenElement) { // if (elem?.requestFullscreen) { // elem.requestFullscreen(); @@ -159,12 +165,24 @@ export class LandingComponent implements OnInit { startAnimation() { const stepTime = 50; + // Initialize counts to NaN if the target is NaN (api didn't respond), otherwise start from 0 and animate the count + this.animatedCounts = { + registeredUsers: isNaN(this.targetCounts.registeredUsers) ? NaN : 0, + publishedServiceSpecs: isNaN(this.targetCounts.publishedServiceSpecs) ? NaN : 0, + registeredResourceSpecs: isNaN(this.targetCounts.registeredResourceSpecs) ? NaN : 0 + }; + this.intervalId = setInterval(() => { for (const key in this.animatedCounts) { const k = key as keyof typeof this.animatedCounts; const current = this.animatedCounts[k]; const target = this.targetCounts[k]; + if (isNaN(target)) { + this.animatedCounts[k] = NaN; + continue; + } + const delta = target - current; if (Math.abs(delta) > 0) { @@ -172,6 +190,7 @@ export class LandingComponent implements OnInit { } } }, stepTime); + } ngOnDestroy() { @@ -184,5 +203,5 @@ export class LandingComponent implements OnInit { this.authService.login() } - + } diff --git a/src/app/landing/metrics/doughnutchart/doughnutchart.component.html b/src/app/landing/metrics/doughnutchart/doughnutchart.component.html index 2eaed51db884397ce3aaea301dd73a08b0e0b4f2..d08cc83634ae6e9bd54462cf4a8017fce889af41 100644 --- a/src/app/landing/metrics/doughnutchart/doughnutchart.component.html +++ b/src/app/landing/metrics/doughnutchart/doughnutchart.component.html @@ -1,27 +1,27 @@
-
+
-
+
{{noDataMessage}}
-
-
+
-
+
{{chartTitle}}
-
+
-
+
  • Service Orders Last 15 Days
    -
    +
    @@ -78,7 +78,7 @@
    No Service Orders found for the last 15 days
    -
    diff --git a/src/app/landing/metrics/metrics.component.ts b/src/app/landing/metrics/metrics.component.ts index 29d214f9589c2884870bf73d68ed9219107b6062..180c86f61c11fd37534e6788babfc06b66e3f6e9 100644 --- a/src/app/landing/metrics/metrics.component.ts +++ b/src/app/landing/metrics/metrics.component.ts @@ -15,6 +15,8 @@ import { ResourcesGroupByStateItem, ServiceOrdersGroupByDayItem, ServicesGroupB import { asyncScheduler, forkJoin, scheduled } from 'rxjs'; import { catchError } from 'rxjs/internal/operators/catchError'; import * as moment from 'moment'; +import { ToastrService } from 'ngx-toastr'; + @Component({ selector: 'app-landing', @@ -30,30 +32,31 @@ export class MetricsComponent implements OnInit { private serviceMetricsService: ServiceMetricsApiService, private resourceMetricsService: ResourceMetricsApiService, private serviceOrderMetricsService: ServiceOrderMetricsApiService, - private nfvMetricsService: NfvMetricsApiService + private nfvMetricsService: NfvMetricsApiService, + private toast: ToastrService ) { } isLoading: boolean = true; config: IAppConfig loggedIn: boolean - registeredUsers = 9999; - activeServices = 9999; - registeredNSDs = 9999; - registeredVNFs = 9999; - registeredMANO = 9999; - registeredResourceSpecs = 9999; - publishedServiceSpecs = 9999; - availableResources = 9999; - activeServiceOrders = 9999; - publishedProductOfferings = 9999; - totalCreatedServices = 9999; - totalCompletedOrders = 9999; - totalResources = 9999; - serviceOrdersTotal15days = 9999; - serviceOrdersTotalMonth = 9999; - servicesTotalMonth = 9999; - resourcesTotalMonth = 9999; + registeredUsers = NaN; + activeServices = NaN; + registeredNSDs = NaN; + registeredVNFs = NaN; + registeredMANO = NaN; + registeredResourceSpecs = NaN; + publishedServiceSpecs = NaN; + availableResources = NaN; + activeServiceOrders = NaN; + publishedProductOfferings = NaN; + totalCreatedServices = NaN; + totalCompletedOrders = NaN; + totalResources = NaN; + serviceOrdersTotal15days = NaN; + serviceOrdersTotalMonth = NaN; + servicesTotalMonth = NaN; + resourcesTotalMonth = NaN; serviceOrdersByState = []; servicesByState: ServicesGroupByStateItem[]; @@ -118,8 +121,8 @@ export class MetricsComponent implements OnInit { datasets: [ { data: [], - backgroundColor: ['#428bca', '#42ca74', '#c5ca42' ], - hoverBackgroundColor: ['#235e91', '#24a34a', '#8d911f'], + backgroundColor: ['#fd8f00', '#28a745', '#dc3545' ], + hoverBackgroundColor: ['#d27700', '#1e873d', '#941822'], hoverBorderColor: 'white' } ], @@ -131,8 +134,8 @@ export class MetricsComponent implements OnInit { datasets: [ { data: [], - backgroundColor: ['#428bca', '#42ca74', '#c5ca42', '#ca42c1', '#d91424'], - hoverBackgroundColor: ['#235e91', '#24a34a', '#8d911f', '#871880', '#941822'], + backgroundColor: ['#428bca', '#cab642ff', '#fd8f00', '#28a745', '#dc3545'], + hoverBackgroundColor: ['#235e91', '#8d911f', '#d27700', '#1e873d', '#941822'], hoverBorderColor: 'white', } ], @@ -145,8 +148,8 @@ export class MetricsComponent implements OnInit { datasets: [ { data: [], - backgroundColor: ['#428bca', '#c5ca42', '#42ca74', '#d91424'], - hoverBackgroundColor: ['#235e91', '#8d911f', '#24a34a', '#941822'], + backgroundColor: ['#fd8f00', '#a2a3a4', '#28a745', '#d91424'], + hoverBackgroundColor: ['#d27700', '#818181', '#1e873d', '#941822'], hoverBorderColor: 'white' } ] @@ -168,85 +171,85 @@ export class MetricsComponent implements OnInit { forkJoin({ registeredMANO: this.nfvMetricsService.getRegisteredManoProviders().pipe( catchError(error => { - console.error('Failed to load registered MANO providers', error); - return scheduled([{ registeredManoProviders: this.registeredMANO }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ registeredManoProviders: NaN }], asyncScheduler); }) ), registeredNSDs: this.nfvMetricsService.getRegisteredNsds().pipe( catchError(error => { - console.error('Failed to load registered NSDs', error); - return scheduled([{ registeredNSDs: this.registeredNSDs }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ registeredNSDs: NaN }], asyncScheduler); }) ), registeredVNFs: this.nfvMetricsService.getRegisteredVnfs().pipe( catchError(error => { - console.error('Failed to load registered VNFs', error); - return scheduled([{ registeredVNFs: this.registeredVNFs }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ registeredVNFs: NaN }], asyncScheduler); }) ), registeredUsers: this.generalMetricsService.getRegisteredIndividuals().pipe( catchError(error => { - console.error('Failed to load registered users', error); - return scheduled([{ registeredIndividuals: this.registeredUsers }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ registeredIndividuals: NaN }], asyncScheduler); }) ), publishedServiceSpecs: this.generalMetricsService.getPublishedServiceSpecifications().pipe( catchError(error => { - console.error('Failed to load published specs', error); - return scheduled([{ publishedServiceSpecifications: this.publishedServiceSpecs }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ publishedServiceSpecifications: NaN }], asyncScheduler); }) ), registeredResourceSpecs: this.generalMetricsService.getRegisteredResourceSpecifications().pipe( catchError(error => { - console.error('Failed to load resource specs', error); - return scheduled([{ registeredResourceSpecifications: this.registeredResourceSpecs }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ registeredResourceSpecifications: NaN }], asyncScheduler); }) ), totalCreatedServices: this.serviceMetricsService.getTotalServices().pipe( catchError(error => { - console.error('Failed to load total services', error); - return scheduled([{ totalServices: this.totalCreatedServices }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ totalServices: NaN }], asyncScheduler); }) ), activeServices: this.serviceMetricsService.getTotalServices({ state: 'ACTIVE' }).pipe( catchError(error => { - console.error('Failed to load active services', error); - return scheduled([{ totalServices: this.activeServices }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ totalServices: NaN }], asyncScheduler); }) ), totalResources: this.resourceMetricsService.getTotalResources().pipe( catchError(error => { - console.error('Failed to load total resources', error); - return scheduled([{ totalResources: this.totalResources }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ totalResources: NaN }], asyncScheduler); }) ), availableResources: this.resourceMetricsService.getTotalResources({ state: 'AVAILABLE' }).pipe( catchError(error => { - console.error('Failed to load available resources', error); - return scheduled([{ totalResources: this.availableResources }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ totalResources: NaN }], asyncScheduler); }) ), activeServiceOrders: this.serviceOrderMetricsService.getTotalActiveServiceOrders().pipe( catchError(error => { - console.error('Failed to load active service orders', error); - return scheduled([{ activeServiceOrders: this.activeServiceOrders }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ activeServiceOrders: NaN }], asyncScheduler); }) ), totalCompletedOrders: this.serviceOrderMetricsService.getTotalServiceOrders({ state: 'COMPLETED' }).pipe( catchError(error => { - console.error('Failed to load completed service orders', error); - return scheduled([{ totalServiceOrders: this.totalCompletedOrders }], asyncScheduler); + this.toast.error('API services are not responding. Please verify service health.'); + return scheduled([{ totalServiceOrders: NaN }], asyncScheduler); }) ), serviceOrdersTotal15days: this.serviceOrderMetricsService.getServiceOrdersGroupedByDay({ starttime, endtime }).pipe( catchError(error => { - console.error('Failed to load service orders by day', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ serviceOrders:{ total: this.serviceOrdersTotal15days } }], asyncScheduler); }) ), serviceOrdersGroupedByDay: this.serviceOrderMetricsService.getServiceOrdersGroupedByDay({ starttime, endtime }).pipe( catchError(error => { - console.error('Failed to load service orders by day', error); + this.toast.error('API services are not responding. Please verify service health.'); const mockGroupByDay = this.barChartLabels.map(label => { const [day, month] = label.split('/').map(Number); const isoDate = new Date(Date.UTC(new Date().getFullYear(), month - 1, day)).toISOString(); @@ -266,13 +269,13 @@ export class MetricsComponent implements OnInit { ), serviceOrdersTotalMonth: this.serviceOrderMetricsService.getServiceOrdersGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load service orders by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ serviceOrders:{ total: this.serviceOrdersTotalMonth } }], asyncScheduler); }) ), serviceOrdersGroupedByState: this.serviceOrderMetricsService.getServiceOrdersGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load service orders by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ serviceOrders: { aggregations: { @@ -287,13 +290,13 @@ export class MetricsComponent implements OnInit { ), resourcesTotalMonth: this.resourceMetricsService.getResourcesGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load resources by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ resources:{ total: this.resourcesTotalMonth } }], asyncScheduler); }) ), resourcesGroupedByState: this.resourceMetricsService.getResourcesGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load resources by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ resources: { aggregations: { @@ -308,13 +311,13 @@ export class MetricsComponent implements OnInit { ), servicesTotalMonth: this.serviceMetricsService.getServicesGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load services by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ services:{ total: this.servicesTotalMonth } }], asyncScheduler); }) ), servicesGroupedByState: this.serviceMetricsService.getServicesGroupedByState({ starttime: starttimeMonth, endtime }).pipe( catchError(error => { - console.error('Failed to load services by state', error); + this.toast.error('API services are not responding. Please verify service health.'); return scheduled([{ services: { aggregations: { @@ -365,6 +368,7 @@ export class MetricsComponent implements OnInit { .filter(item => chartLabelsServices.includes(item.key)); this.servicesData.datasets[0].data = this.servicesByState.map(data => data.count); this.isLoading = false; + }); } @@ -373,6 +377,9 @@ export class MetricsComponent implements OnInit { this.authService.login() } + isNaN(value: any): boolean { + return isNaN(value); + } chartOptions: ChartConfiguration<'doughnut'>['options'] = { responsive: true, diff --git a/src/app/landing/metrics/statuscard/statuscard.component.html b/src/app/landing/metrics/statuscard/statuscard.component.html index bdfc87762240a481973ceb85c6eeebd74eec63b3..cef80385b63005876404c7c09b9304aa26aa6734 100644 --- a/src/app/landing/metrics/statuscard/statuscard.component.html +++ b/src/app/landing/metrics/statuscard/statuscard.component.html @@ -21,10 +21,8 @@
    -
    - -
    -
    + +
    @@ -49,13 +47,17 @@
    -
    {{statusStats}}
    +
    +
    + +
    + +
    {{statusStats}}
    +
    +
    -
    - -
    -

    {{statusTitle}}

    +

    {{statusTitle}}

    diff --git a/src/app/landing/metrics/statuscard/statuscard.component.scss b/src/app/landing/metrics/statuscard/statuscard.component.scss index 3a55ea16ac11241b2eadd01ebd52594db065ff3f..e08e37a861d4f09f201cb5e03a12fab3b05bc193 100644 --- a/src/app/landing/metrics/statuscard/statuscard.component.scss +++ b/src/app/landing/metrics/statuscard/statuscard.component.scss @@ -50,6 +50,10 @@ font-size: 0.9rem; } +.status-stats { + font-weight: 700; +} + .portal-button { color: #0078AE; } diff --git a/src/app/landing/metrics/statuscard/statuscard.component.ts b/src/app/landing/metrics/statuscard/statuscard.component.ts index a7162037af0a4fa1cdda79851edc7f55d9eebe54..eecabe92dea9564e775930afc7520af443db0402 100644 --- a/src/app/landing/metrics/statuscard/statuscard.component.ts +++ b/src/app/landing/metrics/statuscard/statuscard.component.ts @@ -19,5 +19,9 @@ export class StatusCardComponent implements OnInit { ngOnInit(): void { } + + isNaN(value: any): boolean { + return isNaN(value); + } } \ No newline at end of file