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