Adding component for entity display

This commit is contained in:
Martin Stockhammer 2020-11-08 11:39:00 +01:00
parent 1e11eea36f
commit 0e2187bf0e
12 changed files with 325 additions and 103 deletions

View File

@ -41,8 +41,8 @@ import { ManageRolesComponent } from './modules/user/manage-roles/manage-roles.c
import { SecurityConfigurationComponent } from './modules/user/security-configuration/security-configuration.component';
import { ManageUsersListComponent } from './modules/user/users/manage-users-list/manage-users-list.component';
import { ManageUsersAddComponent } from './modules/user/users/manage-users-add/manage-users-add.component';
import { EnableTooltipDirective } from './directives/enable-tooltip.directive';
import {NgbPagination, NgbPaginationModule} from "@ng-bootstrap/ng-bootstrap";
import { NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
import { PaginatedEntitiesComponent } from './modules/general/paginated-entities/paginated-entities.component';
@NgModule({
@ -64,9 +64,7 @@ import {NgbPagination, NgbPaginationModule} from "@ng-bootstrap/ng-bootstrap";
SecurityConfigurationComponent,
ManageUsersListComponent,
ManageUsersAddComponent,
EnableTooltipDirective,
PaginatedEntitiesComponent,
],
imports: [
BrowserModule,
@ -81,7 +79,8 @@ import {NgbPagination, NgbPaginationModule} from "@ng-bootstrap/ng-bootstrap";
deps: [HttpClient]
}
}),
NgbPaginationModule
NgbPaginationModule,
NgbTooltipModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -16,18 +16,13 @@
* under the License.
*/
import {AfterViewChecked, AfterViewInit, Directive, ElementRef, OnInit} from '@angular/core';
import {PagedResult} from "./paged-result";
import {Observable} from "rxjs";
declare var jQuery:any;
@Directive({
selector: '[appEnableTooltip]'
})
export class EnableTooltipDirective implements AfterViewInit {
constructor() { }
ngAfterViewInit(): void {
jQuery('[data-toggle="tooltip"]').tooltip({container: 'body', html: true});
}
/**
* This is a functional interface that is used to retrieve entity data.
* @typeparam T The type of the entity that is returned from the service
*/
export interface EntityService<T> {
(searchTerm:string,offset:number,limit:number,orderBy:string,order:string):Observable<PagedResult<T>>
}

View File

@ -0,0 +1,38 @@
<!--
~ 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.
-->
<form class="mt-3 mb-3">
<div class="form-row align-items-center">
<div class="col-lg-4 col-md-2 col-sm-1">
<label class="sr-only" for="searchQuery">{{'search.label' |translate}}</label>
<input type="text" class="form-control" id="searchQuery" placeholder="Search" #searchTerm
(keyup)="search(searchTerm.value)">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">{{'search.button'|translate}}</button>
</div>
</div>
</form>
<ng-content></ng-content>
<ngb-pagination [collectionSize]="total$|async" [pageSize]="pageSize" [maxSize]="pagination.maxSize" [rotate]="pagination.rotate"
[boundaryLinks]="pagination.boundaryLinks" [ellipses]="pagination.ellipses"
[(page)]="page" (pageChange)="changePage($event)" aria-label="Pagination"></ngb-pagination>

View File

@ -1,4 +1,4 @@
/*
/*!
* 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
@ -16,11 +16,3 @@
* under the License.
*/
import { EnableTooltipDirective } from './enable-tooltip.directive';
describe('EnableTooltipDirective', () => {
it('should create an instance', () => {
const directive = new EnableTooltipDirective();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
/*
* 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { PaginatedEntitiesComponent } from './paginated-entities.component';
describe('PaginatedEntitiesComponent', () => {
let component: PaginatedEntitiesComponent;
let fixture: ComponentFixture<PaginatedEntitiesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PaginatedEntitiesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PaginatedEntitiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,138 @@
/*
* 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, OnInit, Input, Output, EventEmitter} from '@angular/core';
import {merge, Observable, Subject} from "rxjs";
import {UserInfo} from "../../../model/user-info";
import {TranslateService} from "@ngx-translate/core";
import {debounceTime, distinctUntilChanged, map, mergeMap, pluck, share, startWith} from "rxjs/operators";
import {EntityService} from "../../../model/entity-service";
/**
* This component has a search field and pagination section. Entering data in the search field, or
* a button click on the pagination triggers a call to a service method, that returns the entity data.
* The service must implement the {@link EntityService} interface.
*
* The content is displayed between the search input and the pagination section. To use the data, you should
* add an identifier and refer to the item$ variable:
* ```
* <app-paginated-entities #parent>
* <table>
* <tr ngFor="let entity in parent.item$ | async" >
* <td>{{entity.id}}</td>
* </tr>
* </table>
* </app-paginated-entities>
* ```
*
* @typeparam T The type of the retrieved entity elements.
*/
@Component({
selector: 'app-paginated-entities',
templateUrl: './paginated-entities.component.html',
styleUrls: ['./paginated-entities.component.scss']
})
export class PaginatedEntitiesComponent<T> implements OnInit {
/**
* This must be set, if you use the component. This service retrieves the entity data.
*/
@Input() service : EntityService<T>;
/**
* The number of elements per page retrieved
*/
@Input() pageSize = 10;
/**
* Pagination controls
*/
@Input() pagination = {
maxSize:5,
rotate:true,
boundaryLinks:true,
ellipses:false
}
/**
* The current page that is selected
*/
page = 1;
/**
* The current search term entered in the search field
*/
searchTerm: string;
/**
* Event thrown, if the page value changes
*/
@Output() pageEvent : EventEmitter<number> = new EventEmitter<number>();
/**
* Event thrown, if the search term changes
*/
@Output() searchTermEvent: EventEmitter<string> = new EventEmitter<string>();
/**
* The total number of elements available for the given search term
*/
total$: Observable<number>;
/**
* The entity items retrieved from the service
*/
items$: Observable<T[]>;
private pageStream: Subject<number> = new Subject<number>();
private searchTermStream: Subject<string> = new Subject<string>();
constructor() { }
ngOnInit(): void {
// We combine the sources for the page and the search input field to a observable 'source'
const pageSource = this.pageStream.pipe(map(pageNumber => {
return {search: this.searchTerm, page: pageNumber}
}));
const searchSource = this.searchTermStream.pipe(
debounceTime(1000),
distinctUntilChanged(),
map(searchTerm => {
this.searchTerm = searchTerm;
return {search: searchTerm, page: 1}
}));
const source = merge(pageSource, searchSource).pipe(
startWith({search: this.searchTerm, page: this.page}),
mergeMap((params: { search: string, page: number }) => {
return this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, "", "asc");
}),share());
this.total$ = source.pipe(pluck('pagination','totalCount'));
this.items$ = source.pipe(pluck('data'));
}
search(terms: string) {
// console.log("Keystroke " + terms);
this.searchTermEvent.emit(terms);
this.searchTermStream.next(terms)
}
changePage(pageNumber : number) {
// console.log("Page change " +pageNumber);
this.pageEvent.emit(pageNumber);
this.pageStream.next(pageNumber);
}
}

View File

@ -17,35 +17,21 @@
~ under the License.
-->
<form class="mt-3 mb-3">
<div class="form-row align-items-center">
<div class="col-lg-4 col-md-2 col-sm-1">
<label class="sr-only" for="searchQuery">{{'users.list.search' |translate}}</label>
<input type="text" class="form-control" id="searchQuery" placeholder="Search" #searchTerm
(keyup)="search(searchTerm.value)">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">{{'search.button'|translate}}</button>
</div>
</div>
<app-paginated-entities [service]="service" pageSize="5"We #parent>
</form>
<table class="table table-striped table-bordered" appEnableTooltip>
<table class="table table-striped table-bordered">
<thead class="thead-light">
<tr>
<th scope="col">{{'users.list.table.head.id' | translate}}</th>
<th scope="col">{{'users.list.table.head.user_id' | translate}}</th>
<th scope="col">{{'users.list.table.head.fullName' | translate}}</th>
<th scope="col">{{'users.list.table.head.email' | translate}}</th>
<th scope="col"><span class="fas fa-check" data-toggle="tooltip" data-placement="top"
[attr.data-original-title]="heads.validated" [attr.aria-label]="heads.validated"></span>
<th scope="col"><span class="fas fa-check" placement="top"
[ngbTooltip]="heads.validated" [attr.aria-label]="heads.validated"></span>
</th>
<th scope="col"><span class="fas fa-lock" data-toggle="tooltip" data-placement="top"
[attr.data-original-title]="heads.locked" [attr.aria-label]="heads.locked"></span></th>
<th scope="col"><span class="fa fa-chevron-circle-right" data-toggle="tooltip" data-placement="top"
[attr.data-original-title]="heads.pwchange" [attr.aria-label]="heads.pwchange"></span>
<th scope="col"><span class="fas fa-lock" placement="top"
[ngbTooltip]="heads.locked" [attr.aria-label]="heads.locked"></span></th>
<th scope="col"><span class="fa fa-chevron-circle-right" placement="top"
[ngbTooltip]="heads.pwchange" [attr.aria-label]="heads.pwchange"></span>
</th>
<th scope="col">{{'users.list.table.head.lastLogin' | translate}}</th>
<th scope="col">{{'users.list.table.head.created' | translate}}</th>
@ -53,9 +39,8 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let user of items$ | async" [ngClass]="user.permanent?'table-secondary':''">
<td>{{user.id}}</td>
<td>{{user.user_id}}</td>
<tr *ngFor="let user of parent.items$ | async" [ngClass]="(user.permanent||user.readOnly)?'table-secondary':''" >
<td><span data-toggle="tooltip" placement="left" ngbTooltip="{{user.id}}">{{user.user_id}}</span></td>
<td>{{user.fullName}}</td>
<td>{{user.email}}</td>
<td><span class="far" [attr.aria-valuetext]="user.validated" [ngClass]="user.validated?'fa-check-circle':'fa-circle'"></span></td>
@ -68,4 +53,4 @@
</tbody>
</table>
<ngb-pagination [collectionSize]="total$|async" maxSize="2" rotate="true" [(page)]="page" (pageChange)="changePage($event)" aria-label="Default pagination"></ngb-pagination>
</app-paginated-entities>

View File

@ -17,12 +17,13 @@
* under the License.
*/
import { Component, OnInit, Input } from '@angular/core';
import {Component, OnInit, Input, OnDestroy} from '@angular/core';
import {TranslateService} from "@ngx-translate/core";
import {UserService} from "../../../../services/user.service";
import {Observable, Subject, merge} from 'rxjs';
import { map, pluck, debounceTime, distinctUntilChanged, startWith, mergeMap} from "rxjs/operators";
import {UserInfo} from "../../../../model/user-info";
import {EntityService} from "../../../../model/entity-service";
import {Observable, of} from "rxjs";
import {PagedResult} from "../../../../model/paged-result";
@Component({
@ -31,16 +32,17 @@ import {UserInfo} from "../../../../model/user-info";
styleUrls: ['./manage-users-list.component.scss']
})
export class ManageUsersListComponent implements OnInit {
@Input() heads: any;
page = 1;
pageSize = 10;
total$: Observable<number>;
items$: Observable<UserInfo[]>;
searchTerm: string;
private pageStream: Subject<number> = new Subject<number>();
private searchTermStream: Subject<string> = new Subject<string>();
constructor(private translator: TranslateService, private userService : UserService) { }
@Input() heads: any;
service : EntityService<UserInfo>;
constructor(private translator: TranslateService, private userService : UserService) {
this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string, order: string) : Observable<PagedResult<UserInfo>> {
return userService.query(searchTerm, offset, limit, orderBy, order);
}
}
ngOnInit(): void {
this.heads = {};
@ -51,42 +53,17 @@ export class ManageUsersListComponent implements OnInit {
this.heads[suffix] = this.translator.instant('users.list.table.head.' + suffix);
}
});
const pageSource = this.pageStream.pipe(map(pageNumber => {
return {search: this.searchTerm, page: pageNumber}
}));
const searchSource = this.searchTermStream.pipe(
debounceTime(1000),
distinctUntilChanged(),
map(searchTerm => {
this.searchTerm = searchTerm;
console.log("Search term " + searchTerm);
return {search: searchTerm, page: 1}
}));
const source = merge(pageSource, searchSource).pipe(
startWith({search: this.searchTerm, page: this.page}),
mergeMap((params: { search: string, page: number }) => {
console.log("Executing user list " + params.search);
return this.userService.getUserList(params.search, params.page*this.pageSize, this.pageSize)
}));
this.total$ = source.pipe(pluck('pagination.total'));
this.items$ = source.pipe(pluck('data'));
// const pageSource = map(pageNumber => {
// this.page = pageNumber
// return {search: this.searchTerm, page: pageNumber}
// })
}
search(terms: string) {
console.log("Keystroke " + terms);
this.searchTermStream.next(terms)
}
changePage(pageNumber : number) {
console.log("Page change " +typeof(pageNumber) +":" + JSON.stringify(pageNumber));
this.pageStream.next(pageNumber);
}
}

View File

@ -54,7 +54,11 @@ export class ArchivaRequestService {
headers = {};
}
if (type == "get") {
return this.http.get<R>(url, {"headers": headers});
let params = {}
if (input!=null) {
params = input;
}
return this.http.get<R>(url, {"headers": headers,"params":params});
} else if (type == "post") {
return this.http.post<R>(url, input, {"headers": headers});
}

View File

@ -24,6 +24,7 @@ import {ErrorResult} from "../model/error-result";
import {Observable} from "rxjs";
import {Permission} from '../model/permission';
import {PagedResult} from "../model/paged-result";
import {EntityService} from "../model/entity-service";
@Injectable({
providedIn: 'root'
@ -202,7 +203,6 @@ export class UserService implements OnInit, OnDestroy {
}
}
}
console.log("New permissions: " + JSON.stringify(this.uiPermissions));
}
private deepCopy(src: Object, dst: Object) {
@ -258,8 +258,12 @@ export class UserService implements OnInit, OnDestroy {
this.authenticated = false;
}
public getUserList(searchTerm : string, offset : number = 0, limit : number = 10) : Observable<PagedResult<UserInfo>> {
return this.rest.executeRestCall<PagedResult<UserInfo>>("get", "redback", "users", {'offset':offset,'limit':limit});
public query(searchTerm : string, offset : number = 0, limit : number = 10, orderBy : string = 'user_id', order: string = 'asc') : Observable<PagedResult<UserInfo>> {
console.log("getUserList " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
if (searchTerm==null) {
searchTerm=""
}
return this.rest.executeRestCall<PagedResult<UserInfo>>("get", "redback", "users", {'q':searchTerm, 'offset':offset,'limit':limit});
}
}

View File

@ -72,6 +72,7 @@
}
},
"search": {
"button": "Search"
"button": "Search",
"label": "Enter your search term"
}
}

