@@ -535,6 +535,215 @@ | |||
"tslib": "^2.0.0" | |||
} | |||
}, | |||
"@angular/localize": { | |||
"version": "10.2.0", | |||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-10.2.0.tgz", | |||
"integrity": "sha512-hAtmjdPs8BLQfHPtYUSFFDSn1mv/OoMxDO2iXdnvPQ5HBVIHuHb9qrpY3twX8LutJf2O175cJR7OB66DljuBmA==", | |||
"dev": true, | |||
"requires": { | |||
"@babel/core": "7.8.3", | |||
"glob": "7.1.2", | |||
"yargs": "15.3.0" | |||
}, | |||
"dependencies": { | |||
"@babel/core": { | |||
"version": "7.8.3", | |||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz", | |||
"integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==", | |||
"dev": true, | |||
"requires": { | |||
"@babel/code-frame": "^7.8.3", | |||
"@babel/generator": "^7.8.3", | |||
"@babel/helpers": "^7.8.3", | |||
"@babel/parser": "^7.8.3", | |||
"@babel/template": "^7.8.3", | |||
"@babel/traverse": "^7.8.3", | |||
"@babel/types": "^7.8.3", | |||
"convert-source-map": "^1.7.0", | |||
"debug": "^4.1.0", | |||
"gensync": "^1.0.0-beta.1", | |||
"json5": "^2.1.0", | |||
"lodash": "^4.17.13", | |||
"resolve": "^1.3.2", | |||
"semver": "^5.4.1", | |||
"source-map": "^0.5.0" | |||
} | |||
}, | |||
"ansi-regex": { | |||
"version": "5.0.0", | |||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", | |||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", | |||
"dev": true | |||
}, | |||
"ansi-styles": { | |||
"version": "4.3.0", | |||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", | |||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", | |||
"dev": true, | |||
"requires": { | |||
"color-convert": "^2.0.1" | |||
} | |||
}, | |||
"cliui": { | |||
"version": "6.0.0", | |||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", | |||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", | |||
"dev": true, | |||
"requires": { | |||
"string-width": "^4.2.0", | |||
"strip-ansi": "^6.0.0", | |||
"wrap-ansi": "^6.2.0" | |||
} | |||
}, | |||
"color-convert": { | |||
"version": "2.0.1", | |||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", | |||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", | |||
"dev": true, | |||
"requires": { | |||
"color-name": "~1.1.4" | |||
} | |||
}, | |||
"color-name": { | |||
"version": "1.1.4", | |||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", | |||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", | |||
"dev": true | |||
}, | |||
"emoji-regex": { | |||
"version": "8.0.0", | |||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | |||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", | |||
"dev": true | |||
}, | |||
"find-up": { | |||
"version": "4.1.0", | |||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", | |||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", | |||
"dev": true, | |||
"requires": { | |||
"locate-path": "^5.0.0", | |||
"path-exists": "^4.0.0" | |||
} | |||
}, | |||
"glob": { | |||
"version": "7.1.2", | |||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", | |||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", | |||
"dev": true, | |||
"requires": { | |||
"fs.realpath": "^1.0.0", | |||
"inflight": "^1.0.4", | |||
"inherits": "2", | |||
"minimatch": "^3.0.4", | |||
"once": "^1.3.0", | |||
"path-is-absolute": "^1.0.0" | |||
} | |||
}, | |||
"is-fullwidth-code-point": { | |||
"version": "3.0.0", | |||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", | |||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", | |||
"dev": true | |||
}, | |||
"locate-path": { | |||
"version": "5.0.0", | |||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", | |||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", | |||
"dev": true, | |||
"requires": { | |||
"p-locate": "^4.1.0" | |||
} | |||
}, | |||
"p-locate": { | |||
"version": "4.1.0", | |||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", | |||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", | |||
"dev": true, | |||
"requires": { | |||
"p-limit": "^2.2.0" | |||
} | |||
}, | |||
"path-exists": { | |||
"version": "4.0.0", | |||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", | |||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", | |||
"dev": true | |||
}, | |||
"semver": { | |||
"version": "5.7.1", | |||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", | |||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", | |||
"dev": true | |||
}, | |||
"source-map": { | |||
"version": "0.5.7", | |||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", | |||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", | |||
"dev": true | |||
}, | |||
"string-width": { | |||
"version": "4.2.0", | |||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", | |||
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", | |||
"dev": true, | |||
"requires": { | |||
"emoji-regex": "^8.0.0", | |||
"is-fullwidth-code-point": "^3.0.0", | |||
"strip-ansi": "^6.0.0" | |||
} | |||
}, | |||
"strip-ansi": { | |||
"version": "6.0.0", | |||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", | |||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", | |||
"dev": true, | |||
"requires": { | |||
"ansi-regex": "^5.0.0" | |||
} | |||
}, | |||
"wrap-ansi": { | |||
"version": "6.2.0", | |||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", | |||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", | |||
"dev": true, | |||
"requires": { | |||
"ansi-styles": "^4.0.0", | |||
"string-width": "^4.1.0", | |||
"strip-ansi": "^6.0.0" | |||
} | |||
}, | |||
"yargs": { | |||
"version": "15.3.0", | |||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz", | |||
"integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==", | |||
"dev": true, | |||
"requires": { | |||
"cliui": "^6.0.0", | |||
"decamelize": "^1.2.0", | |||
"find-up": "^4.1.0", | |||
"get-caller-file": "^2.0.1", | |||
"require-directory": "^2.1.1", | |||
"require-main-filename": "^2.0.0", | |||
"set-blocking": "^2.0.0", | |||
"string-width": "^4.2.0", | |||
"which-module": "^2.0.0", | |||
"y18n": "^4.0.0", | |||
"yargs-parser": "^18.1.0" | |||
} | |||
}, | |||
"yargs-parser": { | |||
"version": "18.1.3", | |||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", | |||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", | |||
"dev": true, | |||
"requires": { | |||
"camelcase": "^5.0.0", | |||
"decamelize": "^1.2.0" | |||
} | |||
} | |||
} | |||
}, | |||
"@angular/platform-browser": { | |||
"version": "10.2.0", | |||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-10.2.0.tgz", | |||
@@ -1733,6 +1942,14 @@ | |||
"schema-utils": "^2.7.0" | |||
} | |||
}, | |||
"@ng-bootstrap/ng-bootstrap": { | |||
"version": "8.0.0", | |||
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-8.0.0.tgz", | |||
"integrity": "sha512-v77Gfd8xHH+exq0WqIqVRlxbUEHdA/2+RUJenUP2IDTQN9E1rWl7O461/kosr+0XPuxPArHQJxhh/WsCYckcNg==", | |||
"requires": { | |||
"tslib": "^2.0.0" | |||
} | |||
}, | |||
"@ngtools/webpack": { | |||
"version": "10.2.0", | |||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz", |
@@ -21,6 +21,7 @@ | |||
"@angular/router": "~10.2.0", | |||
"@fortawesome/fontawesome-free": "^5.13.1", | |||
"@fortawesome/fontawesome-svg-core": "^1.2.29", | |||
"@ng-bootstrap/ng-bootstrap": "^8.0.0", | |||
"@ngx-translate/core": "^13.0.0", | |||
"@ngx-translate/http-loader": "^6.0.0", | |||
"bootstrap": "^4.5.0", | |||
@@ -36,9 +37,10 @@ | |||
"@angular-devkit/build-angular": "~0.1002.0", | |||
"@angular/cli": "~10.2.0", | |||
"@angular/compiler-cli": "~10.2.0", | |||
"@types/node": "^12.11.1", | |||
"@angular/localize": "^10.2.0", | |||
"@types/jasmine": "~3.5.0", | |||
"@types/jasminewd2": "~2.0.3", | |||
"@types/node": "^12.11.1", | |||
"codelyzer": "^6.0.0-next.1", | |||
"jasmine-core": "~3.5.0", | |||
"jasmine-spec-reporter": "~5.0.0", |
@@ -42,6 +42,7 @@ import { SecurityConfigurationComponent } from './modules/user/security-configur | |||
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"; | |||
@NgModule({ | |||
@@ -79,7 +80,8 @@ import { EnableTooltipDirective } from './directives/enable-tooltip.directive'; | |||
useFactory: httpTranslateLoader, | |||
deps: [HttpClient] | |||
} | |||
}) | |||
}), | |||
NgbPaginationModule | |||
], | |||
providers: [], | |||
bootstrap: [AppComponent] |
@@ -0,0 +1,7 @@ | |||
import { PagedResult } from './paged-result'; | |||
describe('PagedResult', () => { | |||
it('should create an instance', () => { | |||
expect(new PagedResult()).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,6 @@ | |||
import {PaginationInfo} from "./pagination-info"; | |||
export class PagedResult<T> { | |||
pagination : PaginationInfo; | |||
data : Array<T>; | |||
} |
@@ -0,0 +1,7 @@ | |||
import { PaginationInfo } from './pagination-info'; | |||
describe('PaginationInfo', () => { | |||
it('should create an instance', () => { | |||
expect(new PaginationInfo()).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,5 @@ | |||
export class PaginationInfo { | |||
totalCount : number; | |||
offset: number; | |||
limit: number; | |||
} |
@@ -21,8 +21,8 @@ | |||
<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" data-toggle="tooltip" | |||
data-placement="top" title="SEARCHHH"> | |||
<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> | |||
@@ -32,7 +32,7 @@ | |||
</form> | |||
<table class="table" appEnableTooltip> | |||
<table class="table table-striped table-bordered" appEnableTooltip> | |||
<thead class="thead-light"> | |||
<tr> | |||
<th scope="col">{{'users.list.table.head.id' | translate}}</th> | |||
@@ -49,8 +49,23 @@ | |||
</th> | |||
<th scope="col">{{'users.list.table.head.lastLogin' | translate}}</th> | |||
<th scope="col">{{'users.list.table.head.created' | translate}}</th> | |||
<th scope="col">{{'users.list.table.head.lastPwChange' | translate}}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr *ngFor="let user of items$ | async" [ngClass]="user.permanent?'table-secondary':''"> | |||
<td>{{user.id}}</td> | |||
<td>{{user.user_id}}</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> | |||
<td><span class="far" [attr.aria-valuetext]="user.locked" [ngClass]="user.locked?'fa-check-circle':'fa-circle'"></span></td> | |||
<td><span class="far" [attr.aria-valuetext]="user.passwordChangeRequired" [ngClass]="user.passwordChangeRequired?'fa-check-circle':'fa-circle'"></span></td> | |||
<td>{{user.timestampLastLogin | date:'yyyy-MM-ddTHH:mm:ss'}}</td> | |||
<td>{{user.timestampAccountCreation | date : 'yyyy-MM-ddTHH:mm:ss'}}</td> | |||
<td>{{user.timestampLastPasswordChange| date : 'yyyy-MM-ddTHH:mm:ss'}}</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<ngb-pagination [collectionSize]="total$|async" maxSize="2" rotate="true" [(page)]="page" (pageChange)="changePage($event)" aria-label="Default pagination"></ngb-pagination> |
@@ -19,8 +19,11 @@ | |||
import { Component, OnInit, Input } from '@angular/core'; | |||
import {TranslateService} from "@ngx-translate/core"; | |||
import {AppComponent} from "../../../../app.component"; | |||
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"; | |||
@Component({ | |||
selector: 'app-manage-users-list', | |||
@@ -29,18 +32,61 @@ import {UserService} from "../../../../services/user.service"; | |||
}) | |||
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) { } | |||
ngOnInit(): void { | |||
this.heads={}; | |||
this.heads = {}; | |||
// We need to wait for the translator initialization and use the init key as step in. | |||
this.translator.get('init').subscribe( () => { | |||
this.translator.get('init').subscribe(() => { | |||
// Only table headings for small columns that use icons | |||
for (let suffix of ['validated', 'locked', 'pwchange']) { | |||
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); | |||
} | |||
} |
@@ -23,6 +23,7 @@ import {HttpErrorResponse} from "@angular/common/http"; | |||
import {ErrorResult} from "../model/error-result"; | |||
import {Observable} from "rxjs"; | |||
import {Permission} from '../model/permission'; | |||
import {PagedResult} from "../model/paged-result"; | |||
@Injectable({ | |||
providedIn: 'root' | |||
@@ -257,4 +258,8 @@ 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}); | |||
} | |||
} |
@@ -55,14 +55,15 @@ | |||
"table":{ | |||
"head": { | |||
"id": "ID", | |||
"user_id": "User Identifier", | |||
"user_id": "Login Name", | |||
"email": "Email", | |||
"fullName": "Name", | |||
"validated": "User Validated", | |||
"locked": "User Locked", | |||
"pwchange": "Password Change Required", | |||
"lastLogin": "Last Login", | |||
"created": "Created" | |||
"created": "Created", | |||
"lastPwChange": "Last Password Change" | |||
} | |||
} | |||
}, |
@@ -19,7 +19,8 @@ | |||
/*************************************************************************************************** | |||
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. | |||
*/ | |||
// import '@angular/localize/init'; | |||
// We don't use it, but ng-bootstrap needs it. | |||
import '@angular/localize/init'; | |||
/** | |||
* This file includes polyfills needed by Angular and is loaded before the app. | |||
* You can add your own extra polyfills to this file. |