Compare commits
11 Commits
7e267b0074
...
c8ce2aca60
Author | SHA1 | Date |
---|---|---|
root | c8ce2aca60 | 1 year ago |
plazmoid | e2ba50d947 | 1 year ago |
plazmoid | 886d4833fb | 1 year ago |
plazmoid | 69b1d3d901 | 2 years ago |
plazmoid | 45bba0dd9b | 2 years ago |
plazmoid | 32e96476cf | 2 years ago |
plazmoid | a38e3a1561 | 2 years ago |
plazmoid | f5d2190fc4 | 2 years ago |
plazmoid | 1b0cdae404 | 2 years ago |
plazmoid | d0d7d0aca5 | 2 years ago |
plazmoid | ce708d0c98 | 2 years ago |
125 changed files with 3623 additions and 2177 deletions
@ -1,6 +1,15 @@ |
||||
[build] |
||||
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/.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 { 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,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 { 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,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, |
||||
hostname: 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, |
||||
alias: string, |
||||
created: UTCDate, |
||||
id: string, |
||||
job_id: string, |
||||
result: number[], |
||||
result: number[] | null, |
||||
state: "Queued" | "Running" | "Finished", |
||||
retcode: number | null, |
||||
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 mkdir -p /tests && chmod 777 /tests |
||||
|
@ -1,3 +1,3 @@ |
||||
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; |
||||
|
||||
#[rstest] |
||||
#[tokio::test] |
||||
async fn non_auth_connection_dropped() { |
||||
async fn non_auth_connection_dropped(env_default: EndpointsEnv) { |
||||
let client = reqwest::ClientBuilder::new() |
||||
.danger_accept_invalid_certs(true) |
||||
.build() |
||||
.unwrap(); |
||||
match client |
||||
.get(format!("https://{}:{}", &ENV.u_server, MASTER_PORT)) |
||||
.get(format!("https://{}:{}", &env_default.u_server, MASTER_PORT)) |
||||
.send() |
||||
.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 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