View File

@ -0,0 +1,46 @@
#!/bin/bash
# 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.
#
# Just a simple script to generate users on the local archiva instance for interactive UI testing
BASE_URL="http://localhost:8080/archiva"
USER_NAME="admin"
PASSWD="admin456"
USERS=25
#Authenticate
TOKEN=$(curl -s -X POST "${BASE_URL}/api/v2/redback/auth/authenticate" -H "accept: application/json" -H "Content-Type: application/json" \
-d "{\"grant_type\":\"authorization_code\",\"client_id\":\"test-bash\",\"client_secret\":\"string\",\"code\":\"string\",\"scope\":\"string\",\"state\":\"string\",\"user_id\":\"${USER_NAME}\",\
\"password\":\"${PASSWD}\",\"redirect_uri\":\"string\"}"|sed -n -e '/access_token/s/.*"access_token":"\([^"]\+\)".*/\1/gp')
if [ "${TOKEN}" == "" ]; then
echo "Authentication failed!"
exit 1
fi
NUM=$USERS
while [ $NUM -ge 0 ]; do
SUFFIX=$(printf "%03d" $NUM)
echo "User: test${SUFFIX}"
curl -s -w ' - %{http_code}' -X POST "${BASE_URL}/api/v2/redback/users" -H "accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"user_id\":\"test${SUFFIX}\",\"fullName\":\"Test User ${SUFFIX}\",\"email\":\"test${SUFFIX}@test.org\",\"validated\":true,\"locked\":false,\"passwordChangeRequired\":false,\"password\":\"test123\"}"
NUM=$((NUM-1))
echo " "
done