- 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 { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||
import { AgentModel } from '../../models/agent.model'; |
||||
import { AgentModel } from '../../../models/agent.model'; |
||||
import { EventEmitter } from '@angular/core'; |
||||
|
||||
@Component({ |
||||
selector: 'agent-info-dialog', |
||||
templateUrl: 'agent-info-dialog.html', |
||||
styleUrls: ['info-dialog.component.less'] |
||||
templateUrl: 'agent-info-dialog.component.html', |
||||
styleUrls: ['../base-info-dialog.component.less'] |
||||
}) |
||||
export class AgentInfoDialogComponent { |
||||
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 { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||
import { ResultModel } from '../../models/result.model'; |
||||
import { ResultModel } from '../../../models/result.model'; |
||||
|
||||
@Component({ |
||||
selector: 'result-info-dialog', |
||||
templateUrl: 'result-info-dialog.html', |
||||
styleUrls: ['info-dialog.component.less'] |
||||
templateUrl: 'result-info-dialog.component.html', |
||||
styleUrls: ['../base-info-dialog.component.less'] |
||||
}) |
||||
export class ResultInfoDialogComponent { |
||||
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, |
||||
hostname: 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, |
||||
alias: string, |
||||
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::io::{stderr, stdout}; |
||||
use std::path::Path; |
||||
|
||||
use tracing_appender::rolling; |
||||
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() { |
||||
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() |
||||
.with(EnvFilter::from_default_env()) |
||||
.with(fmt::layer()); |
||||
.with(output_layer); |
||||
|
||||
match logfile { |
||||
Some(file) => reg |
||||
.with( |
||||
Some(file) => { |
||||
let file_path = Path::new(file).with_extension("log"); |
||||
reg.with( |
||||
fmt::layer() |
||||
.with_writer(move || { |
||||
rolling::never("logs", file.as_ref().with_extension("log")) |
||||
}) |
||||
.with_writer(move || rolling::never("logs", &file_path)) |
||||
.with_ansi(false), |
||||
) |
||||
.init(), |
||||
.init() |
||||
} |
||||
None => reg.init(), |
||||
}; |
||||
} |
||||
|
Loading…
Reference in new issue