diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 78c19f686..3e737eb04 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,11 +1,10 @@ import { NgModule } from "@angular/core"; -import { Routes, RouterModule, PreloadAllModules } from "@angular/router"; -import { AuthComponent } from "./auth/auth.component"; -import { ErrorPageComponent } from "./error-page/error-page.component"; -import { SearchComponent } from "./search/search.component"; -import { AuthGuardService as AuthGuard } from "./auth/auth-guard.service"; +import { PreloadAllModules, RouterModule, Routes } from "@angular/router"; import { NewUserComponent } from "./admin/users/new-kombit-user-page/new-user.component"; import { UserPageComponent } from "./admin/users/user-page/user-page.component"; +import { AuthGuardService as AuthGuard } from "./auth/auth-guard.service"; +import { AuthComponent } from "./auth/auth.component"; +import { ErrorPageComponent } from "./error-page/error-page.component"; const routes: Routes = [ { @@ -44,7 +43,6 @@ const routes: Routes = [ loadChildren: () => import("./device-model/device-model.module").then(m => m.DeviceModelModule), canActivate: [AuthGuard], }, - { path: "search", component: SearchComponent, canActivate: [AuthGuard] }, { path: "not-found", component: ErrorPageComponent, data: { message: "not-found", code: 404 } }, { path: "not-authorized", component: ErrorPageComponent }, { path: "new-user", component: NewUserComponent, canActivate: [AuthGuard] }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0647bca93..526131d34 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,36 +1,37 @@ -import { BrowserModule, Title } from "@angular/platform-browser"; +import { NgIf } from "@angular/common"; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http"; import { NgModule } from "@angular/core"; -import { TranslateModule, TranslateLoader } from "@ngx-translate/core"; -import { TranslateHttpLoader } from "@ngx-translate/http-loader"; -import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; -import { AppRoutingModule } from "./app-routing.module"; -import { AppComponent } from "./app.component"; -import { NavbarModule } from "./navbar/navbar.module"; -import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { ProfilesModule } from "./profiles/profiles.module"; -import { AuthJwtInterceptor } from "@shared/helpers/auth-jwt.interceptor"; -import { AuthModule } from "./auth/auth.module"; -import { GatewayModule } from "./gateway/gateway.module"; -import { SharedVariableModule } from "@shared/shared-variable/shared-variable.module"; -import { SAVER, getSaver } from "@shared/providers/saver.provider"; -import { ErrorPageComponent } from "./error-page/error-page.component"; -import { SearchModule } from "./search/search.module"; -import { JwtModule } from "@auth0/angular-jwt"; -import { MonacoEditorModule } from "ngx-monaco-editor-v2"; import { MatInputModule } from "@angular/material/input"; import { MatPaginatorIntl } from "@angular/material/paginator"; -import { MatPaginatorIntlDa } from "@shared/helpers/mat-paginator-intl-da"; import { MatTooltipModule } from "@angular/material/tooltip"; -import { NewUserComponent } from "./admin/users/new-kombit-user-page/new-user.component"; +import { BrowserModule, Title } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { JwtModule } from "@auth0/angular-jwt"; +import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; +import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { MatSelectSearchModule } from "@shared/components/mat-select-search/mat-select-search.module"; import { WelcomeDialogModule } from "@shared/components/welcome-dialog/welcome-dialog.module"; +import { AuthJwtInterceptor } from "@shared/helpers/auth-jwt.interceptor"; +import { MatPaginatorIntlDa } from "@shared/helpers/mat-paginator-intl-da"; import { NGMaterialModule } from "@shared/Modules/materiale.module"; -import { MatSelectSearchModule } from "@shared/components/mat-select-search/mat-select-search.module"; -import { UserPageComponent } from "./admin/users/user-page/user-page.component"; -import { SharedModule } from "@shared/shared.module"; import { PipesModule } from "@shared/pipes/pipes.module"; +import { SAVER, getSaver } from "@shared/providers/saver.provider"; +import { SharedVariableModule } from "@shared/shared-variable/shared-variable.module"; +import { SharedModule } from "@shared/shared.module"; import { CookieService } from "ngx-cookie-service"; +import { MonacoEditorModule } from "ngx-monaco-editor-v2"; +import { NewUserComponent } from "./admin/users/new-kombit-user-page/new-user.component"; +import { UserPageComponent } from "./admin/users/user-page/user-page.component"; +import { AppRoutingModule } from "./app-routing.module"; +import { AppComponent } from "./app.component"; +import { AuthModule } from "./auth/auth.module"; +import { ErrorPageComponent } from "./error-page/error-page.component"; +import { GatewayModule } from "./gateway/gateway.module"; +import { NavbarModule } from "./navbar/navbar.module"; +import { ProfilesModule } from "./profiles/profiles.module"; +import { SearchModule } from "./search/search.module"; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, "./assets/i18n/", ".json"); @@ -60,6 +61,7 @@ export function tokenGetter() { }, }), NgbModule, + NgIf, FormsModule, ReactiveFormsModule, BrowserAnimationsModule, diff --git a/src/app/applications/application-detail/application-detail.component.ts b/src/app/applications/application-detail/application-detail.component.ts index cbc27fbac..6bee80ae9 100644 --- a/src/app/applications/application-detail/application-detail.component.ts +++ b/src/app/applications/application-detail/application-detail.component.ts @@ -1,25 +1,24 @@ -import { AfterViewInit, Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; +import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; import { Title } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; +import { Gateway, GatewayResponseMany } from "@app/gateway/gateway.model"; import { Application } from "@applications/application.model"; import { ApplicationService } from "@applications/application.service"; +import { IotDevicesApplicationMapResponse } from "@applications/iot-devices/iot-device.model"; import { TranslateService } from "@ngx-translate/core"; import { DeleteDialogService } from "@shared/components/delete-dialog/delete-dialog.service"; +import { OrganizationAccessScope } from "@shared/enums/access-scopes"; import { BackButton } from "@shared/models/back-button.model"; +import { ApplicationDialogModel } from "@shared/models/dialog.model"; import { DropdownButton } from "@shared/models/dropdown-button.model"; +import { ChirpstackGatewayService } from "@shared/services/chirpstack-gateway.service"; import { MeService } from "@shared/services/me.service"; -import { Subscription } from "rxjs"; -import { OrganizationAccessScope } from "@shared/enums/access-scopes"; -import { IotDevicesApplicationMapResponse } from "@applications/iot-devices/iot-device.model"; import { RestService } from "@shared/services/rest.service"; -import { Observable } from "rxjs"; -import { map } from "rxjs/operators"; import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; -import { ChirpstackGatewayService } from "@shared/services/chirpstack-gateway.service"; -import { Gateway, GatewayResponseMany } from "@app/gateway/gateway.model"; -import { MatDialog } from "@angular/material/dialog"; +import { Observable, Subscription } from "rxjs"; +import { map } from "rxjs/operators"; import { ApplicationChangeOrganizationDialogComponent } from "../application-change-organization-dialog/application-change-organization-dialog.component"; -import { ApplicationDialogModel } from "@shared/models/dialog.model"; @Component({ selector: "app-application", @@ -72,11 +71,13 @@ export class ApplicationDetailComponent implements OnInit, OnDestroy, AfterViewI private restService: RestService, private sharedVariableService: SharedVariableService, private chirpstackGatewayService: ChirpstackGatewayService, - private changeOrganizationDialog: MatDialog + private changeOrganizationDialog: MatDialog, + private cdr: ChangeDetectorRef ) {} ngOnInit(): void { this.id = +this.route.snapshot.paramMap.get("id"); + if (this.id) { this.bindApplication(this.id); this.dropdownButton = { @@ -147,7 +148,7 @@ export class ApplicationDetailComponent implements OnInit, OnDestroy, AfterViewI useGeolocation: false, markerInfo: { name: dev.name, - active: false, + active: dev.type, id: dev.id, isDevice: true, internalOrganizationId: this.sharedVariableService.getSelectedOrganisationId(), @@ -214,6 +215,7 @@ export class ApplicationDetailComponent implements OnInit, OnDestroy, AfterViewI bindApplication(id: number): void { this.applicationsSubscription = this.applicationService.getApplication(id).subscribe(application => { this.application = application; + this.cdr.detectChanges(); }); } diff --git a/src/app/applications/application.model.ts b/src/app/applications/application.model.ts index 15863e908..63fd9a979 100644 --- a/src/app/applications/application.model.ts +++ b/src/app/applications/application.model.ts @@ -1,12 +1,14 @@ +import { PermissionResponse } from "@app/admin/permission/permission.model"; import { ControlledPropertyTypes } from "@app/device-model/Enums/controlled-propperty.enum"; +import { Datatarget } from "@applications/datatarget/datatarget.model"; import { ApplicationDeviceTypeUnion } from "@shared/enums/device-type"; import { ControlledProperty } from "@shared/models/controlled-property.model"; import { Organisation } from "../admin/organisation/organisation.model"; import { ApplicationStatus } from "./enums/status.enum"; import { IotDevice } from "./iot-devices/iot-device.model"; import { ApplicationDeviceType } from "./models/application-device-type.model"; -import { PermissionResponse } from "@app/admin/permission/permission.model"; -import { Datatarget } from "@applications/datatarget/datatarget.model"; + +export type ApplicationWithStatus = Application & { statusCheck: "stable" | "alert" }; export class Application { public id: number; diff --git a/src/app/applications/application.service.ts b/src/app/applications/application.service.ts index 80936717c..2740a8f4a 100644 --- a/src/app/applications/application.service.ts +++ b/src/app/applications/application.service.ts @@ -1,9 +1,12 @@ import { Injectable } from "@angular/core"; +import { UserMinimalService } from "@app/admin/users/user-minimal.service"; import { Application, ApplicationData, UpdateApplicationOrganization } from "@applications/application.model"; -import { RestService } from "../shared/services/rest.service"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; -import { UserMinimalService } from "@app/admin/users/user-minimal.service"; +import { RestService } from "../shared/services/rest.service"; +import { ApplicationsFilterService } from "./applications-list/application-filter/applications-filter.service"; +import { ApplicationStatus, ApplicationStatusCheck } from "./enums/status.enum"; +import { IotDevice } from "./iot-devices/iot-device.model"; interface GetApplicationParameters { limit: number; @@ -12,6 +15,15 @@ interface GetApplicationParameters { orderOn: string; organizationId?: number; permissionId?: number; + status?: ApplicationStatus; + statusCheck?: ApplicationStatusCheck; + owner?: string; +} + +interface GetDevicesParameters { + status?: ApplicationStatus; + statusCheck?: ApplicationStatusCheck; + owner?: string; } @Injectable({ @@ -20,7 +32,11 @@ interface GetApplicationParameters { export class ApplicationService { public id: number; public canEdit = false; - constructor(private restService: RestService, private userMinimalService: UserMinimalService) {} + constructor( + private restService: RestService, + private userMinimalService: UserMinimalService, + private filterService: ApplicationsFilterService + ) {} createApplication(body: any): Observable { return this.restService.post("application", body, { observe: "response" }); @@ -41,6 +57,17 @@ export class ApplicationService { }) ); } + getApplicationFilterOptions(id: number): Observable { + return this.restService.get(`application/${id}/filter-information`); + } + + getApplicationsWithError(id: number): Observable<{ + total: number; + withError: number; + totalDevices: number; + }> { + return this.restService.get(`application/${id}/application-dashboard-data`); + } getApplications( limit: number, @@ -55,6 +82,9 @@ export class ApplicationService { offset, sort, orderOn, + statusCheck: this.filterService.statusCheck === "All" ? null : this.filterService.statusCheck, + status: this.filterService.status === "All" ? null : this.filterService.status, + owner: this.filterService.owner === "All" ? null : this.filterService.owner, }; if (permissionId) { body.permissionId = permissionId; @@ -66,6 +96,16 @@ export class ApplicationService { return this.restService.get("application", body); } + getApplicationDevices(organizationId?: number): Observable { + const body: GetDevicesParameters = { + statusCheck: this.filterService.statusCheck === "All" ? null : this.filterService.statusCheck, + status: this.filterService.status === "All" ? null : this.filterService.status, + owner: this.filterService.owner === "All" ? null : this.filterService.owner, + }; + + return this.restService.get(`application/${organizationId}/iot-devices-org`, body); + } + getApplicationsByOrganizationId(organizationId: number): Observable { const body = { organizationId, diff --git a/src/app/applications/applications-list/application-filter/application-filter.component.html b/src/app/applications/applications-list/application-filter/application-filter.component.html new file mode 100644 index 000000000..931174f9d --- /dev/null +++ b/src/app/applications/applications-list/application-filter/application-filter.component.html @@ -0,0 +1,47 @@ +
+
+
{{ "APPLICATION-FILTER.STATUS" | translate }}
+ + + + {{ option.label | translate }} + + + +
+ +
+
{{ "APPLICATION-FILTER.STATE" | translate }}
+ + + + {{ option.label | translate }} + + + +
+ +
+
{{ "APPLICATION-FILTER.OWNER" | translate }}
+ + + + {{ "APPLICATION-FILTER.ALL" | translate }} + + + {{ option.label }} + + + +
+ +
+ +
diff --git a/src/app/applications/applications-list/application-filter/application-filter.component.scss b/src/app/applications/applications-list/application-filter/application-filter.component.scss new file mode 100644 index 000000000..fa699bdd0 --- /dev/null +++ b/src/app/applications/applications-list/application-filter/application-filter.component.scss @@ -0,0 +1,83 @@ +@import "../../../../assets/scss/setup/variables"; + +.filter-container { + display: flex; + flex-direction: row; + flex: 1; + min-height: 65px; + margin-bottom: 25px; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + .filter-container { + flex-direction: column; + } +} + +.filter-selector-container { + width: 200px; + margin-right: 30px; + display: flex; + flex-direction: column; + margin-bottom: 30px; +} + +.filter-title { + font-size: 14px; +} + +.spacer { + display: flex; + flex: 1; +} + +.filter-button { + margin-top: 26px; + height: 40px !important; + width: 170px !important; + min-width: 170px !important; + justify-content: center; + display: flex !important; + align-items: center; + border: 1px solid $color-link !important; +} + +.filter-icon-container { + display: flex !important; + align-items: center; + justify-content: center; + gap: 10px; + width: 150px; +} + +.filter-label { + font-size: 16px; + font-weight: 500; +} + +:host .mat-mdc-select { + height: 24px; + display: flex; + align-items: center; + font-size: 16px !important; + color: $selector-font-color !important; +} + +:host .mat-mdc-form-field { + height: 20px !important; + display: flex; + align-items: center; +} + +:host .mat-mdc-text-field-wrapper { + background-color: $white !important; + height: 40px !important; + display: flex !important; + align-items: center !important; + font-size: 16px !important; +} + +:host .mat-mdc-select-arrow { + display: none; +} diff --git a/src/app/applications/applications-list/application-filter/application-filter.component.ts b/src/app/applications/applications-list/application-filter/application-filter.component.ts new file mode 100644 index 000000000..3ba13cc74 --- /dev/null +++ b/src/app/applications/applications-list/application-filter/application-filter.component.ts @@ -0,0 +1,95 @@ +import { NgFor, NgOptimizedImage } from "@angular/common"; +import { Component, OnInit, ViewEncapsulation } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatOptionModule } from "@angular/material/core"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatIconModule } from "@angular/material/icon"; +import { MatSelectModule } from "@angular/material/select"; +import { ApplicationService } from "@applications/application.service"; +import { ApplicationStatus, ApplicationStatusCheck } from "@applications/enums/status.enum"; +import { ChirpstackGatewayService } from "./../../../shared/services/chirpstack-gateway.service"; + +import { TranslateModule, TranslateService } from "@ngx-translate/core"; +import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; +import { ApplicationsFilterService } from "./applications-filter.service"; + +@Component({ + selector: "app-application-filter", + standalone: true, + imports: [ + NgFor, + NgOptimizedImage, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + MatOptionModule, + MatButtonModule, + TranslateModule, + ], + templateUrl: "./application-filter.component.html", + styleUrl: "./application-filter.component.scss", + encapsulation: ViewEncapsulation.ShadowDom, +}) +export class ApplicationFilterComponent implements OnInit { + constructor( + private applicationService: ApplicationService, + private filterService: ApplicationsFilterService, + private sharedVariableService: SharedVariableService, + public translate: TranslateService, + public ChirpstackGatewayService: ChirpstackGatewayService + ) {} + ngOnInit(): void { + this.loadOwnerOptions(); + this.ChirpstackGatewayService.getMultiple().subscribe(data => data); + } + + stateOptions: { label: string; value: ApplicationStatus | "All" }[] = [ + { label: "APPLICATION-FILTER.ALL", value: "All" }, + { label: "APPLICATION-FILTER.NONE", value: ApplicationStatus["NONE"] }, + { label: "APPLICATION-FILTER.IN-OPERATION", value: ApplicationStatus["IN-OPERATION"] }, + { label: "APPLICATION-FILTER.PROJECT", value: ApplicationStatus["PROJECT"] }, + { label: "APPLICATION-FILTER.PROTOTYPE", value: ApplicationStatus["PROTOTYPE"] }, + { label: "APPLICATION-FILTER.OTHER", value: ApplicationStatus["OTHER"] }, + ]; + + statusOptions: { label: string; value: ApplicationStatusCheck | "All" }[] = [ + { label: "APPLICATION-FILTER.ALL", value: "All" }, + { label: "APPLICATION-FILTER.ALERT", value: "alert" }, + { label: "APPLICATION-FILTER.STABLE", value: "stable" }, + ]; + + ownerOptions: { label: string; value: string | "All" }[] = []; + + loadOwnerOptions(): void { + this.applicationService + .getApplicationFilterOptions(this.sharedVariableService.getSelectedOrganisationId()) + .subscribe(options => { + const optionsArray: { label: string; value: string }[] = []; + options.forEach(option => optionsArray.push({ label: option, value: option })); + this.ownerOptions = optionsArray; + }); + } + + state: string = "All"; + status: string = "All"; + owner: string = "All"; + + onStatusCheck(event: any): void { + this.filterService.updateStatusCheck(event.value); + } + + onStatus(event: any): void { + this.filterService.updateStatus(event.value); + } + + onOwner(event: any): void { + this.filterService.updateOwner(event.value); + } + + onButtonClick() { + this.state = "All"; + this.status = "All"; + this.owner = "All"; + this.filterService.resetFilter(); + } +} diff --git a/src/app/applications/applications-list/application-filter/applications-filter.service.ts b/src/app/applications/applications-list/application-filter/applications-filter.service.ts new file mode 100644 index 000000000..2150cfd7c --- /dev/null +++ b/src/app/applications/applications-list/application-filter/applications-filter.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@angular/core"; +import { ApplicationStatus, ApplicationStatusCheck } from "@applications/enums/status.enum"; +import { BehaviorSubject } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class ApplicationsFilterService { + public status: ApplicationStatus | "All" = "All"; + + public statusCheck: ApplicationStatusCheck | "All" = "All"; + public owner: string | "All" = "All"; + + private valueChanges = new BehaviorSubject<{ + status: ApplicationStatus | "All"; + statusCheck: ApplicationStatusCheck | "All"; + owner: string; + }>({ + status: this.status, + statusCheck: this.statusCheck, + owner: this.owner, + }); + + filterChanges$ = this.valueChanges.asObservable(); + + updateStatus(newValue: ApplicationStatus | null) { + this.status = newValue; + this.emitChange(); + } + + updateStatusCheck(newValue: ApplicationStatusCheck | null) { + this.statusCheck = newValue; + this.emitChange(); + } + + updateOwner(newValue: string) { + this.owner = newValue; + this.emitChange(); + } + + public resetFilter() { + this.statusCheck = "All"; + this.status = "All"; + this.owner = "All"; + this.emitChange(); + } + + private emitChange() { + this.valueChanges.next({ statusCheck: this.statusCheck, status: this.status, owner: this.owner }); + } +} diff --git a/src/app/applications/applications-list/application-map/application-map.component.html b/src/app/applications/applications-list/application-map/application-map.component.html new file mode 100644 index 000000000..ac32fa940 --- /dev/null +++ b/src/app/applications/applications-list/application-map/application-map.component.html @@ -0,0 +1,5 @@ +
+ @if(coordinateList){ + + } +
diff --git a/src/app/applications/applications-list/application-map/application-map.component.scss b/src/app/applications/applications-list/application-map/application-map.component.scss new file mode 100644 index 000000000..3ce8b2c05 --- /dev/null +++ b/src/app/applications/applications-list/application-map/application-map.component.scss @@ -0,0 +1,36 @@ +@import "../../../../assets/scss/setup/variables"; + +:host .map-page { + background-color: #ffffff; + padding: 20px; + height: 600px; + position: relative; +} + +:host .map-overlay { + height: 50px; + width: 132px; + background-color: $white; + position: absolute; + bottom: 30px; + left: 30px; + z-index: 10000; + border-radius: 4px; + display: flex; + flex-direction: column; + padding-top: 5px; +} + +.check-box-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.mdc-checkbox__background { + background-color: $white !important; +} + +.mdc-checkbox { + background-color: $white !important; +} diff --git a/src/app/applications/applications-list/application-map/application-map.component.ts b/src/app/applications/applications-list/application-map/application-map.component.ts new file mode 100644 index 000000000..f4a8c5de8 --- /dev/null +++ b/src/app/applications/applications-list/application-map/application-map.component.ts @@ -0,0 +1,127 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { Gateway } from "@app/gateway/gateway.model"; +import { ApplicationService } from "@applications/application.service"; +import { ApplicationStatus, ApplicationStatusCheck } from "@applications/enums/status.enum"; +import { IotDevice } from "@applications/iot-devices/iot-device.model"; +import { TranslateService } from "@ngx-translate/core"; +import { MapCoordinates } from "@shared/components/map/map-coordinates.model"; +import { ChirpstackGatewayService } from "@shared/services/chirpstack-gateway.service"; +import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; +import { forkJoin, Subscription } from "rxjs"; +import { SharedModule } from "../../../shared/shared.module"; +import { ApplicationsFilterService } from "../application-filter/applications-filter.service"; +import moment from "moment"; + +@Component({ + selector: "app-application-map", + standalone: true, + imports: [MatCheckboxModule, SharedModule], + templateUrl: "./application-map.component.html", + styleUrls: ["./application-map.component.scss"], +}) +export class ApplicationMapComponent implements OnInit, OnDestroy { + public devices: IotDevice[] = []; + public gateways: Gateway[] = []; + public device: boolean = true; + public gateway: boolean = true; + filterValues: { + status: ApplicationStatus | "All"; + statusCheck: ApplicationStatusCheck | "All"; + owner: string | "All"; + }; + coordinateList: MapCoordinates[] = null; + private valueSubscription!: Subscription; + + constructor( + private filterService: ApplicationsFilterService, + private applicationService: ApplicationService, + public translate: TranslateService, + private sharedVariableService: SharedVariableService, + private gatewayService: ChirpstackGatewayService + ) {} + + ngOnInit() { + this.loadMapData(); + + this.valueSubscription = this.filterService.filterChanges$.subscribe(updatedValues => { + this.filterValues = updatedValues; + }); + } + + ngOnDestroy() { + if (this.valueSubscription) { + this.valueSubscription.unsubscribe(); + } + } + + private loadMapData(): void { + forkJoin({ + devices: this.applicationService.getApplicationDevices(this.sharedVariableService.getSelectedOrganisationId()), + gateways: this.gatewayService.getForMaps(), + }).subscribe(({ devices, gateways }) => { + this.devices = devices; + this.gateways = gateways.resultList; + + this.mapToCoordinateList(); + }); + } + + private mapToCoordinateList() { + const tempCoordinateList: MapCoordinates[] = []; + + if (Array.isArray(this.devices)) { + this.devices.forEach(dev => { + const [longitude, latitude] = dev.location.coordinates; + + const isActive = dev.latestReceivedMessage?.sentTime + ? moment(dev.latestReceivedMessage?.sentTime).unix() > moment(new Date()).subtract(1, "day").unix() + : false; + + tempCoordinateList.push({ + longitude: longitude, + latitude: latitude, + draggable: false, + editEnabled: false, + useGeolocation: false, + markerInfo: { + internalOrganizationName: "s", + name: dev.name, + active: isActive, + isGateway: false, + id: dev.id, + isDevice: true, + internalOrganizationId: this.sharedVariableService.getSelectedOrganisationId(), + networkTechnology: dev.type, + lastActive: dev?.latestReceivedMessage?.sentTime, + }, + }); + }); + } + + if (Array.isArray(this.gateways)) { + this.gateways.forEach(gw => { + tempCoordinateList.push({ + longitude: gw.location.longitude, + latitude: gw.location.latitude, + draggable: false, + editEnabled: false, + useGeolocation: false, + markerInfo: { + internalOrganizationName: gw.organizationName, + name: gw.name, + active: this.gatewayService.isGatewayActive(gw), + id: gw.id, + isDevice: false, + isGateway: true, + internalOrganizationId: this.sharedVariableService.getSelectedOrganisationId(), + networkTechnology: "", + lastActive: gw.lastSeenAt, + }, + }); + }); + } + + this.coordinateList = tempCoordinateList; + } +} diff --git a/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.html b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.html new file mode 100644 index 000000000..416b2f040 --- /dev/null +++ b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.html @@ -0,0 +1,36 @@ +
+ + + + +
diff --git a/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.scss b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.scss new file mode 100644 index 000000000..e22fc819c --- /dev/null +++ b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.scss @@ -0,0 +1,7 @@ +.info-box-containers { + display: flex; + flex-direction: row; + gap: 30px; + margin-bottom: 30px; + height: fit-content; +} diff --git a/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.ts b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.ts new file mode 100644 index 000000000..6cb697862 --- /dev/null +++ b/src/app/applications/applications-list/applications-list-dashboard/applications-list-dashboard.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit } from "@angular/core"; +import { MatIconRegistry } from "@angular/material/icon"; +import { DomSanitizer } from "@angular/platform-browser"; +import { ApplicationService } from "@applications/application.service"; +import { TranslateModule, TranslateService } from "@ngx-translate/core"; +import { ChirpstackGatewayService } from "@shared/services/chirpstack-gateway.service"; +import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; +import { BasicInformationBoxComponent } from "../../../shared/components/basic-information-box/basic-information-box.component"; + +@Component({ + selector: "app-applications-list-dashboard", + standalone: true, + imports: [BasicInformationBoxComponent, TranslateModule], + templateUrl: "./applications-list-dashboard.component.html", + styleUrl: "./applications-list-dashboard.component.scss", +}) +export class ApplicationsListDashboardComponent implements OnInit { + total: number = 0; + withError: number = 0; + withoutError: number = 0; + totalDevices: number = 0; + totalGateways: number; + + constructor( + private gatewayService: ChirpstackGatewayService, + private applicationService: ApplicationService, + private translate: TranslateService, + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private sharedVariableService: SharedVariableService + ) { + this.matIconRegistry.addSvgIcon( + "micro-chip", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/microchip.svg"), + {} + ); + + this.matIconRegistry.addSvgIcon( + "satellite-dish", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/satellite-dish.svg"), + {} + ); + + this.matIconRegistry.addSvgIcon( + "check-circle", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/check-circle.svg"), + {} + ); + + this.matIconRegistry.addSvgIcon( + "exclamation-triangle", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/exclamation-triangle.svg"), + {} + ); + } + ngOnInit(): void { + this.applicationService + .getApplicationsWithError(this.sharedVariableService.getSelectedOrganisationId()) + .subscribe(data => { + this.withError = data.withError; + this.totalDevices = data.totalDevices; + this.withoutError = data.total - data.withError; + this.total = data.total; + }); + + this.gatewayService + .getMultiple() + .subscribe( + data => + (this.totalGateways = data.resultList.filter( + gw => gw.organizationId === this.sharedVariableService.getSelectedOrganisationId() + ).length) + ); + } +} diff --git a/src/app/applications/applications-list/applications-list.component.html b/src/app/applications/applications-list/applications-list.component.html index 4fc5997cd..c0cbc0dea 100644 --- a/src/app/applications/applications-list/applications-list.component.html +++ b/src/app/applications/applications-list/applications-list.component.html @@ -13,23 +13,28 @@

