Merge branch 'spencer/metadata-history' into 'master'

Add metadata history datatable.

See merge request grassrootseconomics/cic-staff-client!44
This commit is contained in:
Blair Vanderlugt 2021-08-02 17:47:30 +00:00
commit a4813152ed
16 changed files with 27137 additions and 2213 deletions

29004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,6 +74,7 @@
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-junit-reporter": "^2.0.1", "karma-junit-reporter": "^2.0.1",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"openpgp": "^4.10.10",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"protractor": "~7.0.0", "protractor": "~7.0.0",

View File

@ -10,3 +10,4 @@ export * from '@app/_helpers/read-csv';
export * from '@app/_helpers/schema-validation'; export * from '@app/_helpers/schema-validation';
export * from '@app/_helpers/sync'; export * from '@app/_helpers/sync';
export * from '@app/_helpers/online-status'; export * from '@app/_helpers/online-status';
export * from './to-hex';

View File

@ -0,0 +1,9 @@
function asciiToHex(str: string): string {
const arr = [];
for (let n = 0, l = str.length; n < l; n++) {
arr.push(Number(str.charCodeAt(n)).toString(16));
}
return arr.join('');
}
export { asciiToHex };

View File

@ -13,6 +13,7 @@ import { CICRegistry } from '@cicnet/cic-client';
import { personValidation, updateSyncable, vcardValidation } from '@app/_helpers'; import { personValidation, updateSyncable, vcardValidation } from '@app/_helpers';
import { add0x, strip0x } from '@src/assets/js/ethtx/dist/hex'; import { add0x, strip0x } from '@src/assets/js/ethtx/dist/hex';
import { KeystoreService } from '@app/_services/keystore.service'; import { KeystoreService } from '@app/_services/keystore.service';
import * as Automerge from 'automerge';
const vCard = require('vcard-parser'); const vCard = require('vcard-parser');
@Injectable({ @Injectable({
@ -38,6 +39,10 @@ export class UserService {
private categoriesList: BehaviorSubject<object> = new BehaviorSubject<object>(this.categories); private categoriesList: BehaviorSubject<object> = new BehaviorSubject<object>(this.categories);
categoriesSubject: Observable<object> = this.categoriesList.asObservable(); categoriesSubject: Observable<object> = this.categoriesList.asObservable();
history: Array<any> = [];
private historyList: BehaviorSubject<any> = new BehaviorSubject<any>(this.history);
historySubject: Observable<Array<any>> = this.historyList.asObservable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private loggingService: LoggingService, private loggingService: LoggingService,
@ -243,13 +248,22 @@ export class UserService {
async getAccountByAddress( async getAccountByAddress(
accountAddress: string, accountAddress: string,
limit: number = 100 limit: number = 100,
history: boolean = false
): Promise<Observable<AccountDetails>> { ): Promise<Observable<AccountDetails>> {
const accountSubject: Subject<any> = new Subject<any>(); const accountSubject: Subject<any> = new Subject<any>();
this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress))) this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress)))
.pipe(first()) .pipe(first())
.subscribe(async (res) => { .subscribe(async (res) => {
const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
if (history) {
try {
// @ts-ignore
this.historyList.next(Automerge.getHistory(account.m).reverse());
} catch (error) {
this.loggingService.sendErrorLevelMessage('No history found', this, { error });
}
}
const accountInfo = account.m.data; const accountInfo = account.m.data;
await personValidation(accountInfo); await personValidation(accountInfo);
this.tokenService.load.subscribe(async (status: boolean) => { this.tokenService.load.subscribe(async (status: boolean) => {

View File

@ -44,6 +44,23 @@ export class AppComponent implements OnInit {
await this.tokenService.init(); await this.tokenService.init();
await this.userService.init(); await this.userService.init();
await this.transactionService.init(); await this.transactionService.init();
try {
const publicKeys = await this.authService.getPublicKeys();
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
this.authService.getTrustedUsers();
} catch (error) {
this.errorDialogService.openDialog({
message: 'Trusted keys endpoint cannot be reached. Please try again later.',
});
// TODO do something to halt user progress...show a sad cicada page 🦗?
}
if (!this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => {
if (confirm('New Version available. Load New Version?')) {
window.location.reload();
}
});
}
await this.router.events await this.router.events
.pipe(filter((e) => e instanceof NavigationEnd)) .pipe(filter((e) => e instanceof NavigationEnd))
.forEach(async (routeInfo) => { .forEach(async (routeInfo) => {
@ -69,23 +86,6 @@ export class AppComponent implements OnInit {
} }
} }
}); });
try {
const publicKeys = await this.authService.getPublicKeys();
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
this.authService.getTrustedUsers();
} catch (error) {
this.errorDialogService.openDialog({
message: 'Trusted keys endpoint cannot be reached. Please try again later.',
});
// TODO do something to halt user progress...show a sad cicada page 🦗?
}
if (!this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => {
if (confirm('New Version available. Load New Version?')) {
window.location.reload();
}
});
}
} }
// Load resize // Load resize

View File

@ -333,7 +333,88 @@
</div> </div>
</div> </div>
<mat-tab-group *ngIf="account" dynamicHeight mat-align-tabs="start"> <div class="card mt-1">
<app-account-history
*ngIf="history"
[account]="history?.snapshot.data"
(closeWindow)="history = $event"
></app-account-history>
<mat-card-title class="card-header"> HISTORY </mat-card-title>
<div class="card-body">
<div *ngIf="historyLoading">
<h2 class="text-center"><strong>Loading History!</strong></h2>
<mat-progress-bar [mode]="'query'"></mat-progress-bar>
</div>
<mat-table
class="mat-elevation-z10"
[dataSource]="historyDataSource"
matSort
matSortActive="timestamp"
#HistoryTableSort="matSort"
matSortDirection="asc"
matSortDisableClear
>
<ng-container matColumnDef="actor">
<mat-header-cell *matHeaderCellDef mat-sort-header>Actor</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ history?.change.actor }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="signer">
<mat-header-cell *matHeaderCellDef mat-sort-header>Signer</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ history?.snapshot.signature?.data | signatureUser | async }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header>Message</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ history?.change.message }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="sequence">
<mat-header-cell *matHeaderCellDef mat-sort-header>Sequence</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ history?.change.seq }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="dependencies">
<mat-header-cell *matHeaderCellDef mat-sort-header>Dependencies</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ getKeyValue(history?.change.deps) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="timestamp">
<mat-header-cell *matHeaderCellDef mat-sort-header>ChangedAt</mat-header-cell>
<mat-cell *matCellDef="let history">
{{ history?.snapshot.timestamp | unixDate }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="historyDisplayedColumns"></mat-header-row>
<mat-row
*matRowDef="let history; columns: historyDisplayedColumns"
(click)="viewHistory(history)"
matRipple
></mat-row>
</mat-table>
<mat-paginator
#HistoryTablePaginator="matPaginator"
[pageSize]="historyDefaultPageSize"
[pageSizeOptions]="historyPageSizeOptions"
showFirstLastButtons
></mat-paginator>
</div>
</div>
<mat-tab-group dynamicHeight mat-align-tabs="start">
<mat-tab label="Transactions"> <mat-tab label="Transactions">
<app-transaction-details <app-transaction-details
[transaction]="transaction" [transaction]="transaction"

View File

@ -47,6 +47,20 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
@ViewChild('UserTablePaginator', { static: true }) userTablePaginator: MatPaginator; @ViewChild('UserTablePaginator', { static: true }) userTablePaginator: MatPaginator;
@ViewChild('UserTableSort', { static: true }) userTableSort: MatSort; @ViewChild('UserTableSort', { static: true }) userTableSort: MatSort;
historyDataSource: MatTableDataSource<any>;
historyDisplayedColumns: Array<string> = [
'actor',
'signer',
'message',
'sequence',
'dependencies',
'timestamp',
];
historyDefaultPageSize: number = 10;
historyPageSizeOptions: Array<number> = [10, 20, 50, 100];
@ViewChild('HistoryTablePaginator', { static: true }) historyTablePaginator: MatPaginator;
@ViewChild('HistoryTableSort', { static: true }) historyTableSort: MatSort;
accountInfoForm: FormGroup; accountInfoForm: FormGroup;
account: AccountDetails; account: AccountDetails;
accountAddress: string; accountAddress: string;
@ -71,6 +85,9 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
areaType: string; areaType: string;
accountsLoading: boolean = true; accountsLoading: boolean = true;
transactionsLoading: boolean = true; transactionsLoading: boolean = true;
histories: Array<any> = [];
history: any;
historyLoading: boolean = true;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@ -109,7 +126,7 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
this.transactionService.resetTransactionsList(); this.transactionService.resetTransactionsList();
await this.blockSyncService.blockSync(this.accountAddress); await this.blockSyncService.blockSync(this.accountAddress);
this.userService.resetAccountsList(); this.userService.resetAccountsList();
(await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe( (await this.userService.getAccountByAddress(this.accountAddress, 100, true)).subscribe(
async (res) => { async (res) => {
if (res !== undefined) { if (res !== undefined) {
this.account = res; this.account = res;
@ -173,6 +190,18 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
} }
this.cdr.detectChanges(); this.cdr.detectChanges();
}); });
this.userService.historySubject.subscribe(async (histories) => {
this.historyDataSource = new MatTableDataSource<any>(histories);
this.historyDataSource.paginator = this.historyTablePaginator;
this.historyDataSource.sort = this.historyTableSort;
this.histories = histories;
if (histories.length > 0) {
this.historyLoading = false;
}
this.cdr.detectChanges();
});
this.userService.getCategories(); this.userService.getCategories();
this.userService.categoriesSubject.subscribe((res) => { this.userService.categoriesSubject.subscribe((res) => {
this.categories = Object.keys(res); this.categories = Object.keys(res);
@ -213,6 +242,10 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
this.transactionsDataSource.paginator = this.transactionTablePaginator; this.transactionsDataSource.paginator = this.transactionTablePaginator;
this.transactionsDataSource.sort = this.transactionTableSort; this.transactionsDataSource.sort = this.transactionTableSort;
} }
if (this.historyDataSource) {
this.historyDataSource.paginator = this.historyTablePaginator;
this.historyDataSource.sort = this.historyTableSort;
}
} }
doTransactionFilter(value: string): void { doTransactionFilter(value: string): void {
@ -227,6 +260,10 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
this.transaction = transaction; this.transaction = transaction;
} }
viewHistory(history): void {
this.history = history;
}
viewAccount(account): void { viewAccount(account): void {
this.router.navigateByUrl( this.router.navigateByUrl(
`/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}` `/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`
@ -308,4 +345,14 @@ export class AccountDetailsComponent implements OnInit, AfterViewInit {
}); });
} }
} }
getKeyValue(obj: any): string {
let str = '';
if (obj instanceof Object) {
for (const [key, value] of Object.entries(obj)) {
str += `${key}: ${value} `;
}
}
return str;
}
} }

