@@ -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] |
@@ -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>> | |||
} |
@@ -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> |
@@ -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(); | |||
}); | |||
}); |
@@ -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(); | |||
}); | |||
}); |
@@ -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); | |||
} | |||
} |
@@ -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> |
@@ -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>(); | |||
service : EntityService<UserInfo>; | |||
constructor(private translator: TranslateService, private userService : UserService) { } | |||
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); | |||
} | |||
} |
@@ -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}); | |||
} |
@@ -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}); | |||
} | |||
} |
@@ -72,6 +72,7 @@ | |||
} | |||
}, | |||
"search": { | |||
"button": "Search" | |||
"button": "Search", | |||
"label": "Enter your search term" | |||
} | |||
} |
@@ -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 |