{{ "WELCOME-DIALOG.NO-ACCESS" | translate }}

-
+
-
-
-
- -
-
+ + +
+ + + @if (currentPath === mapRoute) { + + } + @if (currentPath === listRoute) { + + }
- +
diff --git a/src/app/applications/applications-list/applications-list.component.scss b/src/app/applications/applications-list/applications-list.component.scss index e69de29bb..07f7c01f5 100644 --- a/src/app/applications/applications-list/applications-list.component.scss +++ b/src/app/applications/applications-list/applications-list.component.scss @@ -0,0 +1,11 @@ +.info-box-containers { + display: flex; + flex-direction: row; + gap: 30px; + margin-bottom: 30px; + height: fit-content; +} + +.main-page { + margin: 30px; +} diff --git a/src/app/applications/applications-list/applications-list.component.ts b/src/app/applications/applications-list/applications-list.component.ts index b569758a3..3332871c9 100644 --- a/src/app/applications/applications-list/applications-list.component.ts +++ b/src/app/applications/applications-list/applications-list.component.ts @@ -1,18 +1,20 @@ import { Component, Input, OnInit } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; -import { Title } from "@angular/platform-browser"; +import { MatIconRegistry } from "@angular/material/icon"; +import { DomSanitizer, Title } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { UserMinimalService } from "@app/admin/users/user-minimal.service"; import { NavbarComponent } from "@app/navbar/navbar.component"; -import { Application } from "@applications/application.model"; +import { ApplicationService } from "@applications/application.service"; import { AuthService } from "@auth/auth.service"; import { environment } from "@environments/environment"; import { TranslateService } from "@ngx-translate/core"; import { WelcomeDialogComponent } from "@shared/components/welcome-dialog/welcome-dialog.component"; -import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; import { OrganizationAccessScope } from "@shared/enums/access-scopes"; -import { MeService } from "@shared/services/me.service"; import { WelcomeDialogModel } from "@shared/models/dialog.model"; +import { MeService } from "@shared/services/me.service"; +import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; +import { Counter, Tab } from "@shared/components/basic-tab-switch/basic-tab-switch.component"; const welcomeDialogId = "welcome-dialog"; @@ -23,19 +25,24 @@ const welcomeDialogId = "welcome-dialog"; styleUrls: ["./applications-list.component.scss"], }) export class ApplicationsListComponent implements OnInit { + currentSubPath: string = ""; + tabs: Tab[]; + isLoadingResults = true; public pageLimit = environment.tablePageSize; public resultsLength: number; - public pageOffset = 0; - public applications: Application[]; + mapRoute = "/applications/map"; + listRoute = "/applications"; + @Input() organizationId: number; canEdit: boolean; + hasSomePermission: boolean; + isGlobalAdmin = false; + currentPath = ""; private unauthorizedMessage: string; private kombitError: string; private noAccess: string; - hasSomePermission: boolean; - isGlobalAdmin = false; constructor( public translate: TranslateService, @@ -44,23 +51,69 @@ export class ApplicationsListComponent implements OnInit { private meService: MeService, private sharedVariableService: SharedVariableService, private authService: AuthService, - private route: ActivatedRoute, - private router: Router, + public route: ActivatedRoute, + public router: Router, private dialog: MatDialog, - private userMinimalService: UserMinimalService + private userMinimalService: UserMinimalService, + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private applicationService: ApplicationService ) { translate.use("da"); + + this.matIconRegistry.addSvgIcon( + "layers-tap", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/layers.svg"), + {} + ); + + this.matIconRegistry.addSvgIcon( + "map-tap", + this.domSanitizer.bypassSecurityTrustResourceUrl("assets/images/circle-dot.svg"), + {} + ); } ngOnInit(): void { + this.route.url.subscribe(urlSegments => { + this.currentSubPath = urlSegments.map(segment => segment.path).join("/"); + }); + this.translate.get(["TITLE.APPLICATION"]).subscribe(translations => { this.titleService.setTitle(translations["TITLE.APPLICATION"]); }); this.organizationId = this.globalService.getSelectedOrganisationId(); this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); + this.applicationService + .getApplicationsWithError(this.sharedVariableService.getSelectedOrganisationId()) + .subscribe(data => { + const counters: Counter[] = [ + { + color: "default", + value: data.total.toString(), + }, + ]; + + if (data.withError) { + counters.push({ color: "alert", value: data.withError.toString() }); + } + + this.tabs = [ + { + title: "Applikationer", + icon: { matSVGSrc: "layers-tap", height: 16, width: 16 }, + counters: counters, + uri: this.listRoute, + }, + { title: "Kort", icon: { matSVGSrc: "map-tap", height: 17, width: 18 }, uri: this.mapRoute }, + ]; + }); + // Authenticate user this.verifyUserAndInit(); + + this.currentPath = this.router.url; } verifyUserAndInit() { @@ -156,4 +209,8 @@ export class ApplicationsListComponent implements OnInit { this.isLoadingResults = false; }); } + + onTapClicked(url: string) { + this.currentPath = url; + } } diff --git a/src/app/applications/applications-list/applications-table/applications-table.component.html b/src/app/applications/applications-list/applications-table/applications-table.component.html index 4e641b6b6..8adf062c4 100644 --- a/src/app/applications/applications-list/applications-table/applications-table.component.html +++ b/src/app/applications/applications-list/applications-table/applications-table.component.html @@ -1,95 +1,156 @@ - - -
-
- -
-
-
- {{ errorMessage | translate }} -
- +
+ +
+
+ {{ errorMessage | translate }} +
+
+
+
- - - - - - - + + + + + - - - - - - - + + + + + - - - - - - + - +
- {{ "APPLICATION-TABLE.NAME" | translate }} + + +
+ {{ "APPLICATION-TABLE.STATUS" | translate }} +
+
- {{ - element.name - }} + + - {{ "APPLICATION-TABLE.OWNER" | translate }} + + +
+ {{ "APPLICATION-TABLE.STATE" | translate }} +
+
- {{ application.owner ?? "-" }} +
+ +
- {{ "APPLICATION-TABLE.CONTACT-PERSON" | translate }} + + +
+ {{ "APPLICATION-TABLE.NAME" | translate }} +
+ +
+
+
+ {{ element.name }} +
+
+ {{ element.description }} +
+
+
+
+ {{ "APPLICATION-TABLE.CONTROLLED-PROPERTIES" | translate }} +
+
- {{ application.contactPerson ?? "-" }} +
+ @for (property of application.controlledProperties; track $index) { + + } +
- {{ "APPLICATION-TABLE.IOT-DEVICES" | translate }} + +
+ {{ "APPLICATION-TABLE.IOT-DEVICES" | translate }} +
+
{{ element?.iotDevices?.length ?? 0 }} - {{ "APPLICATION-TABLE.DATA-TARGETS" | translate }} + + +
+ {{ "APPLICATION-TABLE.OWNER" | translate }} +
+
- {{ application?.dataTargets?.length ?? 0 }} + {{ application.owner ?? "-" }} - {{ "APPLICATION-TABLE.OPEN-DATA-DK" | translate }} + + + + +
+ {{ "APPLICATION-TABLE.CONTACT-PERSON" | translate }} +
+
- {{ isOpenDataDK(application.dataTargets) | yesNo }} + {{ application.contactPerson ?? "-" }} - {{ "APPLICATION-TABLE.STATUS" | translate }} + + +
+ {{ "APPLICATION-TABLE.DATA-TARGETS" | translate }} +
+ +
+ {{ application?.dataTargets?.length ?? 0 }} + +
+ {{ "APPLICATION-TABLE.OPEN-DATA-DK" | translate }} +
+
- {{ application.status ? ("APPLICATION.STATUS." + application.status | translate) : "-" }} + {{ isOpenDataDK(application.dataTargets) | yesNo }} - {{ "APPLICATION-TABLE.PERSONAL-DATA" | translate }} + +
+ {{ "APPLICATION-TABLE.PERSONAL-DATA" | translate }} +
+
@@ -99,7 +160,10 @@ - {{ "APPLICATION-TABLE.START-DATE" | translate }} +
+ {{ "APPLICATION-TABLE.START-DATE" | translate }} +
+
{{ (application.startDate | dateOnly) ?? "-" }} @@ -108,7 +172,10 @@ - {{ "APPLICATION-TABLE.END-DATE" | translate }} +
+ {{ "APPLICATION-TABLE.END-DATE" | translate }} +
+
{{ (application.endDate | dateOnly) ?? "-" }} @@ -117,25 +184,22 @@ - {{ "APPLICATION-TABLE.CATEGORY" | translate }} +
+ {{ "APPLICATION-TABLE.CATEGORY" | translate }} +
+
{{ application.category ?? "-" }} - {{ "APPLICATION-TABLE.CONTROLLED-PROPERTIES" | translate }} - - {{ mapControlledProperties(application.controlledProperties) ?? "-" }} - - {{ "APPLICATION-TABLE.DEVICE-TYPES" | translate }} +
+ {{ "APPLICATION-TABLE.DEVICE-TYPES" | translate }} +
+
{{ mapDeviceTypes(application.deviceTypes) ?? "-" }} @@ -144,6 +208,7 @@
+
- - - +
+ +
+
+
+
diff --git a/src/app/applications/applications-list/applications-table/applications-table.component.scss b/src/app/applications/applications-list/applications-table/applications-table.component.scss index 29eef3e50..3c1d07ea9 100644 --- a/src/app/applications/applications-list/applications-table/applications-table.component.scss +++ b/src/app/applications/applications-list/applications-table/applications-table.component.scss @@ -1,3 +1,68 @@ -.flag-icon { - color: red; +@import "../../../../assets/scss/setup/variables"; + +.name-and-description { + max-width: 300px; +} + +.application-name { + font-weight: bold; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.application-description { + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.options-container { + display: flex; + width: fit-content; + gap: 5px; + align-items: center; + flex-wrap: wrap; + margin-top: 5px; +} + +.status-container { + width: 10px; +} + +.main-container-table { + background-color: $white; + display: flex; + flex-direction: column; +} + +.tool-container { + width: 35px; + background-color: $white; +} + +.table-container { + display: flex; + flex: 1; +} + +.paginator { + display: none !important; +} + +.mat-sort-header-arrow { + display: none !important; +} + +.column-title-color { + color: $color-link; + font-weight: bold; +} + +.column-title-color-inactive { + color: $default-icon-color; } diff --git a/src/app/applications/applications-list/applications-table/applications-table.component.ts b/src/app/applications/applications-list/applications-table/applications-table.component.ts index 3b469b6b6..4e0890f05 100644 --- a/src/app/applications/applications-list/applications-table/applications-table.component.ts +++ b/src/app/applications/applications-list/applications-table/applications-table.component.ts @@ -7,44 +7,50 @@ import { ViewChild, ViewEncapsulation, } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; import { MatPaginator } from "@angular/material/paginator"; import { MatSort } from "@angular/material/sort"; import { Router } from "@angular/router"; +import { ApplicationChangeOrganizationDialogComponent } from "@applications/application-change-organization-dialog/application-change-organization-dialog.component"; import { Application, ApplicationData } from "@applications/application.model"; import { ApplicationService } from "@applications/application.service"; -import { environment } from "@environments/environment"; +import { Datatarget } from "@applications/datatarget/datatarget.model"; +import { ApplicationDeviceType } from "@applications/models/application-device-type.model"; +import { faFlag } from "@fortawesome/free-solid-svg-icons"; import { TranslateService } from "@ngx-translate/core"; import { DeleteDialogService } from "@shared/components/delete-dialog/delete-dialog.service"; -import { merge, Observable, of as observableOf } from "rxjs"; -import { catchError, map, startWith, switchMap } from "rxjs/operators"; import { DefaultPageSizeOptions } from "@shared/constants/page.constants"; -import { ControlledProperty } from "@shared/models/controlled-property.model"; import { ApplicationDeviceTypeEntries } from "@shared/enums/device-type"; -import { ApplicationDeviceType } from "@applications/models/application-device-type.model"; -import { Datatarget } from "@applications/datatarget/datatarget.model"; -import { faFlag } from "@fortawesome/free-solid-svg-icons"; -import { TableColumn } from "@shared/types/table.type"; -import { MatDialog } from "@angular/material/dialog"; +import { ControlledProperty } from "@shared/models/controlled-property.model"; import { ApplicationDialogModel } from "@shared/models/dialog.model"; -import { ApplicationChangeOrganizationDialogComponent } from "@applications/application-change-organization-dialog/application-change-organization-dialog.component"; +import { TableColumn } from "@shared/types/table.type"; +import { merge, Observable, of as observableOf } from "rxjs"; +import { catchError, map, startWith, switchMap } from "rxjs/operators"; +import { ApplicationsFilterService } from "../application-filter/applications-filter.service"; const columnDefinitions: TableColumn[] = [ { - id: "name", - display: "APPLICATION-TABLE.NAME", + id: "statusCheck", + display: "APPLICATION-TABLE.STATUS", default: true, toggleable: false, }, { - id: "owner", - display: "APPLICATION-TABLE.OWNER", + id: "status", + display: "APPLICATION-TABLE.STATE", default: true, toggleable: true, }, { - id: "contactPerson", - display: "APPLICATION-TABLE.CONTACT-PERSON", - default: false, + id: "name", + display: "APPLICATION-TABLE.NAME", + default: true, + toggleable: false, + }, + { + id: "data", + display: "APPLICATION-TABLE.CATEGORY", + default: true, toggleable: true, }, { @@ -53,28 +59,35 @@ const columnDefinitions: TableColumn[] = [ default: true, toggleable: true, }, + { + id: "owner", + display: "APPLICATION-TABLE.OWNER", + default: true, + toggleable: true, + }, { id: "dataTargets", display: "APPLICATION-TABLE.DATA-TARGETS", default: true, toggleable: true, }, + // Not default columns { - id: "openDataDkEnabled", - display: "APPLICATION-TABLE.OPEN-DATA-DK", + id: "contactPerson", + display: "APPLICATION-TABLE.CONTACT-PERSON", default: false, toggleable: true, }, { - id: "status", - display: "APPLICATION-TABLE.STATUS", - default: true, + id: "openDataDkEnabled", + display: "APPLICATION-TABLE.OPEN-DATA-DK", + default: false, toggleable: true, }, { id: "personalData", display: "APPLICATION-TABLE.PERSONAL-DATA", - default: true, + default: false, toggleable: true, }, { @@ -95,12 +108,6 @@ const columnDefinitions: TableColumn[] = [ default: false, toggleable: true, }, - { - id: "controlledProperties", - display: "APPLICATION-TABLE.CONTROLLED-PROPERTIES", - default: false, - toggleable: true, - }, { id: "deviceTypes", display: "APPLICATION-TABLE.DEVICE-TYPES", @@ -131,14 +138,12 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { data: Application[] = []; - public pageSize = environment.tablePageSize; pageSizeOptions = DefaultPageSizeOptions; resultsLength = 0; isLoadingResults = true; public errorMessage: string; applicationSavedColumns = "applicationSavedColumns"; - @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -148,7 +153,8 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { private router: Router, private deleteDialogService: DeleteDialogService, private cdRef: ChangeDetectorRef, - private changeOrganizationDialog: MatDialog + private changeOrganizationDialog: MatDialog, + private filterService: ApplicationsFilterService ) {} ngOnInit() { @@ -157,11 +163,14 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { this.translate.use("da"); } + announceSortChange(event: { active: string; direction: string }) { + this.columnDefinitions.find(column => column.id === event.active).sort = event.direction as "asc" | "desc"; + } ngAfterViewInit() { // If the user changes the sort order, reset back to the first page. this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0)); - merge(this.sort.sortChange, this.paginator.page) + merge(this.sort.sortChange, this.paginator.page, this.filterService.filterChanges$) .pipe( startWith({}), switchMap(() => { @@ -180,7 +189,9 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { return observableOf([]); }) ) - .subscribe(data => (this.data = data)); + .subscribe(data => { + this.data = data; + }); } getApplications(orderByColumn: string, orderByDirection: string): Observable { @@ -197,9 +208,7 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { map((data: ApplicationData) => { // Status is getting translated in frontend, and therefore sorting is not working since the backend doesn't know the translation // Therefore we do it manually in the frontend. - if (orderByColumn !== "status") { - return data; - } else { + if (orderByColumn === "status") { data.data.sort((a: Application, b: Application) => { const valueA = a[orderByColumn]; const valueB = b[orderByColumn]; @@ -216,6 +225,24 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { return translatedA.localeCompare(translatedB) * (orderByDirection === "asc" ? 1 : -1); }); + return data; + } else if (orderByColumn === "statusCheck") { + data.data.sort((a: Application, b: Application) => { + const valueA = a[orderByColumn]; + const valueB = b[orderByColumn]; + + if (valueA === "alert" && valueB !== "alert") { + return orderByDirection === "asc" ? 1 : -1; + } + if (valueA !== "alert" && valueB === "alert") { + return orderByDirection === "asc" ? -1 : 1; + } + + return 0; + }); + + return data; + } else { return data; } }) @@ -279,6 +306,9 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { } as ApplicationDialogModel, }); } + getSortDirection(id: string) { + return columnDefinitions.find(c => c.id === id).sort; + } - protected readonly columnDefinitions = columnDefinitions; + protected columnDefinitions = columnDefinitions; } diff --git a/src/app/applications/applications-routing.module.ts b/src/app/applications/applications-routing.module.ts index a9f0a0020..1e36fb9c8 100644 --- a/src/app/applications/applications-routing.module.ts +++ b/src/app/applications/applications-routing.module.ts @@ -16,6 +16,7 @@ import { DatatargetDetailComponent } from "./datatarget/datatarget-detail/datata import { DatatargetEditComponent } from "./datatarget/datatarget-edit/datatarget-edit.component"; import { DatatargetLogComponent } from "./datatarget/datatarget-log/datatarget-log.component"; import { DatatargetNewComponent } from "./datatarget/datatarget-new/datatarget-new.component"; +import { DatatargetTestConnectionComponent } from "./datatarget/datatarget-test-connection/datatarget-test-connection.component"; import { FiwareDetailComponent } from "./datatarget/fiware/fiware-detail/fiware-detail.component"; import { HttppushDetailComponent } from "./datatarget/httppush/httppush-detail/httppush-detail.component"; import { MqttDetailComponent } from "./datatarget/mqtt/mqtt-detail/mqtt-detail.component"; @@ -23,7 +24,6 @@ import { IoTDeviceDetailComponent } from "./iot-devices/iot-device-detail/iot-de import { IotDeviceEditComponent } from "./iot-devices/iot-device-edit/iot-device-edit.component"; import { MulticastDetailComponent } from "./multicast/multicast-detail/multicast-detail.component"; import { MulticastEditComponent } from "./multicast/multicast-edit/multicast-edit.component"; -import { DatatargetTestConnectionComponent } from "./datatarget/datatarget-test-connection/datatarget-test-connection.component"; const applicationRoutes: Routes = [ { @@ -31,6 +31,7 @@ const applicationRoutes: Routes = [ component: ApplicationsComponent, children: [ { path: "", component: ApplicationsListComponent }, + { path: "map", component: ApplicationsListComponent }, { path: "new-application", component: ApplicationEditComponent }, { path: "edit-application/:id", component: ApplicationEditComponent }, { diff --git a/src/app/applications/applications.module.ts b/src/app/applications/applications.module.ts index 1450e7ea5..4ccf03a7f 100644 --- a/src/app/applications/applications.module.ts +++ b/src/app/applications/applications.module.ts @@ -1,25 +1,34 @@ -import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { TranslateModule } from "@ngx-translate/core"; -import { ApplicationsComponent } from "./applications.component"; +import { BasicInformationBoxComponent } from "@shared/components/basic-information-box/basic-information-box.component"; import { FormModule } from "@shared/components/forms/form.module"; +import { OptionFieldComponent } from "@shared/components/option-field/option-field.component"; +import { StatusIconComponent } from "@shared/components/status-icon/status-icon.component"; +import { TablePaginatorComponent } from "@shared/components/table-pagiantor.ts/table-paginator.component"; +import { TableSortIconComponent } from "@shared/components/table-sort-icon/table-sort-icon.component"; +import { DirectivesModule } from "@shared/directives/directives.module"; +import { NGMaterialModule } from "@shared/Modules/materiale.module"; +import { PipesModule } from "@shared/pipes/pipes.module"; +import { SharedModule } from "@shared/shared.module"; +import { ApplicationChangeOrganizationDialogComponent } from "./application-change-organization-dialog/application-change-organization-dialog.component"; import { ApplicationDetailComponent } from "./application-detail/application-detail.component"; import { ApplicationEditComponent } from "./application-edit/application-edit.component"; +import { ApplicationFilterComponent } from "./applications-list/application-filter/application-filter.component"; +import { ApplicationMapComponent } from "./applications-list/application-map/application-map.component"; +import { ApplicationsListDashboardComponent } from "./applications-list/applications-list-dashboard/applications-list-dashboard.component"; import { ApplicationsListComponent } from "./applications-list/applications-list.component"; +import { ApplicationsTableComponent } from "./applications-list/applications-table/applications-table.component"; import { ApplicaitonsRoutingModule } from "./applications-routing.module"; +import { ApplicationsComponent } from "./applications.component"; +import { BulkImportComponent } from "./bulk-import/bulk-import.component"; import { DatatargetModule } from "./datatarget/datatarget.module"; import { IotDevicesModule } from "./iot-devices/iot-devices.module"; -import { SharedModule } from "@shared/shared.module"; -import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; -import { DirectivesModule } from "@shared/directives/directives.module"; -import { NGMaterialModule } from "@shared/Modules/materiale.module"; -import { BulkImportComponent } from "./bulk-import/bulk-import.component"; -import { PipesModule } from "@shared/pipes/pipes.module"; -import { ApplicationsTableComponent } from "./applications-list/applications-table/applications-table.component"; import { MulticastModule } from "./multicast/multicast.module"; -import { ReactiveFormsModule } from "@angular/forms"; -import { ApplicationChangeOrganizationDialogComponent } from "./application-change-organization-dialog/application-change-organization-dialog.component"; +import { BasicTabSwitchComponent } from "@shared/components/basic-tab-switch/basic-tab-switch.component"; @NgModule({ declarations: [ @@ -27,8 +36,8 @@ import { ApplicationChangeOrganizationDialogComponent } from "./application-chan ApplicationDetailComponent, ApplicationEditComponent, ApplicationsListComponent, - ApplicationsTableComponent, BulkImportComponent, + ApplicationsTableComponent, ApplicationChangeOrganizationDialogComponent, ], exports: [ApplicaitonsRoutingModule, ApplicationsComponent, ApplicationsTableComponent], @@ -46,6 +55,15 @@ import { ApplicationChangeOrganizationDialogComponent } from "./application-chan PipesModule, MulticastModule, ReactiveFormsModule, + OptionFieldComponent, + StatusIconComponent, + TableSortIconComponent, + TablePaginatorComponent, + BasicTabSwitchComponent, + ApplicationMapComponent, + BasicInformationBoxComponent, + ApplicationFilterComponent, + ApplicationsListDashboardComponent, ], }) export class ApplicationsModule {} diff --git a/src/app/applications/enums/status.enum.ts b/src/app/applications/enums/status.enum.ts index 9c888c2ec..82c6010e1 100644 --- a/src/app/applications/enums/status.enum.ts +++ b/src/app/applications/enums/status.enum.ts @@ -7,5 +7,6 @@ export enum ApplicationStatus { "PROTOTYPE" = "PROTOTYPE", "OTHER" = "OTHER", } +export type ApplicationStatusCheck = "stable" | "alert"; export const ApplicationStatusEntries = recordToEntries(ApplicationStatus); diff --git a/src/app/gateway/gateway-overview/gateway-tabs/gateway-map/gateway-map.component.ts b/src/app/gateway/gateway-overview/gateway-tabs/gateway-map/gateway-map.component.ts index cf6ce2f6e..fab2e0f39 100644 --- a/src/app/gateway/gateway-overview/gateway-tabs/gateway-map/gateway-map.component.ts +++ b/src/app/gateway/gateway-overview/gateway-tabs/gateway-map/gateway-map.component.ts @@ -14,9 +14,9 @@ import { MeService } from "@shared/services/me.service"; export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit { public gateways: Gateway[]; public coordinateList = []; + isLoadingResults = true; private gatewaySubscription: Subscription; private organizationChangeSubscription: Subscription; - isLoadingResults = true; constructor( private chirpstackGatewayService: ChirpstackGatewayService, @@ -41,6 +41,25 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit { } } + gatewayStatus(gateway: Gateway): boolean { + return this.chirpstackGatewayService.isGatewayActive(gateway); + } + + setCanEdit() { + this.gateways.forEach(gateway => { + gateway.canEdit = this.meService.hasAccessToTargetOrganization( + OrganizationAccessScope.GatewayWrite, + gateway.organizationId + ); + }); + } + + ngOnDestroy() { + // prevent memory leak by unsubscribing + this.gatewaySubscription?.unsubscribe(); + this.organizationChangeSubscription.unsubscribe(); + } + private getGateways(): void { this.gatewaySubscription = this.chirpstackGatewayService.getForMaps().subscribe((gateways: GatewayResponseMany) => { this.gateways = gateways.resultList; @@ -75,6 +94,7 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit { markerInfo: { name: gateway.name, active: this.gatewayStatus(gateway), + isGateway: true, id: gateway.gatewayId, internalOrganizationId: gateway.organizationId, internalOrganizationName: gateway.organizationName, @@ -83,23 +103,4 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit { ); this.coordinateList = tempcoordinateList; } - - gatewayStatus(gateway: Gateway): boolean { - return this.chirpstackGatewayService.isGatewayActive(gateway); - } - - setCanEdit() { - this.gateways.forEach(gateway => { - gateway.canEdit = this.meService.hasAccessToTargetOrganization( - OrganizationAccessScope.GatewayWrite, - gateway.organizationId - ); - }); - } - - ngOnDestroy() { - // prevent memory leak by unsubscribing - this.gatewaySubscription?.unsubscribe(); - this.organizationChangeSubscription.unsubscribe(); - } } diff --git a/src/app/navbar/global-admin/global-admin.component.html b/src/app/navbar/global-admin/global-admin.component.html index 8e55f771e..a4bdc9179 100644 --- a/src/app/navbar/global-admin/global-admin.component.html +++ b/src/app/navbar/global-admin/global-admin.component.html @@ -1,29 +1,20 @@ - - - - - - - +@if(isGlobalAdmin){ - - + +} diff --git a/src/app/navbar/global-admin/global-admin.component.scss b/src/app/navbar/global-admin/global-admin.component.scss index 516f599a5..545b7387c 100644 --- a/src/app/navbar/global-admin/global-admin.component.scss +++ b/src/app/navbar/global-admin/global-admin.component.scss @@ -1,3 +1,57 @@ +@import "../../../assets/scss/setup/variables"; + mat-expansion-panel { box-shadow: none !important; } + +:host a:focus, +button:focus, +.focus { + outline-offset: 0; + transition: all 0.2s; +} + +:host a { + color: $color-link; + border-bottom: none; + cursor: pointer; + text-decoration: none; + display: block; + padding: 10px 15px; + font-weight: 500; + border-radius: 3px; + height: 33px; + line-height: 14px; + margin: 2px 10px; + + &:hover, + &.link-hover, + &:visited:hover { + color: $white; + background-color: $color-link-active-bg; + } + + &:active, + &.link-active { + background-color: $color-link-active-bg; + color: $color-link; + } + + &:focus, + &.link-focus { + color: $white; + background-color: $color-link-active-bg; + } + + &:disabled, + &.disabled { + box-shadow: none !important; + cursor: not-allowed !important; + outline-offset: inherit !important; + + &:focus { + outline: none; + background-color: transparent; + } + } +} diff --git a/src/app/navbar/global-admin/global-admin.component.ts b/src/app/navbar/global-admin/global-admin.component.ts index dec1dbb46..90e64127f 100644 --- a/src/app/navbar/global-admin/global-admin.component.ts +++ b/src/app/navbar/global-admin/global-admin.component.ts @@ -1,9 +1,7 @@ import { Component, OnInit } from "@angular/core"; -import { PermissionType } from "@app/admin/permission/permission.model"; import { UserResponse } from "@app/admin/users/user.model"; -import { faGlobe, faUsers, faIdBadge, faSitemap, faUser } from "@fortawesome/free-solid-svg-icons"; -import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; import { MeService } from "@shared/services/me.service"; +import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; @Component({ selector: "app-global-admin", @@ -11,12 +9,6 @@ import { MeService } from "@shared/services/me.service"; styleUrls: ["./global-admin.component.scss"], }) export class GlobalAdminComponent implements OnInit { - faGlobe = faGlobe; - faUsers = faUsers; - faIdBadge = faIdBadge; - faSitemap = faSitemap; - faUser = faUser; - public user: UserResponse; public isGlobalAdmin = false; diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 227b92d9c..4b8f9d644 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -1,90 +1,139 @@