View File

@ -0,0 +1,59 @@
<div *ngIf="account" class="mb-3 mt-1">
<div class="card text-center">
<mat-card-title class="card-header">
<div class="row">
ACCOUNT DETAILS
<button
mat-raised-button
type="button"
class="btn btn-outline-secondary ml-auto mr-2"
(click)="close()"
>
CLOSE
</button>
</div>
</mat-card-title>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<span>Name: {{ account?.vcard?.fn[0].value }}</span>
</li>
<li class="list-group-item">
<span>Phone Number: {{ account?.vcard?.tel[0].value }}</span>
</li>
<li class="list-group-item">
<span>Account Type: {{ account?.type }}</span>
</li>
<li class="list-group-item">
<span>Gender: {{ account?.gender }}</span>
</li>
<li class="list-group-item">
<span>Age: {{ account?.age }}</span>
</li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<span>Bio: {{ account?.products }}</span>
</li>
<li class="list-group-item">
<span>Business Category: {{ account?.category }}</span>
</li>
<li class="list-group-item">
<span>User Location: {{ account?.location?.area_name }}</span>
</li>
<li class="list-group-item">
<span>Location: {{ account?.location?.area }}</span>
</li>
<li class="list-group-item">
<span>Location Type: {{ account?.location?.area_type }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountHistoryComponent } from './account-history.component';
describe('AccountHistoryComponent', () => {
let component: AccountHistoryComponent;
let fixture: ComponentFixture<AccountHistoryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AccountHistoryComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AccountHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
SimpleChanges,
OnChanges,
} from '@angular/core';
const vCard = require('vcard-parser');
@Component({
selector: 'app-account-history',
templateUrl: './account-history.component.html',
styleUrls: ['./account-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountHistoryComponent implements OnInit, OnChanges {
@Input() account;
@Output() closeWindow: EventEmitter<any> = new EventEmitter<any>();
constructor() {}
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {
if (this.account) {
this.account.vcard = vCard.parse(atob(this.account.vcard));
}
}
close(): void {
this.account = null;
this.closeWindow.emit(this.account);
}
}

View File

@ -24,6 +24,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { AccountSearchComponent } from './account-search/account-search.component'; import { AccountSearchComponent } from './account-search/account-search.component';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { AccountHistoryComponent } from './account-history/account-history.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -31,7 +32,9 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
AccountDetailsComponent, AccountDetailsComponent,
CreateAccountComponent, CreateAccountComponent,
AccountSearchComponent, AccountSearchComponent,
AccountHistoryComponent,
], ],
exports: [AccountHistoryComponent],
imports: [ imports: [
CommonModule, CommonModule,
AccountsRoutingModule, AccountsRoutingModule,

View File

@ -0,0 +1,8 @@
import { SignatureUserPipe } from './signature-user.pipe';
describe('SignatureUserPipe', () => {
it('create an instance', () => {
const pipe = new SignatureUserPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import * as openpgp from 'openpgp';
import { asciiToHex } from '@app/_helpers';
import { KeystoreService } from '@app/_services';
@Pipe({
name: 'signatureUser',
})
export class SignatureUserPipe implements PipeTransform {
async transform(armoredSignature: string, ...args: unknown[]): Promise<string> {
const keystore = await KeystoreService.getKeystore();
const signature = await openpgp.signature.readArmored(armoredSignature);
const keyId = asciiToHex(signature.packets[0].issuerKeyId.bytes);
const pubKey = keystore.getPublicKeyForId(keyId);
if (pubKey) {
return pubKey.users[0].userId.userid;
}
return '';
}
}

View File

@ -13,6 +13,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { SafePipe } from '@app/shared/_pipes/safe.pipe'; import { SafePipe } from '@app/shared/_pipes/safe.pipe';
import { NetworkStatusComponent } from './network-status/network-status.component'; import { NetworkStatusComponent } from './network-status/network-status.component';
import { UnixDatePipe } from './_pipes/unix-date.pipe'; import { UnixDatePipe } from './_pipes/unix-date.pipe';
import { SignatureUserPipe } from './_pipes/signature-user.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -26,6 +27,7 @@ import { UnixDatePipe } from './_pipes/unix-date.pipe';
SafePipe, SafePipe,
NetworkStatusComponent, NetworkStatusComponent,
UnixDatePipe, UnixDatePipe,
SignatureUserPipe,
], ],
exports: [ exports: [
TopbarComponent, TopbarComponent,
@ -36,6 +38,7 @@ import { UnixDatePipe } from './_pipes/unix-date.pipe';
SafePipe, SafePipe,
NetworkStatusComponent, NetworkStatusComponent,
UnixDatePipe, UnixDatePipe,
SignatureUserPipe,
], ],
imports: [CommonModule, RouterModule, MatIconModule, MatDialogModule], imports: [CommonModule, RouterModule, MatIconModule, MatDialogModule],
}) })