commit
c8ce2aca60
125 changed files with 3623 additions and 2177 deletions
@ -1,6 +1,15 @@ |
|||||||
[build] |
[build] |
||||||
rustflags = [ |
rustflags = [ |
||||||
"-L", "/home/ortem/src/rust/unki/static/lib", |
"-L/usr/lib/musl/lib", |
||||||
|
"-L/home/ortem/src/rust/unki/static/lib", |
||||||
"--remap-path-prefix=/home/ortem/src/rust/unki=src", |
"--remap-path-prefix=/home/ortem/src/rust/unki=src", |
||||||
"--remap-path-prefix=/home/ortem/.cargo=cargo" |
"--remap-path-prefix=/home/ortem/.cargo=cargo" |
||||||
] |
] |
||||||
|
target = "x86_64-unknown-linux-musl" |
||||||
|
|
||||||
|
[env] |
||||||
|
STATIC_PREFIX = "static" |
||||||
|
PQ_LIB_STATIC_X86_64_UNKNOWN_LINUX_MUSL = "true" |
||||||
|
PG_CONFIG_X86_64_UNKNOWN_LINUX_GNU = { value = "static/bin/pg_config", relative = true } |
||||||
|
OPENSSL_STATIC = "true" |
||||||
|
OPENSSL_DIR = { value = "static", relative = true } |
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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,36 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { AssignedJobByIdModel } from 'src/app/models'; |
||||||
|
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 assigned_jobs: AssignedJobByIdModel[] = this.selected_rows.map(row => { |
||||||
|
const job_id = row.split(' ', 1)[0]; |
||||||
|
return { |
||||||
|
job_id: job_id, |
||||||
|
agent_id: this.agent_id |
||||||
|
} |
||||||
|
}); |
||||||
|
this.dataSource.createResult(assigned_jobs).subscribe(_ => { |
||||||
|
alert("Created") |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -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,48 @@ |
|||||||
|
<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.meta.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Alias</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.alias"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Args</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.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.meta.exec_type"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Platform</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.target_platforms"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Schedule</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="data.meta.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.meta.payload_id"> |
||||||
|
<mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option> |
||||||
|
</mat-select> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<payload-overview *ngIf="data.payload" [preview]="true" [payload]="data.payload.data"></payload-overview> |
||||||
|
</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,28 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { EventEmitter } from '@angular/core'; |
||||||
|
import { Job, JobModel } from '../../../models/job.model'; |
||||||
|
import { ApiTableService } from 'src/app/services'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'job-info-dialog', |
||||||
|
templateUrl: 'job-info-dialog.component.html', |
||||||
|
styleUrls: ['../base-info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class JobInfoDialogComponent { |
||||||
|
//[id, name]
|
||||||
|
isPreview = true; |
||||||
|
allPayloads: [string | null, string][] = [[null, "none"]]; |
||||||
|
|
||||||
|
onSave = new EventEmitter<JobModel>(); |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) { |
||||||
|
dataSource.getPayloads().subscribe(resp => { |
||||||
|
this.allPayloads = this.allPayloads.concat(resp.map(r => [r.id, r.name])) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
updateJob() { |
||||||
|
this.onSave.emit(this.data.meta); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
<h2 mat-dialog-title>New payload</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
||||||
|
<mat-label>Name</mat-label> |
||||||
|
<input matInput [(ngModel)]="payload.name"> |
||||||
|
</mat-form-field> |
||||||
|
<input type="file" class="file-input" (change)="onFileSelected($event)" #fileUpload> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<mat-form-field class="info-dlg-field" *ngIf="!uploadMode"> |
||||||
|
<mat-label>Data</mat-label> |
||||||
|
<textarea matInput [(ngModel)]="decodedPayload"></textarea> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-raised-button (click)="save()">Save</button> |
||||||
|
<button mat-button mat-dialog-close>Close</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,43 @@ |
|||||||
|
import { Component, EventEmitter, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { NewPayloadModel } from 'src/app/models/payload.model'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'new-payload-dialog', |
||||||
|
templateUrl: 'new-payload-dialog.component.html', |
||||||
|
styleUrls: ['../base-info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class NewPayloadDialogComponent { |
||||||
|
decodedPayload = ""; |
||||||
|
uploadMode = false; |
||||||
|
onSave = new EventEmitter<NewPayloadModel>(); |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public payload: NewPayloadModel) { } |
||||||
|
|
||||||
|
save() { |
||||||
|
if (this.payload.data.length == 0) { |
||||||
|
this.payload.data = Array.from(new TextEncoder().encode(this.decodedPayload)); |
||||||
|
} |
||||||
|
this.onSave.emit(this.payload); |
||||||
|
} |
||||||
|
|
||||||
|
onFileSelected(event: any) { |
||||||
|
const file: File = event.target.files[0]; |
||||||
|
if (file) { |
||||||
|
this.uploadMode = true |
||||||
|
const reader = new FileReader(); |
||||||
|
reader.onload = e => { |
||||||
|
this.payload.name = file.name; |
||||||
|
const result = e.target?.result; |
||||||
|
if (result instanceof ArrayBuffer) { |
||||||
|
const d = Array.from(new Uint8Array(result)); |
||||||
|
this.payload.data = d; |
||||||
|
console.log(this.payload.data) |
||||||
|
} else { |
||||||
|
alert!("no file") |
||||||
|
} |
||||||
|
} |
||||||
|
reader.readAsArrayBuffer(file) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
<h2 mat-dialog-title *ngIf="isPreview">Payload</h2> |
||||||
|
<h2 mat-dialog-title *ngIf="!isPreview">Editing payload</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
||||||
|
<mat-label>ID</mat-label> |
||||||
|
<input matInput disabled value="{{payload.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Name</mat-label> |
||||||
|
<input matInput [readonly]="isPreview" [(ngModel)]="payload.name"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>MIME-type</mat-label> |
||||||
|
<input matInput disabled value="{{payload.mime_type}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Size</mat-label> |
||||||
|
<input matInput disabled value="{{payload.size}}"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<payload-overview [preview]="isPreview" [payload]="payload.data"></payload-overview> |
||||||
|
</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)="updatePayload()">Save</button> |
||||||
|
<button mat-button mat-dialog-close>Close</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,20 @@ |
|||||||
|
import { Component, EventEmitter, 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: ['../base-info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class PayloadInfoDialogComponent { |
||||||
|
isPreview = true; |
||||||
|
|
||||||
|
onSave = new EventEmitter<PayloadModel>(); |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public payload: PayloadModel) { } |
||||||
|
|
||||||
|
updatePayload() { |
||||||
|
this.onSave.emit(this.payload); |
||||||
|
} |
||||||
|
} |
@ -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,6 @@ |
|||||||
|
<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> |
@ -0,0 +1,21 @@ |
|||||||
|
import { Component, Input, OnInit } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'payload-overview', |
||||||
|
templateUrl: './payload-overview.component.html', |
||||||
|
styleUrls: ['./payload-overview.component.less'] |
||||||
|
}) |
||||||
|
export class PayloadOverviewComponent implements OnInit { |
||||||
|
@Input() payload: number[] | null = null; |
||||||
|
@Input("preview") isPreview = true; |
||||||
|
isTooBigPayload = false; |
||||||
|
decodedPayload = ""; |
||||||
|
|
||||||
|
ngOnInit() { |
||||||
|
if (this.payload !== null) { |
||||||
|
this.decodedPayload = new TextDecoder().decode(new Uint8Array(this.payload)) |
||||||
|
} else { |
||||||
|
this.isTooBigPayload = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TableComponent } 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 TableComponent<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,59 @@ |
|||||||
|
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 TableComponent<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_item = params['new'] |
||||||
|
if (id) { |
||||||
|
this.showItemDialog(id); |
||||||
|
} |
||||||
|
if (new_item) { |
||||||
|
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).subscribe(_ => { }) |
||||||
|
this.loadTableData() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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,72 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TableComponent } from '../base-table/base-table.component'; |
||||||
|
import { Area, JobModel, Job } from '../../../models'; |
||||||
|
import { JobInfoDialogComponent } from '../../dialogs'; |
||||||
|
import { Observable } from 'rxjs'; |
||||||
|
|
||||||
|
@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 TableComponent<JobModel> { |
||||||
|
area = 'jobs' as Area; |
||||||
|
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] |
||||||
|
|
||||||
|
showItemDialog(id: string | null) { |
||||||
|
const is_new_job = id === null; |
||||||
|
|
||||||
|
var dialogData$: Observable<Job>; |
||||||
|
|
||||||
|
if (is_new_job) { |
||||||
|
dialogData$ = new Observable(subscriber => { |
||||||
|
var defaultJob: Job = { |
||||||
|
meta: { |
||||||
|
alias: null, |
||||||
|
argv: '', |
||||||
|
exec_type: 'shell', |
||||||
|
target_platforms: '*', |
||||||
|
payload_id: null, |
||||||
|
schedule: null |
||||||
|
}, |
||||||
|
payload: null |
||||||
|
}; |
||||||
|
subscriber.next(defaultJob) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
dialogData$ = this.dataSource.getJob(id) |
||||||
|
} |
||||||
|
|
||||||
|
dialogData$.subscribe(dialogData => { |
||||||
|
const dialog = this.infoDialog.open(JobInfoDialogComponent, { |
||||||
|
data: dialogData, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
dialog.componentInstance.isPreview = !is_new_job; |
||||||
|
|
||||||
|
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
||||||
|
if (is_new_job) { |
||||||
|
this.dataSource.create(dialogData.meta, this.area) |
||||||
|
.subscribe(_ => { |
||||||
|
alert("Created") |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
} else { |
||||||
|
this.dataSource.updateJob(result) |
||||||
|
.subscribe(_ => { |
||||||
|
alert("Updated") |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
} |
||||||
|
dialog.close() |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(result => { |
||||||
|
saveSub.unsubscribe() |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
<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> |
||||||
|
<button id="new_btn" mat-raised-button color="primary" routerLink='.' [queryParams]="{new: true}">Add |
||||||
|
payload</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,67 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
import { Area } from 'src/app/models'; |
||||||
|
import { NewPayloadModel, PayloadModel } from 'src/app/models/payload.model'; |
||||||
|
import { PayloadInfoDialogComponent } from '../../dialogs'; |
||||||
|
import { NewPayloadDialogComponent } from '../../dialogs/new-payload-dialog/new-payload-dialog.component'; |
||||||
|
import { TableComponent } 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 TableComponent<PayloadModel> { |
||||||
|
area = 'payloads' as Area |
||||||
|
displayedColumns = ["name", "mime_type", "size", 'actions']; |
||||||
|
|
||||||
|
showItemDialog(id: string | null) { |
||||||
|
if (id === null) { |
||||||
|
const payload: NewPayloadModel = { |
||||||
|
name: "", |
||||||
|
data: [] |
||||||
|
} |
||||||
|
const dialog = this.infoDialog.open(NewPayloadDialogComponent, { |
||||||
|
data: payload, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
dialog.componentInstance.onSave.subscribe(result => { |
||||||
|
this.dataSource.createPayload(result) |
||||||
|
.subscribe(_ => { |
||||||
|
alert("Created") |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.close() |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(_ => { |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
|
||||||
|
} else { |
||||||
|
this.dataSource.getPayload(id as string).subscribe(resp => { |
||||||
|
const dialog = this.infoDialog.open(PayloadInfoDialogComponent, { |
||||||
|
data: resp, |
||||||
|
width: '1000px', |
||||||
|
}); |
||||||
|
|
||||||
|
const saveSub = dialog.componentInstance.onSave.subscribe(result => { |
||||||
|
this.dataSource.updatePayload(result) |
||||||
|
.subscribe(_ => { |
||||||
|
alert("Updated") |
||||||
|
this.loadTableData() |
||||||
|
}) |
||||||
|
dialog.close() |
||||||
|
}) |
||||||
|
|
||||||
|
dialog.afterClosed().subscribe(_ => { |
||||||
|
saveSub.unsubscribe() |
||||||
|
this.router.navigate(['.'], { relativeTo: this.route }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import { Component, OnInit } from '@angular/core'; |
||||||
|
import { TableComponent } 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 TableComponent<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,20 @@ |
|||||||
|
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 { } |
@ -0,0 +1,16 @@ |
|||||||
|
import { PayloadModel } from './' |
||||||
|
|
||||||
|
export interface JobModel { |
||||||
|
alias: string | null, |
||||||
|
argv: string, |
||||||
|
id?: string, |
||||||
|
exec_type: string, |
||||||
|
target_platforms: string, |
||||||
|
payload_id: string | null, |
||||||
|
schedule: string | null, |
||||||
|
} |
||||||
|
|
||||||
|
export interface Job { |
||||||
|
meta: JobModel, |
||||||
|
payload: PayloadModel | null, |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
export interface PayloadModel { |
||||||
|
id: string, |
||||||
|
mime_type: string, |
||||||
|
name: string, |
||||||
|
size: number, |
||||||
|
data: number[] | null |
||||||
|
} |
||||||
|
|
||||||
|
export interface NewPayloadModel { |
||||||
|
name: string, |
||||||
|
data: number[] |
||||||
|
} |
@ -1,13 +1,18 @@ |
|||||||
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, |
||||||
id: string, |
id: string, |
||||||
job_id: string, |
job_id: string, |
||||||
result: number[], |
result: number[] | null, |
||||||
state: "Queued" | "Running" | "Finished", |
state: "Queued" | "Running" | "Finished", |
||||||
retcode: number | null, |
retcode: number | null, |
||||||
updated: UTCDate, |
updated: UTCDate, |
||||||
|
} |
||||||
|
|
||||||
|
export interface AssignedJobByIdModel { |
||||||
|
job_id: string, |
||||||
|
agent_id: string |
||||||
} |
} |
@ -0,0 +1,142 @@ |
|||||||
|
import { environment } from 'src/environments/environment'; |
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; |
||||||
|
import { Observable, map, catchError, throwError } from 'rxjs'; |
||||||
|
import { ApiModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job, NewPayloadModel, AssignedJobByIdModel } 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) |
||||||
|
} |
||||||
|
|
||||||
|
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<Job> { |
||||||
|
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<JobModel[]> { |
||||||
|
return this.getMany('jobs') |
||||||
|
} |
||||||
|
|
||||||
|
getResults(): Observable<ResultModel[]> { |
||||||
|
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<T extends ApiModel>(item: T | null, area: Area): Observable<string[]> { |
||||||
|
var serialized = '"{}"' |
||||||
|
if (item) { |
||||||
|
serialized = JSON.stringify(item); |
||||||
|
} |
||||||
|
return this.filterErrStatus(this.req(`${area} create '${serialized}'`)) |
||||||
|
} |
||||||
|
|
||||||
|
createResult(item: AssignedJobByIdModel[]): Observable<string[]> { |
||||||
|
return this.create(item, 'map') |
||||||
|
} |
||||||
|
|
||||||
|
createPayload(item: NewPayloadModel): Observable<string[]> { |
||||||
|
return this.create(item, 'payloads') |
||||||
|
} |
||||||
|
|
||||||
|
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(err: HttpErrorResponse, caught: any) { |
||||||
|
var error = err.error.data !== undefined ? JSON.stringify(err.error.data) : err.message; |
||||||
|
this.errorService.handle(error); |
||||||
|
return throwError(() => new Error()); |
||||||
|
} |
||||||
|
} |
@ -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('') |
||||||
|
} |
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
FROM rust:1.67 |
FROM rust:1.72 |
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl |
RUN rustup target add x86_64-unknown-linux-musl |
||||||
RUN mkdir -p /tests && chmod 777 /tests |
RUN mkdir -p /tests && chmod 777 /tests |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
FROM alpine:3.17 |
FROM alpine:3.17 |
||||||
|
|
||||||
RUN apk add iproute2 bash |
RUN apk add iproute2 bash file |
@ -0,0 +1,38 @@ |
|||||||
|
use super::connections::*; |
||||||
|
use super::run_async; |
||||||
|
use u_lib::{api::HttpClient, messaging::Reportable, models::*, types::Id}; |
||||||
|
|
||||||
|
pub struct RegisteredAgent { |
||||||
|
pub id: Id, |
||||||
|
} |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
#[once] |
||||||
|
pub fn registered_agent(client: &HttpClient) -> RegisteredAgent { |
||||||
|
run_async(async { |
||||||
|
let agent = Agent::with_current_platform(); |
||||||
|
let agent_id = agent.id; |
||||||
|
println!("registering agent {agent_id}"); |
||||||
|
debug!("registering agent1 {agent_id}"); |
||||||
|
let resp = client |
||||||
|
.get_personal_jobs(agent_id) |
||||||
|
.await |
||||||
|
.unwrap() |
||||||
|
.pop() |
||||||
|
.unwrap(); |
||||||
|
let job_id = resp.job_id; |
||||||
|
let job = client.get_job(job_id, Brief::No).await.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(job.meta.alias, Some("agent_hello".to_string())); |
||||||
|
|
||||||
|
let mut agent_data = AssignedJob::from((&job.meta, resp)); |
||||||
|
agent_data.set_result(&agent); |
||||||
|
|
||||||
|
client |
||||||
|
.report([Reportable::Assigned(agent_data)]) |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
RegisteredAgent { id: agent_id } |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
use super::env::*; |
||||||
|
use super::run_async; |
||||||
|
|
||||||
|
pub use u_lib::api::HttpClient; |
||||||
|
use u_lib::db::unpooled; |
||||||
|
pub use u_lib::db::PgConnection; |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
#[once] |
||||||
|
pub fn client(env_default: EndpointsEnv) -> HttpClient { |
||||||
|
run_async(HttpClient::new(&env_default.u_server, None)).unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
#[once] |
||||||
|
pub fn client_panel(env_access: AccessEnv) -> HttpClient { |
||||||
|
run_async(HttpClient::new( |
||||||
|
&env_access.u_server, |
||||||
|
Some(env_access.admin_auth_token), |
||||||
|
)) |
||||||
|
.unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
#[once] |
||||||
|
pub fn db(env_db: DBEnv) -> PgConnection { |
||||||
|
unpooled(&env_db) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
pub use u_lib::config::{AccessEnv, DBEnv, EndpointsEnv}; |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
pub fn env_default() -> EndpointsEnv { |
||||||
|
EndpointsEnv::load() |
||||||
|
} |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
pub fn env_access() -> AccessEnv { |
||||||
|
AccessEnv::load().unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
#[fixture] |
||||||
|
pub fn env_db() -> DBEnv { |
||||||
|
DBEnv::load().unwrap() |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
pub mod agent; |
||||||
|
pub mod connections; |
||||||
|
pub mod env; |
||||||
|
|
||||||
|
use std::future::Future; |
||||||
|
use std::thread; |
||||||
|
use tokio::runtime::Runtime; |
||||||
|
|
||||||
|
// tokio runtime cannot be created inside another runtime,
|
||||||
|
// so i create a separate non-'static thread not to interfere
|
||||||
|
fn run_async<R: Send>(fut: impl Future<Output = R> + Send) -> R { |
||||||
|
thread::scope(|s| { |
||||||
|
s.spawn(|| Runtime::new().unwrap().block_on(fut)) |
||||||
|
.join() |
||||||
|
.expect("async task failed") |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
pub mod jobs; |
||||||
|
pub mod panel; |
||||||
|
|
||||||
|
pub use panel::Panel; |
@ -1,14 +1,15 @@ |
|||||||
use crate::helpers::ENV; |
use crate::fixtures::env::*; |
||||||
use u_lib::config::MASTER_PORT; |
use u_lib::config::MASTER_PORT; |
||||||
|
|
||||||
|
#[rstest] |
||||||
#[tokio::test] |
#[tokio::test] |
||||||
async fn non_auth_connection_dropped() { |
async fn non_auth_connection_dropped(env_default: EndpointsEnv) { |
||||||
let client = reqwest::ClientBuilder::new() |
let client = reqwest::ClientBuilder::new() |
||||||
.danger_accept_invalid_certs(true) |
.danger_accept_invalid_certs(true) |
||||||
.build() |
.build() |
||||||
.unwrap(); |
.unwrap(); |
||||||
match client |
match client |
||||||
.get(format!("https://{}:{}", &ENV.u_server, MASTER_PORT)) |
.get(format!("https://{}:{}", &env_default.u_server, MASTER_PORT)) |
||||||
.send() |
.send() |
||||||
.await |
.await |
||||||
{ |
{ |
@ -0,0 +1,97 @@ |
|||||||
|
// get_personal_jobs(&self, url_param: Id)
|
||||||
|
// report(&self, payload: impl OneOrVec<messaging::Reportable>)
|
||||||
|
// dl(&self, file: String)
|
||||||
|
// get_job(&self, job: Id)
|
||||||
|
// get_jobs(&self)
|
||||||
|
// get_agents(&self, agent: Option<Id>)
|
||||||
|
// update_agent(&self, agent: Agent)
|
||||||
|
// update_job(&self, job: FatJob)
|
||||||
|
// update_result(&self, result: AssignedJob)
|
||||||
|
// upload_jobs(&self, payload: impl OneOrVec<FatJob>)
|
||||||
|
// del(&self, item: Id)
|
||||||
|
// assign_jobs(&self, agent: Id, job_idents: impl OneOrVec<String>)
|
||||||
|
// get_agent_jobs(&self, agent: Option<Id>)
|
||||||
|
// ping(&self)
|
||||||
|
|
||||||
|
use crate::fixtures::connections::*; |
||||||
|
use std::iter::repeat; |
||||||
|
use u_lib::models::{Brief, RawJob, RawPayload, MAX_READABLE_PAYLOAD_SIZE}; |
||||||
|
|
||||||
|
#[rstest] |
||||||
|
#[tokio::test] |
||||||
|
async fn jobs_upload_update_get_del(client_panel: &HttpClient) { |
||||||
|
let job_alias = "henlo"; |
||||||
|
let mut job = RawJob::default() |
||||||
|
.with_shell("/bin/bash {}") |
||||||
|
.with_raw_payload("echo henlo") |
||||||
|
.with_alias(job_alias) |
||||||
|
.try_into_job() |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let job_id = job.meta.id; |
||||||
|
|
||||||
|
client_panel.upload_jobs([&job]).await.unwrap(); |
||||||
|
|
||||||
|
let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); |
||||||
|
assert_eq!(job, fetched_job); |
||||||
|
|
||||||
|
let new_alias = "henlo2".to_string(); |
||||||
|
job.meta.alias = Some(new_alias.clone()); |
||||||
|
client_panel.update_job(&job.meta).await.unwrap(); |
||||||
|
|
||||||
|
let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); |
||||||
|
assert_eq!( |
||||||
|
fetched_job.payload.as_ref().unwrap().data.as_ref().unwrap(), |
||||||
|
b"echo henlo" |
||||||
|
); |
||||||
|
assert_eq!(fetched_job.meta.alias, Some(new_alias)); |
||||||
|
|
||||||
|
client_panel.del(job_id).await.unwrap(); |
||||||
|
|
||||||
|
let not_found_err = client_panel.get_brief_job(job_id).await.unwrap_err(); |
||||||
|
assert!(not_found_err.to_string().contains("404 Not Found")) |
||||||
|
} |
||||||
|
|
||||||
|
#[rstest] |
||||||
|
#[tokio::test] |
||||||
|
async fn payloads_upload_update_get_del(client_panel: &HttpClient) { |
||||||
|
let name = "test1".to_string(); |
||||||
|
let data = b"qweasdzxc".to_vec(); |
||||||
|
let payload = RawPayload { |
||||||
|
name: Some(name.clone()), |
||||||
|
data: data.clone(), |
||||||
|
}; |
||||||
|
|
||||||
|
client_panel.upload_payload(&payload).await.unwrap(); |
||||||
|
|
||||||
|
let mut fetched_payload = client_panel.get_payload(&name, Brief::No).await.unwrap(); |
||||||
|
let fetched_payload_auto = client_panel.get_payload(&name, Brief::Auto).await.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(fetched_payload, fetched_payload_auto); |
||||||
|
assert_eq!(fetched_payload.data.unwrap(), data); |
||||||
|
|
||||||
|
let new_size = MAX_READABLE_PAYLOAD_SIZE + 1; |
||||||
|
let big_data = repeat(1u8).take(new_size as usize).collect::<Vec<_>>(); |
||||||
|
|
||||||
|
fetched_payload.data = Some(big_data.clone()); |
||||||
|
client_panel.update_payload(&fetched_payload).await.unwrap(); |
||||||
|
|
||||||
|
let fetched_big_payload = client_panel.get_payload(&name, Brief::Yes).await.unwrap(); |
||||||
|
let fetched_big_payload_auto = client_panel.get_payload(&name, Brief::Auto).await.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(fetched_big_payload, fetched_big_payload_auto); |
||||||
|
assert_eq!(fetched_big_payload.size, new_size); |
||||||
|
assert!(fetched_big_payload.data.is_none()); |
||||||
|
|
||||||
|
let fetched_big_payload_full = client_panel.get_payload(&name, Brief::No).await.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(fetched_big_payload_full.data.unwrap(), big_data); |
||||||
|
|
||||||
|
client_panel.del(fetched_big_payload_full.id).await.unwrap(); |
||||||
|
|
||||||
|
let not_found_err = client_panel |
||||||
|
.get_payload(&name, Brief::Yes) |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(not_found_err.to_string().contains("404 Not Found")) |
||||||
|
} |
@ -1,2 +1,3 @@ |
|||||||
mod behaviour; |
mod behaviour; |
||||||
mod connection; |
mod connection; |
||||||
|
mod endpoints; |
@ -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,36 +0,0 @@ |
|||||||
use crate::helpers::ENV; |
|
||||||
use u_lib::{ |
|
||||||
api::ClientHandler, config::get_self_id, jobs::fat_meta_to_thin, messaging::Reportable, |
|
||||||
models::*, types::Id, |
|
||||||
}; |
|
||||||
|
|
||||||
pub struct RegisteredAgent { |
|
||||||
pub id: Id, |
|
||||||
} |
|
||||||
|
|
||||||
impl RegisteredAgent { |
|
||||||
pub async fn unregister(self) { |
|
||||||
let cli = ClientHandler::new(&ENV.u_server, None).await.unwrap(); |
|
||||||
cli.del(self.id).await.unwrap(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[fixture] |
|
||||||
pub async fn register_agent() -> RegisteredAgent { |
|
||||||
let cli = ClientHandler::new(&ENV.u_server, None).await.unwrap(); |
|
||||||
let agent_id = get_self_id(); |
|
||||||
println!("registering agent {agent_id}"); |
|
||||||
let resp = cli |
|
||||||
.get_personal_jobs(agent_id) |
|
||||||
.await |
|
||||||
.unwrap() |
|
||||||
.pop() |
|
||||||
.unwrap(); |
|
||||||
let job_id = resp.job_id; |
|
||||||
let job = cli.get_job(job_id).await.unwrap(); |
|
||||||
assert_eq!(job.alias, Some("agent_hello".to_string())); |
|
||||||
let mut agent_data = AssignedJob::from((&fat_meta_to_thin(job).unwrap(), resp)); |
|
||||||
agent_data.set_result(&Agent::with_id(agent_id)); |
|
||||||
cli.report(Reportable::Assigned(agent_data)).await.unwrap(); |
|
||||||
RegisteredAgent { id: agent_id } |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
pub mod agent; |
|
@ -1,9 +0,0 @@ |
|||||||
pub mod jobs; |
|
||||||
pub mod panel; |
|
||||||
|
|
||||||
pub use panel::Panel; |
|
||||||
|
|
||||||
use once_cell::sync::Lazy; |
|
||||||
use u_lib::config::EndpointsEnv; |
|
||||||
|
|
||||||
pub static ENV: Lazy<EndpointsEnv> = Lazy::new(|| EndpointsEnv::load()); |
|
@ -1,6 +0,0 @@ |
|||||||
mod fixtures; |
|
||||||
mod helpers; |
|
||||||
mod integration; |
|
||||||
|
|
||||||
#[macro_use] |
|
||||||
extern crate rstest; |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue