- add payloads page - refactor frontend to more nice arch - rename integration to integration-testspull/9/head
parent
f5d2190fc4
commit
a38e3a1561
96 changed files with 1307 additions and 819 deletions
@ -1,12 +1,12 @@ |
|||||||
import { Component, Inject } from '@angular/core'; |
import { Component, Inject } from '@angular/core'; |
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
import { AgentModel } from '../../models/agent.model'; |
import { AgentModel } from '../../../models/agent.model'; |
||||||
import { EventEmitter } from '@angular/core'; |
import { EventEmitter } from '@angular/core'; |
||||||
|
|
||||||
@Component({ |
@Component({ |
||||||
selector: 'agent-info-dialog', |
selector: 'agent-info-dialog', |
||||||
templateUrl: 'agent-info-dialog.html', |
templateUrl: 'agent-info-dialog.component.html', |
||||||
styleUrls: ['info-dialog.component.less'] |
styleUrls: ['../base-info-dialog.component.less'] |
||||||
}) |
}) |
||||||
export class AgentInfoDialogComponent { |
export class AgentInfoDialogComponent { |
||||||
is_preview = true; |
is_preview = true; |
@ -0,0 +1,28 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { ApiTableService } from '../../../services'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'assign-job-dialog', |
||||||
|
templateUrl: 'assign-job-dialog.component.html', |
||||||
|
styleUrls: [] |
||||||
|
}) |
||||||
|
export class AssignJobDialogComponent { |
||||||
|
rows: string[] = []; |
||||||
|
selected_rows: string[] = []; |
||||||
|
|
||||||
|
constructor( |
||||||
|
@Inject(MAT_DIALOG_DATA) public agent_id: string, |
||||||
|
private dataSource: ApiTableService, |
||||||
|
) { |
||||||
|
dataSource.getJobs().subscribe(resp => { |
||||||
|
this.rows = resp.map(j => `${j.id} ${j.alias}`) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
assignSelectedJobs() { |
||||||
|
const job_ids = this.selected_rows.map(row => row.split(' ', 1)[0]).join(' '); |
||||||
|
const request = `${this.agent_id} ${job_ids}` |
||||||
|
this.dataSource.createResult(request) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export * from './agent-info-dialog/agent-info-dialog.component'; |
||||||
|
export * from './result-info-dialog/result-info-dialog.component'; |
||||||
|
export * from './job-info-dialog/job-info-dialog.component'; |
||||||
|
export * from './assign-job-dialog/assign-job-dialog.component'; |
||||||
|
export * from './payload-info-dialog/payload-info-dialog.component'; |
@ -0,0 +1,52 @@ |
|||||||
|
<h2 mat-dialog-title *ngIf="isPreview">Job info</h2> |
||||||
|
<h2 mat-dialog-title *ngIf="!isPreview">Editing job info</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>ID</mat-label> |
||||||
|
<input matInput disabled value="{{data.job.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Alias</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.alias"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Args</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.argv"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Type</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.exec_type"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Platform</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.target_platforms"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Schedule</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.schedule"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Payload</mat-label> |
||||||
|
<mat-select [disabled]="isPreview" [(value)]="data.job.payload"> |
||||||
|
<mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option> |
||||||
|
</mat-select> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field" floatLabel="always"> |
||||||
|
<mat-label>Payload data</mat-label> |
||||||
|
<textarea matInput cdkTextareaAutosize *ngIf="!isTooBigPayload" [readonly]="isPreview" |
||||||
|
[(ngModel)]="decodedPayload"> |
||||||
|
</textarea> |
||||||
|
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button> |
||||||
|
<button mat-raised-button *ngIf="!isPreview" (click)="updateJob()">Save</button> |
||||||
|
<button mat-button mat-dialog-close>Cancel</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,46 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { EventEmitter } from '@angular/core'; |
||||||
|
import { BriefOrFullJobModel } from '../../../models/job.model'; |
||||||
|
import { ApiTableService } from 'src/app/services'; |
||||||
|
import { BriefOrFullPayloadModel, isFullPayload } from 'src/app/models'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'job-info-dialog', |
||||||
|
templateUrl: 'job-info-dialog.component.html', |
||||||
|
styleUrls: ['../base-info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class JobInfoDialogComponent { |
||||||
|
isPreview = true; |
||||||
|
isTooBigPayload = false; |
||||||
|
decodedPayload = ""; |
||||||
|
//[id, name]
|
||||||
|
allPayloads: [string, string][] = []; |
||||||
|
|
||||||
|
onSave = new EventEmitter(); |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: BriefOrFullJobModel, dataSource: ApiTableService) { |
||||||
|
if (data.payload !== null) { |
||||||
|
this.showPayload(data.payload) |
||||||
|
} |
||||||
|
|
||||||
|
dataSource.getPayloads().subscribe(resp => { |
||||||
|
this.allPayloads = resp.map(r => [r.id, r.name]) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
showPayload(payload: BriefOrFullPayloadModel) { |
||||||
|
if (isFullPayload(payload)) { |
||||||
|
this.decodedPayload = new TextDecoder().decode(new Uint8Array(payload.data)) |
||||||
|
} else { |
||||||
|
this.isTooBigPayload = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
updateJob() { |
||||||
|
// if (this.decodedPayload.length > 0) {
|
||||||
|
// this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload))
|
||||||
|
// }
|
||||||
|
// this.onSave.emit(this.data);
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
<h2 mat-dialog-title>Result</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
||||||
|
<mat-label>ID</mat-label> |
||||||
|
<input matInput readonly value="{{data.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Name</mat-label> |
||||||
|
<input matInput value="{{data.name}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>MIME-type</mat-label> |
||||||
|
<input matInput value="{{data.mime_type}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Size</mat-label> |
||||||
|
<input matInput value="{{data.size}}"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-button mat-dialog-close>Close</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,14 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { PayloadModel } from 'src/app/models/payload.model'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'payload-info-dialog', |
||||||
|
templateUrl: 'payload-info-dialog.component.html', |
||||||
|
styleUrls: [] |
||||||
|
}) |
||||||
|
export class PayloadInfoDialogComponent { |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: PayloadModel) { } |
||||||
|
|
||||||
|
} |
@ -1,11 +1,11 @@ |
|||||||
import { Component, Inject } from '@angular/core'; |
import { Component, Inject } from '@angular/core'; |
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
import { ResultModel } from '../../models/result.model'; |
import { ResultModel } from '../../../models/result.model'; |
||||||
|
|
||||||
@Component({ |
@Component({ |
||||||
selector: 'result-info-dialog', |
selector: 'result-info-dialog', |
||||||
templateUrl: 'result-info-dialog.html', |
templateUrl: 'result-info-dialog.component.html', |
||||||
styleUrls: ['info-dialog.component.less'] |
styleUrls: ['../base-info-dialog.component.less'] |
||||||
}) |
}) |
||||||
export class ResultInfoDialogComponent { |
export class ResultInfoDialogComponent { |
||||||
decodedResult: string; |
decodedResult: string; |
@ -0,0 +1,34 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; |
||||||
|
import { ErrorService } from 'src/app/services/error.service'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'global-error', |
||||||
|
templateUrl: './global-error.component.html', |
||||||
|
styleUrls: ['./global-error.component.less'] |
||||||
|
}) |
||||||
|
export class GlobalErrorComponent implements OnInit { |
||||||
|
|
||||||
|
constructor( |
||||||
|
private snackBar: MatSnackBar, |
||||||
|
private errorService: ErrorService |
||||||
|
) { } |
||||||
|
|
||||||
|
ngOnInit() { |
||||||
|
this.errorService.error$.subscribe(err => { |
||||||
|
const _config = (duration: number): MatSnackBarConfig => { |
||||||
|
return { |
||||||
|
horizontalPosition: 'right', |
||||||
|
verticalPosition: 'bottom', |
||||||
|
duration |
||||||
|
} |
||||||
|
} |
||||||
|
const error = true; |
||||||
|
const cfg = error ? _config(0) : _config(2000) |
||||||
|
|
||||||
|
if (err != '') { |
||||||
|
this.snackBar.open(err, 'Ok', cfg) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TablesComponent } from '../base-table/base-table.component'; |
||||||
|
import { AgentModel, Area } from '../../../models'; |
||||||
|
import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'agent-table', |
||||||
|
templateUrl: './agent-table.component.html', |
||||||
|
styleUrls: ['../base-table/base-table.component.less'], |
||||||
|
}) |
||||||
|
export class AgentComponent extends TablesComponent<AgentModel> implements OnInit { |
||||||
|
area = 'agents' as Area |
||||||
|
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions'] |
||||||
|
|
||||||
|
showItemDialog(id: string) { |
||||||
|
this.dataSource.getAgent(id).subscribe(resp => { |
||||||
|
const dialog = this.infoDialog.open(AgentInfoDialogComponent, { |
||||||
|
data: resp, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
||||||
|
this.dataSource.updateAgent(result).subscribe(_ => { |
||||||
|
alert('Saved') |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(result => { |
||||||
|
saveSub.unsubscribe() |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
assignJobs(id: string) { |
||||||
|
const dialog = this.infoDialog.open(AssignJobDialogComponent, { |
||||||
|
data: id, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
import { OnInit, Directive, Component } from '@angular/core'; |
||||||
|
import { ApiTableService } from '../../..'; |
||||||
|
import { MatTableDataSource } from '@angular/material/table'; |
||||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||||
|
import { ApiModel, Area } from '../../../models'; |
||||||
|
import { ActivatedRoute, Router } from '@angular/router'; |
||||||
|
|
||||||
|
@Directive() |
||||||
|
export abstract class TablesComponent<T extends ApiModel> implements OnInit { |
||||||
|
abstract area: Area; |
||||||
|
table_data: MatTableDataSource<T> = new MatTableDataSource; |
||||||
|
isLoadingResults = true; |
||||||
|
|
||||||
|
constructor( |
||||||
|
public dataSource: ApiTableService, |
||||||
|
public infoDialog: MatDialog, |
||||||
|
public route: ActivatedRoute, |
||||||
|
public router: Router, |
||||||
|
) { } |
||||||
|
|
||||||
|
ngOnInit() { |
||||||
|
this.loadTableData(); |
||||||
|
this.route.queryParams.subscribe(params => { |
||||||
|
const id = params['id'] |
||||||
|
const new_agent = params['new'] |
||||||
|
if (id) { |
||||||
|
this.showItemDialog(id); |
||||||
|
} |
||||||
|
if (new_agent) { |
||||||
|
this.showItemDialog(null); |
||||||
|
} |
||||||
|
}) |
||||||
|
//interval(10000).subscribe(_ => this.loadTableData());
|
||||||
|
} |
||||||
|
|
||||||
|
loadTableData() { |
||||||
|
this.isLoadingResults = true; |
||||||
|
|
||||||
|
this.dataSource.getMany(this.area).subscribe(resp => { |
||||||
|
this.isLoadingResults = false; |
||||||
|
this.table_data.data = resp; |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
applyFilter(event: Event) { |
||||||
|
const filterValue = (event.target as HTMLInputElement).value; |
||||||
|
this.table_data.filter = filterValue.trim().toLowerCase(); |
||||||
|
} |
||||||
|
|
||||||
|
deleteItem(id: string) { |
||||||
|
if (confirm(`Delete ${id}?`)) { |
||||||
|
this.dataSource.delete(id, this.area) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
abstract displayedColumns: string[]; |
||||||
|
abstract showItemDialog(id: string | null): void; |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export * from './agent-table/agent-table.component'; |
||||||
|
export * from './base-table/base-table.component'; |
||||||
|
export * from './job-table/job-table.component'; |
||||||
|
export * from './payload-table/payload-table.component'; |
||||||
|
export * from './result-table/result-table.component'; |
@ -0,0 +1,50 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TablesComponent } from '../base-table/base-table.component'; |
||||||
|
import { Area, JobModel } from '../../../models'; |
||||||
|
import { JobInfoDialogComponent } from '../../dialogs'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'job-table', |
||||||
|
templateUrl: './job-table.component.html', |
||||||
|
styleUrls: ['../base-table/base-table.component.less'], |
||||||
|
providers: [{ provide: 'area', useValue: 'jobs' }] |
||||||
|
}) |
||||||
|
export class JobComponent extends TablesComponent<JobModel> { |
||||||
|
area = 'jobs' as Area; |
||||||
|
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] |
||||||
|
|
||||||
|
showItemDialog(id: string | null) { |
||||||
|
const show_dlg = (id: string, edit: boolean) => { |
||||||
|
this.dataSource.getJob(id).subscribe(resp => { |
||||||
|
var dialog = this.infoDialog.open(JobInfoDialogComponent, { |
||||||
|
data: resp, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
if (edit) { |
||||||
|
dialog.componentInstance.isPreview = false |
||||||
|
} |
||||||
|
|
||||||
|
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
||||||
|
this.dataSource.updateJob(result) |
||||||
|
.subscribe(_ => { |
||||||
|
alert("Saved") |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(result => { |
||||||
|
saveSub.unsubscribe() |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if (id) { |
||||||
|
show_dlg(id, false) |
||||||
|
} else { |
||||||
|
this.dataSource.create(null, this.area).subscribe(resp => { |
||||||
|
show_dlg(resp[0], true) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
<div class="mat-elevation-z8"> |
||||||
|
|
||||||
|
<div class="table-container"> |
||||||
|
<div class="loading-shade" *ngIf="isLoadingResults"> |
||||||
|
<mat-spinner *ngIf="isLoadingResults"></mat-spinner> |
||||||
|
</div> |
||||||
|
<mat-form-field appearance="standard"> |
||||||
|
<mat-label>Filter</mat-label> |
||||||
|
<input matInput (keyup)="applyFilter($event)" #input> |
||||||
|
</mat-form-field> |
||||||
|
<button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button> |
||||||
|
|
||||||
|
<table mat-table fixedLayout="true" [dataSource]="table_data" class="data-table" matSort matSortActive="id" |
||||||
|
matSortDisableClear matSortDirection="desc"> |
||||||
|
|
||||||
|
<ng-container matColumnDef="name"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>Name</th> |
||||||
|
<td mat-cell *matCellDef="let row"> |
||||||
|
{{row.name}} |
||||||
|
</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<ng-container matColumnDef="mime_type"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>MIME-type</th> |
||||||
|
<td mat-cell *matCellDef="let row"> |
||||||
|
{{row.mime_type}} |
||||||
|
</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<ng-container matColumnDef="size"> |
||||||
|
<th mat-header-cell *matHeaderCellDef>Size</th> |
||||||
|
<td mat-cell *matCellDef="let row"> |
||||||
|
{{row.size}} |
||||||
|
</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<ng-container matColumnDef="actions"> |
||||||
|
<th mat-header-cell *matHeaderCellDef></th> |
||||||
|
<td mat-cell *matCellDef="let row"> |
||||||
|
<button mat-icon-button routerLink='.' [queryParams]="{id: row.id}"> |
||||||
|
<mat-icon>more_horiz</mat-icon> |
||||||
|
</button> |
||||||
|
| |
||||||
|
<button mat-icon-button (click)="deleteItem(row.id)"> |
||||||
|
<mat-icon>delete</mat-icon> |
||||||
|
</button> |
||||||
|
</td> |
||||||
|
</ng-container> |
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> |
||||||
|
<tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr> |
||||||
|
<tr class="mat-row" *matNoDataRow> |
||||||
|
<td class="mat-cell">No data</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"> |
||||||
|
</mat-paginator> --> |
||||||
|
</div> |
@ -0,0 +1,30 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
import { Area } from 'src/app/models'; |
||||||
|
import { PayloadModel } from 'src/app/models/payload.model'; |
||||||
|
import { PayloadInfoDialogComponent } from '../../dialogs'; |
||||||
|
import { TablesComponent } from '../base-table/base-table.component'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'payload-table', |
||||||
|
templateUrl: './payload-table.component.html', |
||||||
|
styleUrls: ['../base-table/base-table.component.less'], |
||||||
|
providers: [{ provide: 'area', useValue: 'payloads' }] |
||||||
|
}) |
||||||
|
export class PayloadComponent extends TablesComponent<PayloadModel> { |
||||||
|
area = 'payloads' as Area |
||||||
|
displayedColumns = ["name", "mime_type", "size"]; |
||||||
|
|
||||||
|
showItemDialog(id: string) { |
||||||
|
this.dataSource.getPayload(id).subscribe(resp => { |
||||||
|
const dialog = this.infoDialog.open(PayloadInfoDialogComponent, { |
||||||
|
data: resp, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(_ => { |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TablesComponent } from '../base-table/base-table.component'; |
||||||
|
import { Area, ResultModel } from '../../../models'; |
||||||
|
import { ResultInfoDialogComponent } from '../../dialogs'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'results-table', |
||||||
|
templateUrl: './result-table.component.html', |
||||||
|
styleUrls: ['../base-table/base-table.component.less'], |
||||||
|
providers: [{ provide: 'area', useValue: 'map' }] |
||||||
|
}) |
||||||
|
export class ResultComponent extends TablesComponent<ResultModel> { |
||||||
|
area = 'map' as Area |
||||||
|
displayedColumns = [ |
||||||
|
'id', |
||||||
|
'alias', |
||||||
|
'agent_id', |
||||||
|
'job_id', |
||||||
|
'state', |
||||||
|
'last_updated', |
||||||
|
'actions' |
||||||
|
]; |
||||||
|
|
||||||
|
showItemDialog(id: string) { |
||||||
|
this.dataSource.getResult(id).subscribe(resp => { |
||||||
|
const dialog = this.infoDialog.open(ResultInfoDialogComponent, { |
||||||
|
data: resp, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(_ => { |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -1,14 +0,0 @@ |
|||||||
export * from './agent.model'; |
|
||||||
export * from './result.model'; |
|
||||||
export * from './job.model'; |
|
||||||
|
|
||||||
export interface UTCDate { |
|
||||||
secs_since_epoch: number, |
|
||||||
nanos_since_epoch: number |
|
||||||
} |
|
||||||
|
|
||||||
export abstract class ApiModel { } |
|
||||||
|
|
||||||
export interface Empty extends ApiModel { } |
|
||||||
|
|
||||||
export type Area = "agents" | "jobs" | "map"; |
|
@ -1,11 +0,0 @@ |
|||||||
import { ApiModel } from "."; |
|
||||||
|
|
||||||
export interface JobModel extends ApiModel { |
|
||||||
alias: string, |
|
||||||
argv: string, |
|
||||||
id: string, |
|
||||||
exec_type: string, |
|
||||||
platform: string, |
|
||||||
payload: number[] | null, |
|
||||||
schedule: string | null, |
|
||||||
} |
|
@ -1,53 +0,0 @@ |
|||||||
import { environment } from 'src/environments/environment'; |
|
||||||
import { HttpClient } from '@angular/common/http'; |
|
||||||
import { firstValueFrom } from 'rxjs'; |
|
||||||
import { ApiModel, Empty, Area } from '../models'; |
|
||||||
|
|
||||||
interface ServerResponse<T extends ApiModel> { |
|
||||||
status: "ok" | "err", |
|
||||||
data: T | string |
|
||||||
} |
|
||||||
|
|
||||||
export class ApiTableService<T extends ApiModel> { |
|
||||||
area: Area; |
|
||||||
|
|
||||||
constructor(private http: HttpClient, area: Area) { |
|
||||||
this.area = area; |
|
||||||
} |
|
||||||
|
|
||||||
requestUrl = `${environment.server}/cmd/`; |
|
||||||
|
|
||||||
async req<R extends ApiModel>(cmd: string): Promise<ServerResponse<R>> { |
|
||||||
return await firstValueFrom(this.http.post<ServerResponse<R>>(this.requestUrl, cmd)) |
|
||||||
} |
|
||||||
|
|
||||||
async getOne(id: string, area: string = this.area): Promise<ServerResponse<T>> { |
|
||||||
const resp = await this.req<T[]>(`${area} read ${id}`) |
|
||||||
if (resp.data.length === 0) { |
|
||||||
return { |
|
||||||
status: 'err', |
|
||||||
data: `${id} not found in ${area}` |
|
||||||
} |
|
||||||
} |
|
||||||
return { |
|
||||||
status: resp.status, |
|
||||||
data: resp.data[0] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async getMany(): Promise<ServerResponse<T[]>> { |
|
||||||
return await this.req(`${this.area} read`) |
|
||||||
} |
|
||||||
|
|
||||||
async update(item: T): Promise<ServerResponse<Empty>> { |
|
||||||
return await this.req(`${this.area} update '${JSON.stringify(item)}'`) |
|
||||||
} |
|
||||||
|
|
||||||
async delete(id: string): Promise<ServerResponse<Empty>> { |
|
||||||
return await this.req(`${this.area} delete ${id}`) |
|
||||||
} |
|
||||||
|
|
||||||
async create(item: string): Promise<ServerResponse<string[]>> { |
|
||||||
return await this.req(`${this.area} create ${item}`) |
|
||||||
} |
|
||||||
} |
|
@ -1,52 +0,0 @@ |
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core'; |
|
||||||
import { TablesComponent } from './table.component'; |
|
||||||
import { AgentModel } from '../models'; |
|
||||||
import { AgentInfoDialogComponent } from './dialogs/agent_info.component'; |
|
||||||
import { HttpErrorResponse } from '@angular/common/http'; |
|
||||||
import { AssignJobDialogComponent } from './dialogs'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
selector: 'agent-table', |
|
||||||
templateUrl: './agent.component.html', |
|
||||||
styleUrls: ['./table.component.less'] |
|
||||||
}) |
|
||||||
export class AgentComponent extends TablesComponent<AgentModel> implements OnInit { |
|
||||||
|
|
||||||
//dialogSubscr!: Subscription;
|
|
||||||
area = 'agents' as const; |
|
||||||
|
|
||||||
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions'] |
|
||||||
|
|
||||||
show_item_dialog(id: string) { |
|
||||||
this.data_source!.getOne(id).then(resp => { |
|
||||||
if (resp.status === 'ok') { |
|
||||||
const dialog = this.infoDialog.open(AgentInfoDialogComponent, { |
|
||||||
data: resp.data as AgentModel, |
|
||||||
width: '1000px', |
|
||||||
}); |
|
||||||
|
|
||||||
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
|
||||||
this.data_source!.update(result).then(_ => { |
|
||||||
this.openSnackBar('Saved', false) |
|
||||||
this.loadTableData() |
|
||||||
}) |
|
||||||
.catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) |
|
||||||
}) |
|
||||||
|
|
||||||
dialog.afterClosed().subscribe(result => { |
|
||||||
saveSub.unsubscribe() |
|
||||||
this.router.navigate(['.'], { relativeTo: this.route }) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
this.openSnackBar(resp.data) |
|
||||||
} |
|
||||||
}).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) |
|
||||||
} |
|
||||||
|
|
||||||
assignJobs(id: string) { |
|
||||||
const dialog = this.infoDialog.open(AssignJobDialogComponent, { |
|
||||||
data: id, |
|
||||||
width: '1000px', |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
@ -1,33 +0,0 @@ |
|||||||
import { Component, Inject } from '@angular/core'; |
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
|
||||||
import { HttpClient } from '@angular/common/http'; |
|
||||||
import { ApiTableService } from '../../services'; |
|
||||||
import { JobModel } from '../../models'; |
|
||||||
import { MatListOption } from '@angular/material/list'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
selector: 'assign-job-dialog', |
|
||||||
templateUrl: 'assign-job-dialog.html', |
|
||||||
styleUrls: [] |
|
||||||
}) |
|
||||||
export class AssignJobDialogComponent { |
|
||||||
rows: string[] = []; |
|
||||||
selected_rows: string[] = []; |
|
||||||
|
|
||||||
constructor(@Inject(MAT_DIALOG_DATA) public agent_id: string, private http: HttpClient) { |
|
||||||
new ApiTableService(http, "jobs").getMany().then(result => { |
|
||||||
if (result.status == "ok") { |
|
||||||
const jobs = result.data as JobModel[] |
|
||||||
this.rows = jobs.map(j => `${j.id} ${j.alias}`) |
|
||||||
} else { |
|
||||||
alert(result.data as string) |
|
||||||
} |
|
||||||
}).catch(err => alert(err)) |
|
||||||
} |
|
||||||
|
|
||||||
assignSelectedJobs() { |
|
||||||
const job_ids = this.selected_rows.map(row => row.split(' ', 1)[0]).join(' '); |
|
||||||
const request = `${this.agent_id} ${job_ids}` |
|
||||||
new ApiTableService(this.http, "map").create(request).catch(err => alert(err)) |
|
||||||
} |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
export * from './agent_info.component'; |
|
||||||
export * from './result_info.component'; |
|
||||||
export * from './job_info.component'; |
|
||||||
export * from './assign_job.component'; |
|
@ -1,44 +0,0 @@ |
|||||||
<h2 mat-dialog-title *ngIf="is_preview">Job info</h2> |
|
||||||
<h2 mat-dialog-title *ngIf="!is_preview">Editing job info</h2> |
|
||||||
<mat-dialog-content> |
|
||||||
<div class="info-dialog-forms-box-smol"> |
|
||||||
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
|
||||||
<mat-label>ID</mat-label> |
|
||||||
<input matInput disabled value="{{data.id}}"> |
|
||||||
</mat-form-field> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Alias</mat-label> |
|
||||||
<input matInput [readonly]="is_preview" [(ngModel)]="data.alias"> |
|
||||||
</mat-form-field> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Args</mat-label> |
|
||||||
<input matInput [readonly]="is_preview" [(ngModel)]="data.argv"> |
|
||||||
</mat-form-field> |
|
||||||
</div> |
|
||||||
<div class="info-dialog-forms-box-smol"> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Type</mat-label> |
|
||||||
<input matInput [readonly]="is_preview" [(ngModel)]="data.exec_type"> |
|
||||||
</mat-form-field> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Platform</mat-label> |
|
||||||
<input matInput [readonly]="is_preview" [(ngModel)]="data.platform"> |
|
||||||
</mat-form-field> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Schedule</mat-label> |
|
||||||
<input matInput [readonly]="is_preview" [(ngModel)]="data.schedule"> |
|
||||||
</mat-form-field> |
|
||||||
</div> |
|
||||||
<div class="info-dialog-forms-box"> |
|
||||||
<mat-form-field class="info-dlg-field"> |
|
||||||
<mat-label>Payload</mat-label> |
|
||||||
<textarea matInput cdkTextareaAutosize [readonly]="is_preview" [(ngModel)]="decodedPayload"> |
|
||||||
</textarea> |
|
||||||
</mat-form-field> |
|
||||||
</div> |
|
||||||
</mat-dialog-content> |
|
||||||
<mat-dialog-actions align="end"> |
|
||||||
<button mat-raised-button *ngIf="is_preview" (click)="is_preview = false">Edit</button> |
|
||||||
<button mat-raised-button *ngIf="!is_preview" (click)="updateJob()">Save</button> |
|
||||||
<button mat-button mat-dialog-close>Cancel</button> |
|
||||||
</mat-dialog-actions> |
|
@ -1,30 +0,0 @@ |
|||||||
import { Component, Inject } from '@angular/core'; |
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
|
||||||
import { EventEmitter } from '@angular/core'; |
|
||||||
import { JobModel } from '../../models/job.model'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
selector: 'job-info-dialog', |
|
||||||
templateUrl: 'job-info-dialog.html', |
|
||||||
styleUrls: ['info-dialog.component.less'] |
|
||||||
}) |
|
||||||
export class JobInfoDialogComponent { |
|
||||||
is_preview = true; |
|
||||||
decodedPayload: string; |
|
||||||
onSave = new EventEmitter(); |
|
||||||
|
|
||||||
constructor(@Inject(MAT_DIALOG_DATA) public data: JobModel) { |
|
||||||
if (data.payload !== null) { |
|
||||||
this.decodedPayload = new TextDecoder().decode(new Uint8Array(data.payload)) |
|
||||||
} else { |
|
||||||
this.decodedPayload = "" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
updateJob() { |
|
||||||
if (this.decodedPayload.length > 0) { |
|
||||||
this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload)) |
|
||||||
} |
|
||||||
this.onSave.emit(this.data); |
|
||||||
} |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
export * from './agent.component'; |
|
||||||
export * from './job.component'; |
|
||||||
export * from './result.component'; |
|
@ -1,59 +0,0 @@ |
|||||||
import { Component, OnInit } from '@angular/core'; |
|
||||||
import { TablesComponent } from './table.component'; |
|
||||||
import { JobModel } from '../models'; |
|
||||||
import { JobInfoDialogComponent } from './dialogs'; |
|
||||||
import { HttpErrorResponse } from '@angular/common/http'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
selector: 'job-table', |
|
||||||
templateUrl: './job.component.html', |
|
||||||
styleUrls: ['./table.component.less'] |
|
||||||
}) |
|
||||||
export class JobComponent extends TablesComponent<JobModel> { |
|
||||||
area = 'jobs' as const; |
|
||||||
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] |
|
||||||
|
|
||||||
show_item_dialog(id: string | null) { |
|
||||||
const show_dlg = (id: string, edit: boolean) => { |
|
||||||
this.data_source!.getOne(id).then(resp => { |
|
||||||
if (resp.status === 'ok') { |
|
||||||
var dialog = this.infoDialog.open(JobInfoDialogComponent, { |
|
||||||
data: resp.data as JobModel, |
|
||||||
width: '1000px', |
|
||||||
}); |
|
||||||
if (edit) { |
|
||||||
dialog.componentInstance.is_preview = false |
|
||||||
} |
|
||||||
|
|
||||||
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
|
||||||
this.data_source!.update(result) |
|
||||||
.then(_ => { |
|
||||||
this.openSnackBar("Saved", false) |
|
||||||
this.loadTableData() |
|
||||||
}) |
|
||||||
.catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) |
|
||||||
}) |
|
||||||
|
|
||||||
dialog.afterClosed().subscribe(result => { |
|
||||||
saveSub.unsubscribe() |
|
||||||
this.router.navigate(['.'], { relativeTo: this.route }) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
this.openSnackBar(resp.data) |
|
||||||
} |
|
||||||
}).catch((err: any) => this.openSnackBar(err)) |
|
||||||
} |
|
||||||
|
|
||||||
if (id) { |
|
||||||
show_dlg(id, false) |
|
||||||
} else { |
|
||||||
this.data_source!.create('"{}"').then(resp => { |
|
||||||
if (resp.status === 'ok') { |
|
||||||
show_dlg(resp.data[0], true) |
|
||||||
} else { |
|
||||||
this.openSnackBar(resp.data) |
|
||||||
} |
|
||||||
}).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,41 +0,0 @@ |
|||||||
import { Component, OnInit } from '@angular/core'; |
|
||||||
import { TablesComponent } from './table.component'; |
|
||||||
import { ResultModel } from '../models'; |
|
||||||
import { ResultInfoDialogComponent } from './dialogs'; |
|
||||||
import { HttpErrorResponse } from '@angular/common/http'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
selector: 'results-table', |
|
||||||
templateUrl: './result.component.html', |
|
||||||
styleUrls: ['./table.component.less'] |
|
||||||
}) |
|
||||||
export class ResultComponent extends TablesComponent<ResultModel> { |
|
||||||
area = 'map' as const; |
|
||||||
|
|
||||||
displayedColumns = [ |
|
||||||
'id', |
|
||||||
'alias', |
|
||||||
'agent_id', |
|
||||||
'job_id', |
|
||||||
'state', |
|
||||||
'last_updated', |
|
||||||
'actions' |
|
||||||
]; |
|
||||||
|
|
||||||
show_item_dialog(id: string) { |
|
||||||
this.data_source!.getOne(id).then(resp => { |
|
||||||
if (resp.status === 'ok') { |
|
||||||
const dialog = this.infoDialog.open(ResultInfoDialogComponent, { |
|
||||||
data: resp.data as ResultModel, |
|
||||||
width: '1000px', |
|
||||||
}); |
|
||||||
|
|
||||||
dialog.afterClosed().subscribe(result => { |
|
||||||
this.router.navigate(['.'], { relativeTo: this.route }) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
this.openSnackBar(resp.data) |
|
||||||
} |
|
||||||
}).catch((err: HttpErrorResponse) => this.openSnackBar(err.message)) |
|
||||||
} |
|
||||||
} |
|
@ -1,84 +0,0 @@ |
|||||||
import { OnInit, Directive } from '@angular/core'; |
|
||||||
import { HttpClient } from '@angular/common/http'; |
|
||||||
import { ApiTableService } from '../'; |
|
||||||
import { MatTableDataSource } from '@angular/material/table'; |
|
||||||
import { MatDialog } from '@angular/material/dialog'; |
|
||||||
import { ApiModel, Area } from '../models'; |
|
||||||
import { ActivatedRoute, Router } from '@angular/router'; |
|
||||||
import { interval } from 'rxjs'; |
|
||||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; |
|
||||||
|
|
||||||
@Directive() |
|
||||||
export abstract class TablesComponent<T extends ApiModel> implements OnInit { |
|
||||||
abstract area: Area; |
|
||||||
data_source!: ApiTableService<T>; |
|
||||||
table_data!: MatTableDataSource<T>; |
|
||||||
|
|
||||||
isLoadingResults = true; |
|
||||||
|
|
||||||
constructor( |
|
||||||
public httpClient: HttpClient, |
|
||||||
public infoDialog: MatDialog, |
|
||||||
public route: ActivatedRoute, |
|
||||||
public router: Router, |
|
||||||
public snackBar: MatSnackBar |
|
||||||
) { |
|
||||||
this.table_data = new MatTableDataSource; |
|
||||||
} |
|
||||||
|
|
||||||
ngOnInit() { |
|
||||||
this.data_source = new ApiTableService(this.httpClient, this.area); |
|
||||||
this.loadTableData(); |
|
||||||
this.route.queryParams.subscribe(params => { |
|
||||||
const id = params['id'] |
|
||||||
const new_agent = params['new'] |
|
||||||
if (id) { |
|
||||||
this.show_item_dialog(id); |
|
||||||
} |
|
||||||
if (new_agent) { |
|
||||||
this.show_item_dialog(null); |
|
||||||
} |
|
||||||
}) |
|
||||||
//interval(10000).subscribe(_ => this.loadTableData());
|
|
||||||
} |
|
||||||
|
|
||||||
async loadTableData() { |
|
||||||
this.isLoadingResults = true; |
|
||||||
//possibly needs try/catch
|
|
||||||
const data = await this.data_source!.getMany(); |
|
||||||
this.isLoadingResults = false; |
|
||||||
|
|
||||||
if (typeof data.data !== 'string') { |
|
||||||
this.table_data.data = data.data |
|
||||||
} else { |
|
||||||
alert(`Error: ${data}`) |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
apply_filter(event: Event) { |
|
||||||
const filterValue = (event.target as HTMLInputElement).value; |
|
||||||
this.table_data.filter = filterValue.trim().toLowerCase(); |
|
||||||
} |
|
||||||
|
|
||||||
deleteItem(id: string) { |
|
||||||
if (confirm(`Delete ${id}?`)) { |
|
||||||
this.data_source!.delete(id).catch(this.openSnackBar) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
openSnackBar(message: any, error: boolean = true) { |
|
||||||
const msg = JSON.stringify(message) |
|
||||||
const _config = (duration: number): MatSnackBarConfig => { |
|
||||||
return { |
|
||||||
horizontalPosition: 'right', |
|
||||||
verticalPosition: 'bottom', |
|
||||||
duration |
|
||||||
} |
|
||||||
} |
|
||||||
const cfg = error ? _config(0) : _config(2000) |
|
||||||
this.snackBar.open(msg, 'Ok', cfg); |
|
||||||
} |
|
||||||
|
|
||||||
abstract displayedColumns: string[]; |
|
||||||
abstract show_item_dialog(id: string | null): void; |
|
||||||
} |
|
@ -1,6 +1,6 @@ |
|||||||
import { UTCDate, ApiModel } from "."; |
import { UTCDate } from "."; |
||||||
|
|
||||||
export interface AgentModel extends ApiModel { |
export interface AgentModel { |
||||||
alias: string | null, |
alias: string | null, |
||||||
hostname: string, |
hostname: string, |
||||||
host_info: string, |
host_info: string, |
@ -0,0 +1,24 @@ |
|||||||
|
import { AgentModel } from './agent.model'; |
||||||
|
import { JobModel } from './job.model'; |
||||||
|
import { PayloadModel } from './payload.model'; |
||||||
|
import { ResultModel } from './result.model'; |
||||||
|
|
||||||
|
export * from './agent.model'; |
||||||
|
export * from './result.model'; |
||||||
|
export * from './job.model'; |
||||||
|
export * from './payload.model'; |
||||||
|
|
||||||
|
export interface UTCDate { |
||||||
|
secs_since_epoch: number, |
||||||
|
nanos_since_epoch: number |
||||||
|
} |
||||||
|
|
||||||
|
export type Area = "agents" | "jobs" | "map" | "payloads"; |
||||||
|
|
||||||
|
export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty; |
||||||
|
|
||||||
|
export interface Empty { } |
||||||
|
|
||||||
|
export function getAreaByModel(_: AgentModel): Area { |
||||||
|
return "agents" |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { BriefOrFullPayloadModel } from './' |
||||||
|
|
||||||
|
export interface JobModel { |
||||||
|
alias: string | null, |
||||||
|
argv: string, |
||||||
|
id: string, |
||||||
|
exec_type: string, |
||||||
|
target_platforms: string, |
||||||
|
payload: string | null, |
||||||
|
schedule: string | null, |
||||||
|
} |
||||||
|
|
||||||
|
export interface BriefOrFullJobModel { |
||||||
|
job: JobModel, |
||||||
|
payload: BriefOrFullPayloadModel | null, |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
export interface PayloadModel { |
||||||
|
id: string, |
||||||
|
mime_type: string, |
||||||
|
name: string, |
||||||
|
size: number, |
||||||
|
} |
||||||
|
|
||||||
|
export interface FullPayloadModel { |
||||||
|
meta: PayloadModel, |
||||||
|
data: number[] |
||||||
|
} |
||||||
|
|
||||||
|
export type BriefOrFullPayloadModel = PayloadModel | FullPayloadModel; |
||||||
|
|
||||||
|
export function isFullPayload(payload: BriefOrFullPayloadModel): payload is FullPayloadModel { |
||||||
|
return (payload as FullPayloadModel).data !== undefined |
||||||
|
} |
@ -1,6 +1,6 @@ |
|||||||
import { UTCDate, ApiModel } from "."; |
import { UTCDate } from "."; |
||||||
|
|
||||||
export interface ResultModel extends ApiModel { |
export interface ResultModel { |
||||||
agent_id: string, |
agent_id: string, |
||||||
alias: string, |
alias: string, |
||||||
created: UTCDate, |
created: UTCDate, |
@ -0,0 +1,138 @@ |
|||||||
|
import { environment } from 'src/environments/environment'; |
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; |
||||||
|
import { Observable, map, catchError, throwError } from 'rxjs'; |
||||||
|
import { ApiModel, getAreaByModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, BriefOrFullJobModel } from '../models'; |
||||||
|
import { Injectable, Inject } from '@angular/core'; |
||||||
|
import { ErrorService } from './error.service'; |
||||||
|
|
||||||
|
type Status = "ok" | "err"; |
||||||
|
|
||||||
|
interface ServerResponse<T extends ApiModel> { |
||||||
|
status: Status, |
||||||
|
data: T | string |
||||||
|
} |
||||||
|
|
||||||
|
@Injectable({ |
||||||
|
providedIn: 'root' |
||||||
|
}) |
||||||
|
export class ApiTableService { |
||||||
|
|
||||||
|
constructor( |
||||||
|
private http: HttpClient, |
||||||
|
private errorService: ErrorService |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
requestUrl = `${environment.server}/cmd/`; |
||||||
|
|
||||||
|
req<R extends ApiModel>(cmd: string): Observable<ServerResponse<R>> { |
||||||
|
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd).pipe( |
||||||
|
catchError(this.errorHandler) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
getOne<T extends ApiModel>(id: string, area: Area, brief: 'yes' | 'no' | 'auto' | null = null): Observable<T> { |
||||||
|
const request = `${area} read ${id}` + (brief !== null ? `-b=${brief}` : '') |
||||||
|
const resp = this.req<T[]>(request).pipe( |
||||||
|
map(resp => { |
||||||
|
if (resp.data.length === 0) { |
||||||
|
return { |
||||||
|
status: 'err' as Status, |
||||||
|
data: `${id} not found in ${area}` |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
status: resp.status, |
||||||
|
data: resp.data[0] |
||||||
|
} |
||||||
|
})); |
||||||
|
return this.filterErrStatus(resp) |
||||||
|
} |
||||||
|
|
||||||
|
getAgent(id: string): Observable<AgentModel> { |
||||||
|
return this.getOne(id, 'agents') |
||||||
|
} |
||||||
|
|
||||||
|
getJob(id: string): Observable<BriefOrFullJobModel> { |
||||||
|
return this.getOne(id, 'jobs') |
||||||
|
} |
||||||
|
|
||||||
|
getResult(id: string): Observable<ResultModel> { |
||||||
|
return this.getOne(id, 'map') |
||||||
|
} |
||||||
|
|
||||||
|
getPayload(id: string): Observable<PayloadModel> { |
||||||
|
return this.getOne(id, 'payloads') |
||||||
|
} |
||||||
|
|
||||||
|
getMany(area: Area): Observable<any[]> { |
||||||
|
return this.filterErrStatus(this.req(`${area} read`)) |
||||||
|
} |
||||||
|
|
||||||
|
getAgents(): Observable<AgentModel[]> { |
||||||
|
return this.getMany('agents') |
||||||
|
} |
||||||
|
|
||||||
|
getJobs(): Observable<AgentModel[]> { |
||||||
|
return this.getMany('jobs') |
||||||
|
} |
||||||
|
|
||||||
|
getResults(): Observable<AgentModel[]> { |
||||||
|
return this.getMany('map') |
||||||
|
} |
||||||
|
|
||||||
|
getPayloads(): Observable<PayloadModel[]> { |
||||||
|
return this.getMany('payloads') |
||||||
|
} |
||||||
|
|
||||||
|
update<T extends ApiModel>(item: T, area: Area): Observable<Empty> { |
||||||
|
return this.filterErrStatus(this.req(`${area} update '${JSON.stringify(item)}'`)) |
||||||
|
} |
||||||
|
|
||||||
|
updateAgent(item: AgentModel): Observable<Empty> { |
||||||
|
return this.update(item, 'agents') |
||||||
|
} |
||||||
|
|
||||||
|
updateJob(item: JobModel): Observable<Empty> { |
||||||
|
return this.update(item, 'jobs') |
||||||
|
} |
||||||
|
|
||||||
|
updateResult(item: ResultModel): Observable<Empty> { |
||||||
|
return this.update(item, 'map') |
||||||
|
} |
||||||
|
|
||||||
|
updatePayload(item: PayloadModel): Observable<Empty> { |
||||||
|
return this.update(item, 'payloads') |
||||||
|
} |
||||||
|
|
||||||
|
delete(id: string, area: Area): Observable<Empty> { |
||||||
|
return this.filterErrStatus(this.req(`${area} delete ${id}`)) |
||||||
|
} |
||||||
|
|
||||||
|
create(item: string | null, area: Area): Observable<string[]> { |
||||||
|
if (!item) { |
||||||
|
item = '"{}"' |
||||||
|
} |
||||||
|
return this.filterErrStatus(this.req(`${area} create ${item}`)) |
||||||
|
} |
||||||
|
|
||||||
|
createResult(item: string): Observable<string[]> { |
||||||
|
return this.create(item, 'map') |
||||||
|
} |
||||||
|
|
||||||
|
filterErrStatus<R extends ApiModel>(obs: Observable<ServerResponse<R>>): Observable<R> { |
||||||
|
return obs.pipe( |
||||||
|
map(r => { |
||||||
|
if (r.status == 'err') { |
||||||
|
throw new Error(r.data as string) |
||||||
|
} |
||||||
|
return r.data as R |
||||||
|
}), |
||||||
|
catchError(this.errorHandler.bind(this))) |
||||||
|
} |
||||||
|
|
||||||
|
errorHandler<R>(err: HttpErrorResponse, _: R) { |
||||||
|
this.errorService.handle(err.message); |
||||||
|
return throwError(() => new Error(err.message)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import { Injectable } from '@angular/core'; |
||||||
|
import { Subject } from 'rxjs'; |
||||||
|
|
||||||
|
@Injectable({ |
||||||
|
providedIn: 'root' |
||||||
|
}) |
||||||
|
export class ErrorService { |
||||||
|
error$ = new Subject<string>(); |
||||||
|
|
||||||
|
handle(msg: string) { |
||||||
|
this.error$.next(msg) |
||||||
|
} |
||||||
|
|
||||||
|
clear() { |
||||||
|
this.handle('') |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
mod fixtures; |
||||||
|
mod helpers; |
||||||
|
mod integration_tests; |
||||||
|
|
||||||
|
#[macro_use] |
||||||
|
extern crate rstest; |
||||||
|
|
||||||
|
#[macro_use] |
||||||
|
extern crate tracing; |
||||||
|
|
||||||
|
#[ctor::ctor] |
||||||
|
fn __init() { |
||||||
|
u_lib::logging::init_logger(None); |
||||||
|
} |
@ -1,6 +0,0 @@ |
|||||||
mod fixtures; |
|
||||||
mod helpers; |
|
||||||
mod integration; |
|
||||||
|
|
||||||
#[macro_use] |
|
||||||
extern crate rstest; |
|
@ -1,28 +1,35 @@ |
|||||||
use std::env; |
use std::env; |
||||||
|
use std::io::{stderr, stdout}; |
||||||
use std::path::Path; |
use std::path::Path; |
||||||
|
|
||||||
use tracing_appender::rolling; |
use tracing_appender::rolling; |
||||||
use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter}; |
use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter}; |
||||||
|
|
||||||
pub fn init_logger(logfile: Option<impl AsRef<Path> + Send + Sync + 'static>) { |
pub fn init_logger(logfile: Option<&str>) { |
||||||
if env::var("RUST_LOG").is_err() { |
if env::var("RUST_LOG").is_err() { |
||||||
env::set_var("RUST_LOG", "info") |
env::set_var("RUST_LOG", "info") |
||||||
} |
} |
||||||
|
|
||||||
|
let output_layer = if cfg!(test) { |
||||||
|
fmt::layer().with_writer(stdout).with_test_writer().boxed() |
||||||
|
} else { |
||||||
|
fmt::layer().with_writer(stderr).boxed() |
||||||
|
}; |
||||||
|
|
||||||
let reg = registry() |
let reg = registry() |
||||||
.with(EnvFilter::from_default_env()) |
.with(EnvFilter::from_default_env()) |
||||||
.with(fmt::layer()); |
.with(output_layer); |
||||||
|
|
||||||
match logfile { |
match logfile { |
||||||
Some(file) => reg |
Some(file) => { |
||||||
.with( |
let file_path = Path::new(file).with_extension("log"); |
||||||
|
reg.with( |
||||||
fmt::layer() |
fmt::layer() |
||||||
.with_writer(move || { |
.with_writer(move || rolling::never("logs", &file_path)) |
||||||
rolling::never("logs", file.as_ref().with_extension("log")) |
|
||||||
}) |
|
||||||
.with_ansi(false), |
.with_ansi(false), |
||||||
) |
) |
||||||
.init(), |
.init() |
||||||
|
} |
||||||
None => reg.init(), |
None => reg.init(), |
||||||
}; |
}; |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue