This is an automated email from the ASF dual-hosted git repository. hshpak pushed a commit to branch feat/DATALAB-2881/filter-function-to-Images-page in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git
commit c8f3bb284c6c76ff08b02abdcc13f321fe720c3e Author: Hennadii_Shpak <[email protected]> AuthorDate: Wed Jul 27 14:35:48 2022 +0300 initial commit --- .../resources/webapp/src/app/app.routing.module.ts | 15 ++- .../resources/webapp/src/app/core/core.module.ts | 2 +- .../app/core/services/image-page-resolve.guard.ts | 24 ++++ .../webapp/src/app/core/services/index.ts | 1 + .../app/core/services/user-images-page.service.ts | 10 +- .../page-filter/page-filter.component.html | 125 +++++++++++++++++++++ .../page-filter/page-filter.component.scss | 55 +++++++++ .../page-filter/page-filter.component.ts | 71 ++++++++++++ .../exploratory/page-filter/page-filter.config.ts | 26 +++++ .../share-image/share-image-dialog.component.ts | 2 - .../src/app/resources/images/images.component.html | 92 ++++++--------- .../src/app/resources/images/images.component.scss | 18 +++ .../src/app/resources/images/images.component.ts | 97 ++++++++-------- .../src/app/resources/images/images.config.ts | 8 +- .../src/app/resources/images/images.model.ts | 8 ++ .../src/app/resources/images/images.service.ts | 72 ++++++++++-- .../webapp/src/app/resources/resources.module.ts | 2 + 17 files changed, 505 insertions(+), 123 deletions(-) diff --git a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts index 8cc601782..9188aec15 100644 --- a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts +++ b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts @@ -31,11 +31,17 @@ import { ManagementComponent } from './administration/management'; import { ProjectComponent } from './administration/project/project.component'; import { RolesComponent } from './administration/roles/roles.component'; import { SwaggerComponent } from './swagger'; -import { AuthorizationGuard, CheckParamsGuard, CloudProviderGuard, AdminGuard, AuditGuard } from './core/services'; +import { + AuthorizationGuard, + CheckParamsGuard, + CloudProviderGuard, + AdminGuard, + AuditGuard, + ImagePageResolveGuard +} from './core/services'; import {ConfigurationComponent} from './administration/configuration/configuration.component'; import {ProjectAdminGuard} from './core/services/projectAdmin.guard'; import {ReportingComponent} from './reports/reporting/reporting.component'; -import {OdahuComponent} from './administration/odahu/odahu.component'; import {AuditComponent} from './reports/audit/audit.component'; import {ImagesComponent} from './resources/images/images.component'; @@ -62,7 +68,10 @@ const routes: Routes = [ { path: 'images', component: ImagesComponent, - canActivate: [AuthorizationGuard] + canActivate: [AuthorizationGuard], + resolve: { + projectList: ImagePageResolveGuard + } }, { path: 'billing', diff --git a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts index 33dc379fd..b1fb27d86 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts @@ -49,7 +49,7 @@ import { ErrorInterceptor } from './interceptors/error.interceptor'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { ConfigurationService } from './services/configutration.service'; -import { AuditGuard, OdahuDeploymentService, UserImagesPageService } from './services'; +import {AuditGuard, OdahuDeploymentService, UserImagesPageService} from './services'; import { ProjectAdminGuard } from './services/projectAdmin.guard'; @NgModule({ diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/image-page-resolve.guard.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/image-page-resolve.guard.ts new file mode 100644 index 000000000..77df3956f --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/core/services/image-page-resolve.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {EMPTY, Observable, of} from 'rxjs'; + +import {ImagesService} from '../../resources/images/images.service'; +import {ProjectModel} from '../../resources/images'; +import {switchMap, take} from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ImagePageResolveGuard implements Resolve<ProjectModel[]> { + constructor( + private router: Router, + private imagesService: ImagesService + ) {} + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ProjectModel[]> { + return this.imagesService.getUserImagePageInfo().pipe( + switchMap((projectList: ProjectModel[]) => of(projectList)), + take(1) + ); + } +} diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts index 68ce7e151..712ffb4ea 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts @@ -40,4 +40,5 @@ export * from './storage.service'; export * from './project.service'; export * from './odahu-deployment.service'; export * from './endpoint.service'; +export * from './image-page-resolve.guard'; export * from './user-images-page.service'; diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts index 8a4472883..9f9e0afff 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts @@ -23,11 +23,13 @@ import { catchError } from 'rxjs/operators'; import { ErrorUtils } from '../util'; import { ApplicationServiceFacade } from './applicationServiceFacade.service'; -import { ProjectModel, ShareImageAllUsersParams } from '../../resources/images'; +import {ProjectModel, ShareImageAllUsersParams} from '../../resources/images'; @Injectable() export class UserImagesPageService { - constructor(private applicationServiceFacade: ApplicationServiceFacade) { } + constructor( + private applicationServiceFacade: ApplicationServiceFacade + ) { } getUserImagePageInfo(): Observable<ProjectModel[]> { @@ -37,8 +39,8 @@ export class UserImagesPageService { ); } - shareImageAllUsers(params: ShareImageAllUsersParams): Observable<ProjectModel[]> { - return this.applicationServiceFacade.buildShareImageAllUsers(params) + shareImagesAllUser(shareParams: ShareImageAllUsersParams): Observable<ProjectModel[]> { + return this.applicationServiceFacade.buildShareImageAllUsers(shareParams) .pipe( catchError(ErrorUtils.handleServiceError) ); diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.html new file mode 100644 index 000000000..287f9db8e --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.html @@ -0,0 +1,125 @@ +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<div class="dialog-content selection"> + <div id="scrolling" class="content-box mat-reset scrolling-content"> + <form class="filter-table__wrapper" [formGroup]="filterForm"> + <div class="form-control__wrapper control-group"> + <label class="label">Custom tag</label> + <input type="text" class="form-control" [placeholder]="placeholders.imageName" formControlName="imageName" + matInput + [matAutocomplete]="auto"> + <mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"> + <mat-option *ngFor="let option of filteredOptions" [value]="option"> + {{option}} + </mat-option> + </mat-autocomplete> + </div> + + <div class="control-group"> + <label class="label">Status</label> + <div class="control selector-wrapper"> + <span class="form-field-wrapper"> + <mat-form-field> + <mat-select + formControlName="statuses" + disableOptionCentering + panelClass="create-resources-dialog scrolling" + [placeholder]="placeholders.status" + multiple + (click)="onSelectClick()" + > + <mat-option + *ngFor="let status of statuses" + [value]="status" + > + {{ status }} + </mat-option> + </mat-select> + <button class="caret"> + <i class="material-icons">keyboard_arrow_down</i> + </button> + </mat-form-field> + </span> + </div> + </div> + + <div class="control-group"> + <label class="label">Provider</label> + <div class="control selector-wrapper"> + <span class="form-field-wrapper"> + <mat-form-field> + <mat-select + formControlName="cloudProviders" + disableOptionCentering + panelClass="create-resources-dialog scrolling" + [placeholder]="placeholders.provider" + multiple + (click)="onSelectClick()" + > + <mat-option + *ngFor="let provider of cloudProviders" + [value]="provider" + > + {{ provider }} + </mat-option> + </mat-select> + <button class="caret"> + <i class="material-icons">keyboard_arrow_down</i> + </button> + </mat-form-field> + </span> + </div> + </div> + + <div class="control-group"> + <label class="label">Template name</label> + <div class="control selector-wrapper"> + <span class="form-field-wrapper"> + <mat-form-field> + <mat-select + formControlName="templateNames" + disableOptionCentering + panelClass="create-resources-dialog scrolling" + [placeholder]="placeholders.templateName" + multiple + (click)="onSelectClick()" + > + <mat-option + *ngFor="let template of templates" + [value]="template" + > + {{ template }} + </mat-option> + </mat-select> + <button class="caret"> + <i class="material-icons">keyboard_arrow_down</i> + </button> + </mat-form-field> + </span> + </div> + </div> + + <div class="text-center m-top-20"> + <button type="button" class="butt" mat-raised-button (click)="cancelFilter()" >Cancel</button> + <button type="button" class="butt butt-success" mat-raised-button (click)="confirmFilter()" >Apply</button> + </div> + </form> + </div> +</div> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.scss new file mode 100644 index 000000000..1b6956150 --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.scss @@ -0,0 +1,55 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.filter-table__wrapper { + width: 450px; + padding: 20px; + background-color: white; +} + +.filter-table__wrapper, +.form-control { + box-shadow: 0 3px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%); +} + +.form-control { + padding: 8px; + + &__wrapper { + & > .label { + width: 35%; + } + + & > .form-control { + width: 65%; + } + } +} + +.form-field-wrapper { + width: 100%; +} + +::ng-deep .cdk-overlay-pane.normalized-dropdown-position { + transform: translateX(-15px) !important; +} + +.content-box { + padding: 25px 0 35px; +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.ts new file mode 100644 index 000000000..7ed77c714 --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.component.ts @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; + +import {FilterFormPlaceholders} from './page-filter.config'; + +@Component({ + selector: 'datalab-page-filter', + templateUrl: './page-filter.component.html', + styleUrls: ['./page-filter.component.scss'] +}) +export class PageFilterComponent implements OnInit { + @Output() filterFormValue: EventEmitter<any> = new EventEmitter<any>(); + @Output() closeFilter: EventEmitter<any> = new EventEmitter<any>(); + + readonly placeholders: typeof FilterFormPlaceholders = FilterFormPlaceholders; + + filteredOptions = []; + templates = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato']; + statuses = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato']; + cloudProviders = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato']; + filterForm: FormGroup; + + constructor( + private fb: FormBuilder + ) { } + + ngOnInit(): void { + this.initFilterForm(); + } + + initFilterForm(): void { + this.filterForm = this.fb.group({ + imageName: '', + cloudProviders: [[]], + statuses: [[]], + templateNames: [[]] + }); + } + + onSelectClick(): void { + const dropdownList = document.querySelectorAll('.cdk-overlay-pane'); + dropdownList.forEach(el => el.classList.add('normalized-dropdown-position')); + } + + confirmFilter(): void { + this.filterFormValue.emit(this.filterForm.value); + } + + cancelFilter(): void { + this.closeFilter.emit(); + } +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.config.ts new file mode 100644 index 000000000..b647ea02d --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/page-filter/page-filter.config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum FilterFormPlaceholders { + imageName = 'Enter image name', + status = 'Select status', + provider = 'Select provider', + templateName = 'Select template name', + sharing = 'Select sharing status' +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/share-image/share-image-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/share-image/share-image-dialog.component.ts index 25bd72c62..9d0acbc09 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/share-image/share-image-dialog.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/share-image/share-image-dialog.component.ts @@ -20,7 +20,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ImagesService } from '../../images/images.service'; -import { UserImagesPageService } from '../../../core/services'; import { ModalData, Toaster_Message } from '../../images'; import { ToastrService } from 'ngx-toastr'; @@ -36,7 +35,6 @@ export class ShareImageDialogComponent { public dialogRef: MatDialogRef<ShareImageDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: ModalData, private imagesService: ImagesService, - private userImagesPageService: UserImagesPageService, private toastr: ToastrService, ) { } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html index f93c78d1f..ab03ccd93 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html @@ -65,7 +65,7 @@ type="button" class="butt action-button" mat-raised-button - [disabled]="isImageNotSelected" + [disabled]="!isImageSelected" (click)="onActionClick()" > Actions @@ -91,8 +91,19 @@ </button> </div> </span> + <button mat-raised-button [disabled]="!(dataSource | async)?.length" class="butt filter__btn" (click)="onFilterClick()"> + <i class="material-icons">filter_list</i> + <span class="filter__btn--name">Filter</span> + </button> + <div *ngIf="isFilterOpened | async" class="filer__wrapper"> + <datalab-page-filter + (filterFormValue)="onFilterApplyClick($event)" + (closeFilter)="onFilterCancelClick()" + > + </datalab-page-filter> + </div> <span> - <button mat-raised-button class="butt" (click)="onRefresh()"> + <button mat-raised-button class="butt" (click)="onRefreshClick()"> <i class="material-icons highlight">autorenew</i> Refresh </button> @@ -102,20 +113,17 @@ <mat-divider></mat-divider> - <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 image-table data-grid"> + <table mat-table [dataSource]="dataSource | async" class="mat-elevation-z8 image-table data-grid"> <ng-container matColumnDef="checkbox"> <th mat-header-cell *matHeaderCellDef class="image-checkbox--wrapper"> <div class="header-cell--wrapper"> - <span *ngIf="dataSource.length" > + <span *ngIf="(dataSource | async)?.length" > <datalab-checkbox (click)="allCheckboxToggle()" [checked]="checkboxSelected" class="image-checkbox" ></datalab-checkbox> </span> - <i class="material-icons"> - <span>more_vert</span> - </i> </div> </th> <td mat-cell *matCellDef="let element" class="image-checkbox--wrapper"> @@ -131,21 +139,26 @@ <th mat-header-cell *matHeaderCellDef> <div class="header-cell--wrapper"> <span>{{tableHeaderCellTitles.imageName}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> </div> </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> + <ng-container matColumnDef="imageStatus"> + <th mat-header-cell *matHeaderCellDef> + <div class="header-cell--wrapper"> + <span>{{tableHeaderCellTitles.imageStatus}}</span> + </div> + </th> + <td mat-cell *matCellDef="let element" ngClass="{{ element.status.toLowerCase() || ''}}"> + {{element.status | capitalizeFirstLetter}} + </td> + </ng-container> + <ng-container matColumnDef="creationDate"> <th mat-header-cell *matHeaderCellDef> <div class="header-cell--wrapper"> <span>{{tableHeaderCellTitles.creationDate}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> </div> </th> <td mat-cell *matCellDef="let element"> @@ -153,39 +166,28 @@ </td> </ng-container> - <ng-container matColumnDef="provider"> + <ng-container matColumnDef="endpoint"> <th mat-header-cell *matHeaderCellDef> <div class="header-cell--wrapper"> - <span>{{tableHeaderCellTitles.provider}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> + <span>{{tableHeaderCellTitles.endpoint}}</span> </div> </th> - <td mat-cell *matCellDef="let element"> {{element.cloudProvider}} </td> + <td mat-cell *matCellDef="let element"> {{element.endpoint}} </td> </ng-container> - <ng-container matColumnDef="imageStatus"> + <ng-container matColumnDef="templateName"> <th mat-header-cell *matHeaderCellDef> <div class="header-cell--wrapper"> - <span>{{tableHeaderCellTitles.imageStatus}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> + <span>{{tableHeaderCellTitles.templateName}}</span> </div> </th> - <td mat-cell *matCellDef="let element" ngClass="{{ element.status.toLowerCase() || ''}}"> - {{element.status | capitalizeFirstLetter}} - </td> + <td mat-cell *matCellDef="let element"> {{element.templateName}} </td> </ng-container> <ng-container matColumnDef="sharedStatus"> <th mat-header-cell *matHeaderCellDef> <div class="header-cell--wrapper"> <span>{{tableHeaderCellTitles.sharedStatus}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> </div> </th> <td mat-cell *matCellDef="let element"> @@ -198,36 +200,12 @@ </td> </ng-container> - <ng-container matColumnDef="templateName"> - <th mat-header-cell *matHeaderCellDef> - <div class="header-cell--wrapper"> - <span>{{tableHeaderCellTitles.templateName}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> - </div> - </th> - <td mat-cell *matCellDef="let element"> {{element.templateName}} </td> - </ng-container> - - <ng-container matColumnDef="instanceName"> - <th mat-header-cell *matHeaderCellDef> - <div class="header-cell--wrapper"> - <span>{{tableHeaderCellTitles.instanceName}}</span> - <i class="material-icons"> - <span>more_vert</span> - </i> - </div> - </th> - <td mat-cell *matCellDef="let element"> {{element.instanceName}} </td> - </ng-container> - <ng-container matColumnDef="actions"> <th mat-header-cell *matHeaderCellDef> {{tableHeaderCellTitles.actions}} </th> <td mat-cell *matCellDef="let element" class="settings actions-col"> <div class="button--wrapper"> - <span class="currency_details" (click)="onImageInfo(element)"> + <span class="currency_details" (click)="onImageInfoClick(element)"> <i class="material-icons">help_outline</i> </span> <span #settings class="actions" (click)="actions.toggle($event, settings)"></span> @@ -241,7 +219,7 @@ > <button class="action-button__share" - (click)="onShare(element)" + (click)="onShareClick(element)" [disabled]="userName !== element.user || element.status !== imageStatus.active" > <i class="material-icons">screen_share</i> @@ -270,6 +248,6 @@ <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> - <tr [hidden]="dataSource.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr> + <tr [hidden]="(dataSource | async)?.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr> </table> </section> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss index c032060ce..680be511b 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss @@ -60,6 +60,7 @@ .button--wrapper, .header-cell--wrapper { + position: relative; display: flex; justify-content: space-between; } @@ -116,3 +117,20 @@ .list-unstyled > li { margin: 0 !important; } + +.filer__wrapper { + position: absolute; + top: 20px; + left: 0px; + z-index: 10; +} + +.filter__btn { + margin-right: 10px; + vertical-align: middle; + width: 110px; + + &--name { + line-height: 0; + } +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts index cb581f502..bf7d6d34c 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts @@ -17,12 +17,15 @@ * under the License. */ -import { Component, OnInit } from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import {map, tap} from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; import { GeneralEnvironmentStatus } from '../../administration/management/management.model'; -import { HealthStatusService, UserImagesPageService } from '../../core/services'; +import { HealthStatusService } from '../../core/services'; import { ImageModel, ProjectModel } from './images.model'; import { TooltipStatuses, @@ -33,11 +36,11 @@ import { Placeholders, Shared_Status, } from './images.config'; -import { MatDialog } from '@angular/material/dialog'; import { ShareImageDialogComponent } from '../exploratory/share-image/share-image-dialog.component'; import { ImagesService } from './images.service'; import { ProgressBarService } from '../../core/services/progress-bar.service'; import { ImageDetailDialogComponent } from '../exploratory/image-detail-dialog/image-detail-dialog.component'; +import {ActivatedRoute} from '@angular/router'; @Component({ selector: 'datalab-images', @@ -49,7 +52,7 @@ import { ImageDetailDialogComponent } from '../exploratory/image-detail-dialog/i ] }) -export class ImagesComponent implements OnInit { +export class ImagesComponent implements OnInit, OnDestroy { readonly tableHeaderCellTitles: typeof Image_Table_Column_Headers = Image_Table_Column_Headers; readonly displayedColumns: typeof Image_Table_Titles = Image_Table_Titles; readonly placeholder: typeof Placeholders = Placeholders; @@ -59,27 +62,34 @@ export class ImagesComponent implements OnInit { isActionsOpen: boolean = false; healthStatus: GeneralEnvironmentStatus; - dataSource: ImageModel[] = []; + dataSource: Observable<ImageModel[]>; checkboxSelected: boolean = false; projectList: string[] = []; activeProjectName: string = ''; userName!: string; - - private cashedImageListData: ProjectModel[] = []; + isProjectsMoreThanOne: boolean; + isFilterOpened: Observable<boolean>; constructor( private healthStatusService: HealthStatusService, public toastr: ToastrService, - private userImagesPageService: UserImagesPageService, private dialog: MatDialog, private imagesService: ImagesService, - private progressBarService: ProgressBarService + private progressBarService: ProgressBarService, + private route: ActivatedRoute, ) { } ngOnInit(): void { this.getEnvironmentHealthStatus(); this.getUserImagePageInfo(); + this.getUserName(); + this.initImageTable(); + this.initFilterBtn(); + } + + ngOnDestroy(): void { + this.imagesService.closeFilter(); } onCheckboxClick(element: ImageModel): void { @@ -88,12 +98,7 @@ export class ImagesComponent implements OnInit { allCheckboxToggle(): void { this.checkboxSelected = !this.checkboxSelected; - - if (this.checkboxSelected) { - this.dataSource.forEach(image => image.isSelected = true); - } else { - this.dataSource.forEach(image => image.isSelected = false); - } + this.imagesService.changeCheckboxValue(this.checkboxSelected); } onActionClick(): void { @@ -102,20 +107,18 @@ export class ImagesComponent implements OnInit { onSelectClick(projectName: string = ''): void { if (!projectName) { - this.dataSource = this.getImageList(); return; } - const currentProject = this.cashedImageListData.find(({project}) => project === projectName); - this.dataSource = [...currentProject.images]; - this.activeProjectName = currentProject.project; + this.imagesService.getActiveProject(projectName); + this.activeProjectName = projectName; } - onRefresh(): void { + onRefreshClick(): void { this.getUserImagePageInfo(); this.activeProjectName = ''; } - onImageInfo(image: ImageModel): void { + onImageInfoClick(image: ImageModel): void { this.dialog.open(ImageDetailDialogComponent, { data: { image @@ -124,23 +127,27 @@ export class ImagesComponent implements OnInit { }); } - onShare(image: ImageModel): void { + onShareClick(image: ImageModel): void { this.dialog.open(ShareImageDialogComponent, { data: { image }, panelClass: 'modal-sm' }).afterClosed() - .subscribe(() => { - if (this.imagesService.projectList) { - this.initImageTable(this.imagesService.projectList); - } - this.progressBarService.stopProgressBar(); - }); + .subscribe(() => this.progressBarService.stopProgressBar()); } - private getImageList(): ImageModel[] { - return this.cashedImageListData.reduce((acc, {images}) => [...acc, ...images], []); + onFilterClick(): void { + this.imagesService.openFilter(); + } + + onFilterApplyClick(filterFormValue): void { + console.log(filterFormValue); + this.imagesService.closeFilter(); + } + + onFilterCancelClick(): void { + this.imagesService.closeFilter(); } private getEnvironmentHealthStatus(): void { @@ -153,36 +160,36 @@ export class ImagesComponent implements OnInit { } private getUserImagePageInfo(): void { - this.userImagesPageService.getUserImagePageInfo().subscribe(imageListData => this.initImageTable(imageListData)); + this.route.data.pipe( + map(data => data['projectList']), + tap(projectList => this.getProjectList(projectList)) + ).subscribe(); } - private initImageTable(imagePageList: ProjectModel[]): void { - this.cashedImageListData = imagePageList; - this.getProjectList(imagePageList); - this.dataSource = this.getImageList(); - - if (imagePageList.length === 1) { - this.activeProjectName = imagePageList[0].project; - } + private initImageTable(): void { + this.dataSource = this.imagesService.$imageList; } private getProjectList(imagePageList: ProjectModel[]): void { if (!imagePageList) { return; } - this.projectList = []; - imagePageList.forEach(({project}) => this.projectList.push(project)); + this.projectList = this.imagesService.getProjectNameList(imagePageList); + this.isProjectsMoreThanOne = this.projectList.length > 1; + if (this.isProjectsMoreThanOne) { + this.activeProjectName = this.projectList[0]; + } } private getUserName(): void { this.userName = localStorage.getItem(Localstorage_Key.userName); } - get isProjectsMoreThanOne(): boolean { - return this.projectList.length > 1; + private initFilterBtn(): void { + this.isFilterOpened = this.imagesService.$isFilterOpened; } - get isImageNotSelected(): boolean { - return !this.dataSource.some(image => image.isSelected); + get isImageSelected(): boolean { + return this.imagesService.isImageSelected(); } } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts index 9628420e7..b9c958aa0 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts @@ -26,6 +26,7 @@ export enum Image_Table_Column_Headers { templateName = 'Template name', instanceName = 'Instance name', actions = 'Actions', + endpoint = 'Endpoint', } export enum Shared_Status { @@ -36,12 +37,11 @@ export enum Shared_Status { export const Image_Table_Titles = <const>[ 'checkbox', 'imageName', - 'creationDate', - 'provider', 'imageStatus', - 'sharedStatus', + 'creationDate', + 'endpoint', 'templateName', - 'instanceName', + 'sharedStatus', 'actions' ]; diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts index ab63a8842..7bd30a1df 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts @@ -48,3 +48,11 @@ export interface ClusterConfig { Properties: Record<string, any>; Configurations: any[]; } + +export interface FilterDropdownValue { + imageName: string[]; + imageStatus: string[]; + provider: string[]; + templateName: string[]; + sharingStatus: string[]; +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts index 3ac70b47c..56a48bb62 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts @@ -1,28 +1,86 @@ import { Injectable } from '@angular/core'; +import {tap} from 'rxjs/operators'; +import {BehaviorSubject, Observable} from 'rxjs'; + import { ImageModel, ProjectModel, ShareImageAllUsersParams } from './images.model'; -import { Observable } from 'rxjs'; -import { UserImagesPageService } from '../../core/services'; -import { tap } from 'rxjs/operators'; +import {ApplicationServiceFacade, UserImagesPageService} from '../../core/services'; @Injectable({ providedIn: 'root' }) export class ImagesService { + private $$projectList: BehaviorSubject<ProjectModel[]> = new BehaviorSubject<ProjectModel[]>([]); + private $$imageList: BehaviorSubject<ImageModel[]> = new BehaviorSubject<ImageModel[]>([]); + private $$isFilterOpened: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); - projectList: ProjectModel[]; + $imageList = this.$$imageList.asObservable(); + $isFilterOpened = this.$$isFilterOpened.asObservable(); constructor( + private applicationServiceFacade: ApplicationServiceFacade, private userImagesPageService: UserImagesPageService ) { } + getUserImagePageInfo(): Observable<ProjectModel[]> { + return this.userImagesPageService.getUserImagePageInfo() + .pipe( + tap(value => this.getImagePageData(value)) + ); + } + shareImageAllUsers(image: ImageModel): Observable<ProjectModel[]> { const shareParams: ShareImageAllUsersParams = { imageName: image.name, projectName: image.project, endpoint: image.endpoint }; - return this.userImagesPageService.shareImageAllUsers(shareParams).pipe( - tap((response: ProjectModel[]) => this.projectList = response) - ); + + return this.userImagesPageService.shareImagesAllUser(shareParams) + .pipe( + tap(value => this.getImagePageData(value)) + ); + } + + getActiveProject(projectName: string): void { + const currentProject = this.$$projectList.value.find(({project}) => project === projectName); + this.updateImageList(currentProject.images); + } + + getProjectNameList(imageList: ProjectModel[]): string[] { + return imageList.map(({project}) => project); + } + + updateImageList(imageList: ImageModel[]): void { + this.$$imageList.next(imageList); + } + + updateProjectList(projectList: ProjectModel[]): void { + this.$$projectList.next(projectList); + } + + changeCheckboxValue(value: boolean): void { + const updatedImageList = this.$$imageList.value.map(image => { + image.isSelected = value; + return image; + }); + this.$$imageList.next(updatedImageList); + } + + isImageSelected(): boolean { + return this.$$imageList.value.some(image => image.isSelected); + } + + openFilter(): void { + this.$$isFilterOpened.next(true); + } + + closeFilter(): void { + this.$$isFilterOpened.next(false); + } + + private getImagePageData(imagePageData: ProjectModel[]): void { + const imageList = imagePageData.reduce((acc: ImageModel[], {images}) => [...acc, ...images], []); + this.updateProjectList(imagePageData); + this.updateImageList(imageList); } } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts index 931e037d6..488762efa 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts @@ -39,6 +39,7 @@ import { LocalDatePipeModule } from '../core/pipes/local-date-pipe'; import { ShareImageDialogModule } from './exploratory/share-image/share-image-dialog.module'; import { ImageDetailDialogModule } from './exploratory/image-detail-dialog/image-detail-dialog.module'; import {LibraryInfoModalModule} from './exploratory/library-info-modal/library-info-modal.module'; +import { PageFilterComponent } from './exploratory/page-filter/page-filter.component'; @NgModule({ imports: [ @@ -64,6 +65,7 @@ import {LibraryInfoModalModule} from './exploratory/library-info-modal/library-i ManageUngitComponent, ConfirmDeleteAccountDialogComponent, ImagesComponent, + PageFilterComponent, ], entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialogComponent], --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
