"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",
"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",
"@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",
"@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",
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({
useFactory: httpTranslateLoader,
deps: [HttpClient]
}
- })
+ }),
+ NgbPaginationModule
],
providers: [],
bootstrap: [AppComponent]
--- /dev/null
+import { PagedResult } from './paged-result';
+
+describe('PagedResult', () => {
+ it('should create an instance', () => {
+ expect(new PagedResult()).toBeTruthy();
+ });
+});
--- /dev/null
+import {PaginationInfo} from "./pagination-info";
+
+export class PagedResult<T> {
+ pagination : PaginationInfo;
+ data : Array<T>;
+}
--- /dev/null
+import { PaginationInfo } from './pagination-info';
+
+describe('PaginationInfo', () => {
+ it('should create an instance', () => {
+ expect(new PaginationInfo()).toBeTruthy();
+ });
+});
--- /dev/null
+export class PaginationInfo {
+ totalCount : number;
+ offset: number;
+ limit: number;
+}
<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>
</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>
</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>
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',
})
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);
}
}
import {ErrorResult} from "../model/error-result";
import {Observable} from "rxjs";
import {Permission} from '../model/permission';
+import {PagedResult} from "../model/paged-result";
@Injectable({
providedIn: 'root'
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});
+ }
+
}
"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"
}
}
},
/***************************************************************************************************
* 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.