Categories |
diff --git a/src/app/scenario/scenario-wizard/scenario-wizard.component.ts b/src/app/scenario/scenario-wizard/scenario-wizard.component.ts
index 56066e8f..0b678ff5 100644
--- a/src/app/scenario/scenario-wizard/scenario-wizard.component.ts
+++ b/src/app/scenario/scenario-wizard/scenario-wizard.component.ts
@@ -7,7 +7,7 @@ import {
ViewChild,
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
-import { ClrModal, ClrWizard } from '@clr/angular';
+import { ClrModal, ClrWizard, ClrWizardPage } from '@clr/angular';
import { RbacService } from 'src/app/data/rbac.service';
import { Scenario } from 'src/app/data/scenario';
import { ScenarioService } from 'src/app/data/scenario.service';
@@ -20,7 +20,11 @@ import { HttpErrorResponse } from '@angular/common/http';
import { CategoryFormGroup, ScenarioFormGroup } from 'src/app/data/forms';
import { AlertDetails } from 'src/app/alert/alert';
import { ClrAlertType } from 'src/app/clr-alert-type';
+import { VMTasks } from 'src/app/data/vm-tasks';
+interface ValidationError {
+ message: string
+}
@Component({
selector: 'scenario-wizard',
templateUrl: './scenario-wizard.component.html',
@@ -52,6 +56,9 @@ export class ScenarioWizardComponent implements OnInit {
public selectedscenario: Scenario;
+ public disableFinalizeButton: boolean = false;
+ public validationErrors: ValidationError[] = [];
+
get keepaliveRequired() {
const ka = this.scenarioDetails.controls.keepalive_amount;
const ku = this.scenarioDetails.controls.keepalive_unit;
@@ -70,6 +77,8 @@ export class ScenarioWizardComponent implements OnInit {
@ViewChild('createvmmodal', { static: true }) createVMModal: ClrModal;
@ViewChild('stepsscenario', { static: true })
stepScenario: StepsScenarioComponent;
+ @ViewChild('taskPage') taskWizardPage: ClrWizardPage;
+ @ViewChild('finalizePage') finalizeWizardPage: ClrWizardPage;
constructor(
public scenarioService: ScenarioService,
@@ -146,6 +155,59 @@ export class ScenarioWizardComponent implements OnInit {
});
}
});
+
+ this.wizard.currentPageChanged.subscribe(() => {
+ this.validationErrors = []
+ if (this.selectedscenario.vm_tasks.length > 0) {
+ this.validateTaskDefinitions()
+ }
+ this.disableFinalizeButton = !(this.validationErrors.length == 0)
+ });
+ }
+
+ validateTaskDefinitions() {
+ this.taskWizardPage.hasError = false
+ this.validateVMsExist();
+ this.validateNoDuplicateNames();
+ this.validateAllFieldsHaveValues()
+ }
+
+
+ private validateVMsExist() {
+ const vmNames = this.selectedscenario.virtualmachines.map(vm => Object.keys(vm)).reduce((acc, val) => acc.concat(val), []);
+ const taskVmNames = this.selectedscenario.vm_tasks.map(vmTask => vmTask.vm_name);
+ taskVmNames.forEach((name) => {
+ if (!vmNames.includes(name)) {
+ this.taskWizardPage.hasError = true;
+ this.validationErrors.push({ message: "One or more Tasks reference a Virtual Machnine Name, that does not exist" });
+ }
+ });
+ }
+
+ private validateNoDuplicateNames() {
+ this.selectedscenario.vm_tasks.forEach(vmTask => {
+ let tmpArr = [];
+ vmTask.tasks.forEach(task => {
+ const taskName = task.name;
+ if (tmpArr.indexOf(taskName) < 0) {
+ tmpArr.push(taskName);
+ } else {
+ this.taskWizardPage.hasError = true;
+ this.validationErrors.push({ message: "Task " + vmTask.vm_name + ": " + taskName + " is a Duplicate" });
+ }
+ });
+ });
+ }
+
+ private validateAllFieldsHaveValues() {
+ this.selectedscenario.vm_tasks.forEach(vmTask => {
+ vmTask.tasks.forEach((task) => {
+ if (task.name.length == 0 || task.command.length == 0 || task.description.length == 0 || task.return_type.length == 0) {
+ this.taskWizardPage.hasError = true;
+ this.validationErrors.push({ message: "Task " + vmTask.vm_name + ": " + task.name + " is missing required information" });
+ }
+ })
+ })
}
open(wizardMode: 'create' | 'edit', scenario?: Scenario) {
@@ -305,6 +367,7 @@ export class ScenarioWizardComponent implements OnInit {
addVMSet() {
this.selectedscenario.virtualmachines.push({});
+ this.updateSelectedScenarioRef();
}
addVM() {
@@ -312,20 +375,24 @@ export class ScenarioWizardComponent implements OnInit {
this.vmform.controls.vm_name.value
] = this.vmform.controls.vm_template.value;
this.createVMModal.close();
+ this.updateSelectedScenarioRef();
}
deleteVMSet(i: number) {
this.deletingVMSetIndex = i;
this.deleteVMSetModal.open();
+ this.updateSelectedScenarioRef();
}
public deleteVM(setIndex: number, key: string) {
delete this.selectedscenario.virtualmachines[setIndex][key];
+ this.updateSelectedScenarioRef();
}
doDeleteVMSet() {
this.selectedscenario.virtualmachines.splice(this.deletingVMSetIndex, 1);
this.deleteVMSetModal.close();
+ this.updateSelectedScenarioRef();
}
public openCreateVM(i: number) {
@@ -402,5 +469,14 @@ export class ScenarioWizardComponent implements OnInit {
this.selectedscenario.steps = [];
this.selectedscenario.virtualmachines[0] = {};
this.selectedscenario.categories = [];
+ this.selectedscenario.vm_tasks = [];
+ }
+
+ updateSelectedScenarioRef() {
+ this.selectedscenario = { ...this.selectedscenario }; // Force an Update on Child Components using the selectedscenario as Input
+ }
+
+ replaceVmTasks(vmTasks: VMTasks[]) {
+ this.selectedscenario.vm_tasks = vmTasks;
}
}
diff --git a/src/app/scenario/task-form/task-form.component.html b/src/app/scenario/task-form/task-form.component.html
new file mode 100644
index 00000000..386d74e1
--- /dev/null
+++ b/src/app/scenario/task-form/task-form.component.html
@@ -0,0 +1,113 @@
+
diff --git a/src/app/scenario/task-form/task-form.component.scss b/src/app/scenario/task-form/task-form.component.scss
new file mode 100644
index 00000000..01c34a29
--- /dev/null
+++ b/src/app/scenario/task-form/task-form.component.scss
@@ -0,0 +1,11 @@
+.codeInput ::ng-deep .clr-input-wrapper {
+ max-height: fit-content;
+}
+
+clr-input-container .clr-input {
+ width: 90%;
+}
+
+clr-textarea-container .clr-textarea {
+ width: 90%;
+}
\ No newline at end of file
diff --git a/src/app/scenario/task-form/task-form.component.ts b/src/app/scenario/task-form/task-form.component.ts
new file mode 100644
index 00000000..090a3065
--- /dev/null
+++ b/src/app/scenario/task-form/task-form.component.ts
@@ -0,0 +1,111 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormGroup, FormControl, Validators } from '@angular/forms';
+import { ReturnType, Task } from 'src/app/data/vm-tasks';
+import { supportedLanguages } from 'src/app/configuration/code-with-syntax-highlighting/code-with-syntax-highlighting.component';
+
+export interface EditTask extends Task {
+ id: string;
+ vmName: string;
+}
+
+@Component({
+ selector: 'app-task-form',
+ templateUrl: './task-form.component.html',
+ styleUrls: ['./task-form.component.scss'],
+})
+export class TaskFormComponent implements OnInit {
+ @Input() virtualMachineNames: string[];
+
+ @Input() editTask: EditTask;
+
+ @Output() taskChanged = new EventEmitter();
+
+ supportedLanguages = supportedLanguages;
+
+ returnTypes = Object.values(ReturnType);
+
+ private previousReturnType: string = 'Return Text';
+
+ taskForm: FormGroup;
+
+ ngOnInit() {
+ this.taskForm = new FormGroup({
+ taskNode: new FormControl(this.editTask.vmName, [Validators.required]),
+ taskName: new FormControl(this.editTask.name, [Validators.required]),
+ taskDescription: new FormControl(this.editTask.description, [
+ Validators.required,
+ ]),
+ taskCommand: new FormControl(this.editTask.command, [
+ Validators.required,
+ ]),
+ taskExpectedOutput: new FormControl(this.editTask.expected_output_value, []),
+ taskExpectedReurncode: new FormControl(
+ this.editTask.expected_return_code,
+ []
+ ),
+ taskReturnType: new FormControl(ReturnType[this.editTask.return_type], [
+ Validators.required,
+ ]),
+ });
+ this.taskForm.valueChanges.subscribe(() => {
+ this.taskChanged.emit(this.buildEditTaskFromFormData());
+ this.taskForm.updateValueAndValidity({ emitEvent: false });
+ });
+ this.previousReturnType =
+ ReturnType[
+ this.editTask.return_type as unknown as keyof typeof ReturnType
+ ];
+ this.taskForm.controls.taskReturnType.valueChanges.subscribe((newValue) => {
+ if (
+ this.previousReturnType == 'Match Regex' &&
+ newValue !== 'Match Regex'
+ ) {
+ this.taskForm.controls.taskExpectedOutput.setValue('');
+ }
+ if (
+ this.previousReturnType !== 'Match Regex' &&
+ newValue == 'Match Regex'
+ ) {
+ this.taskForm.controls.taskExpectedOutput.setValue('');
+ }
+ if (newValue == 'Return Code') {
+ this.taskForm.controls.taskExpectedOutput.setValue('');
+ }
+ this.previousReturnType = newValue;
+ });
+ }
+
+ private buildEditTaskFromFormData(): EditTask {
+ const rTypeString = this.taskForm.controls.taskReturnType.value;
+ const rTypeKey =
+ Object.keys(ReturnType).find((key) => ReturnType[key] == rTypeString) ??
+ 'Return_Text';
+ const expectedReturnCode =
+ this.taskForm.controls.taskExpectedReurncode.value == ''
+ ? 0
+ : this.taskForm.controls.taskExpectedReurncode.value;
+ return {
+ id: this.editTask.id,
+ vmName: this.taskForm.controls.taskNode.value,
+ name: this.taskForm.controls.taskName.value,
+ description: this.taskForm.controls.taskDescription.value,
+ command: this.taskForm.controls.taskCommand.value,
+ expected_output_value: this.taskForm.controls.taskExpectedOutput.value,
+ expected_return_code: expectedReturnCode,
+ return_type: rTypeKey,
+ } as unknown as EditTask;
+ }
+
+ isOfReturnType(returnTypes: string[]): boolean {
+ return returnTypes.includes(this.taskForm.controls.taskReturnType.value);
+ }
+
+ commandOutput(command) {
+ this.editTask.command = command;
+ this.taskForm.controls.taskCommand.setValue(command);
+ }
+
+ regexOutput(regex) {
+ this.taskForm.controls.taskExpectedOutput.setValue(regex);
+ }
+}
diff --git a/src/app/scenario/task/readonly-task/readonly-task.component.html b/src/app/scenario/task/readonly-task/readonly-task.component.html
new file mode 100644
index 00000000..0b82718f
--- /dev/null
+++ b/src/app/scenario/task/readonly-task/readonly-task.component.html
@@ -0,0 +1,94 @@
+
+
+
+ Node
+
+
+ {{ editTask.vmName }}
+
+
+
+
+
+
+
+ Description
+
+
+ {{ editTask.description }}
+
+
+
+
+
+
+
+ Return Type
+
+
+ {{ translateReturnType(editTask.return_type) }}
+
+
+
+
+ Expected Output
+
+
+ {{ editTask.expected_output_value }}
+
+
+
+
+
+
+
+ Return Code
+
+
+ {{ editTask.expected_return_code }}
+
+
+
diff --git a/src/app/scenario/task/readonly-task/readonly-task.component.scss b/src/app/scenario/task/readonly-task/readonly-task.component.scss
new file mode 100644
index 00000000..bbb842dc
--- /dev/null
+++ b/src/app/scenario/task/readonly-task/readonly-task.component.scss
@@ -0,0 +1,5 @@
+.container {
+ margin-bottom: 1rem;
+ margin-left: 1.5rem;
+ // width: 80%;
+}
\ No newline at end of file
diff --git a/src/app/scenario/task/readonly-task/readonly-task.component.ts b/src/app/scenario/task/readonly-task/readonly-task.component.ts
new file mode 100644
index 00000000..d8d6e661
--- /dev/null
+++ b/src/app/scenario/task/readonly-task/readonly-task.component.ts
@@ -0,0 +1,29 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormGroup, FormControl, Validators } from '@angular/forms';
+import { ReturnType, Task } from 'src/app/data/vm-tasks';
+import { supportedLanguages } from 'src/app/configuration/code-with-syntax-highlighting/code-with-syntax-highlighting.component';
+
+export interface EditTask extends Task {
+ id: string;
+ vmName: string;
+}
+
+@Component({
+ selector: 'app-readonly-task',
+ templateUrl: './readonly-task.component.html',
+ styleUrls: ['./readonly-task.component.scss'],
+})
+export class ReadonlyTaskComponent {
+
+ @Input() editTask: EditTask;
+
+ supportedLanguages = supportedLanguages;
+
+ translateReturnType(rtype: string) {
+ return ReturnType[rtype]
+ }
+
+ isOfReturnType(returnTypes: string[]): boolean {
+ return returnTypes.includes(this.translateReturnType(this.editTask.return_type));
+ }
+}
diff --git a/src/app/scenario/task/task.component.html b/src/app/scenario/task/task.component.html
new file mode 100644
index 00000000..12baa93d
--- /dev/null
+++ b/src/app/scenario/task/task.component.html
@@ -0,0 +1,45 @@
+
+
+ Tasks
+
+
+
+
+
+
+
+
+ {{ task.vmName }}: {{ task.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/scenario/task/task.component.scss b/src/app/scenario/task/task.component.scss
new file mode 100644
index 00000000..d0a91a0e
--- /dev/null
+++ b/src/app/scenario/task/task.component.scss
@@ -0,0 +1,29 @@
+#codeInput ::ng-deep .clr-input-wrapper {
+ max-height: fit-content;
+}
+
+clr-input-container .clr-input {
+ width: 90%;
+}
+
+clr-textarea-container .clr-textarea {
+ width: 90%;
+}
+
+.split-grid {
+ width: 100%;
+ display: inline-grid;
+ grid-template-columns: 50% auto;
+}
+
+.add-task-btn {
+ align-self: end;
+ justify-self: end;
+ padding: 0;
+}
+
+.remove-task-btn {
+ justify-self: end;
+ padding: 0;
+ margin: 0;
+}
\ No newline at end of file
diff --git a/src/app/scenario/task/task.component.ts b/src/app/scenario/task/task.component.ts
new file mode 100644
index 00000000..0de14e51
--- /dev/null
+++ b/src/app/scenario/task/task.component.ts
@@ -0,0 +1,129 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Scenario } from 'src/app/data/scenario';
+import { Task, VMTasks } from 'src/app/data/vm-tasks';
+import { EditTask } from '../task-form/task-form.component';
+
+@Component({
+ selector: 'app-task',
+ templateUrl: './task.component.html',
+ styleUrls: ['./task.component.scss'],
+})
+export class TaskComponent {
+ @Input() set selectedScenario(scenario: Scenario) {
+ this.editTasks = [];
+ this.changedTasks = [];
+ this.vmTasks = scenario.vm_tasks ?? [];
+ this.vmNames = scenario.virtualmachines
+ .map((vmSet) => Object.keys(vmSet))
+ .reduce((acc, curr) => acc.concat(curr), []); //Can be replaced with flatMap once we switch to es2019 or higher
+
+ this.vmTasks.forEach((vmTask) => {
+ vmTask.tasks.forEach((task: Task) => {
+ let editTask = {
+ vmName: vmTask.vm_name,
+ id: this.getRandomId(),
+ ...task,
+ };
+ this.changedTasks.push(editTask);
+ this.editTasks.push(editTask);
+ });
+ });
+ }
+
+ @Input() readonly = false
+
+ editTasks: EditTask[] = [];
+
+ changedTasks: EditTask[] = [];
+
+ vmNames: string[] = [];
+
+ vmTasks: VMTasks[] = [];
+
+ @Output() tasksChanged = new EventEmitter();
+
+ buildEditedVMTasks() {
+ let updatedVMTasks: VMTasks[] = [];
+ this.changedTasks.forEach((editTask) => {
+ let vmTask = updatedVMTasks.find((vmTask) => {
+ return vmTask.vm_name == editTask.vmName;
+ });
+ if (vmTask) {
+ vmTask.tasks.push(this.buildTask(editTask));
+ } else {
+ updatedVMTasks.push({
+ vm_name: editTask.vmName,
+ tasks: [this.buildTask(editTask)],
+ });
+ }
+ });
+ this.vmTasks = updatedVMTasks;
+ this.tasksChanged.emit(this.vmTasks);
+ }
+
+ buildTask(editTask: EditTask = null): Task {
+ return {
+ name: editTask?.name ?? '',
+ description: editTask?.description ?? '',
+ command: editTask?.command ?? '',
+ expected_output_value: editTask?.expected_output_value ?? '',
+ expected_return_code: editTask?.expected_return_code ?? '',
+ return_type: editTask?.return_type ?? 'Return_Text',
+ } as Task;
+ }
+
+ updateTask(changedTask: EditTask) {
+ let idExists = this.changedTasks
+ .map((task) => task.id)
+ .includes(changedTask.id);
+ if (!idExists) {
+ this.changedTasks.push(changedTask);
+ return;
+ }
+ this.changedTasks = this.changedTasks.map((task) =>
+ task.id !== changedTask.id ? task : changedTask
+ );
+ this.buildEditedVMTasks();
+ }
+
+ addTask() {
+ let newTask = {
+ vmName: this.vmNames[0] ?? '',
+ id: this.getRandomId(),
+ ...this.buildTask(),
+ } as EditTask;
+ this.editTasks.push(newTask);
+ this.changedTasks.push(newTask);
+ this.buildEditedVMTasks();
+ }
+
+ deleteTask(id: string) {
+ this.editTasks = this.editTasks.filter((task: EditTask) => {
+ return task.id !== id;
+ });
+ this.changedTasks = this.changedTasks.filter((task: EditTask) => {
+ return task.id !== id;
+ });
+ this.buildEditedVMTasks();
+ }
+
+ applyChanges() {
+ this.changedTasks.forEach((chTask: EditTask) => {
+ const alreadyExists = this.editTasks
+ .map((task) => task.id)
+ .includes(chTask.id);
+ if (alreadyExists) {
+ this.editTasks = this.editTasks.map((eTask: EditTask) => {
+ return eTask.id == chTask.id ? chTask : eTask;
+ });
+ } else {
+ this.editTasks.push(chTask);
+ }
+ });
+ this.buildEditedVMTasks();
+ }
+
+ getRandomId(): string {
+ return Math.random().toString(36).substring(2);
+ }
+}
diff --git a/src/app/step/hf-markdown.component.ts b/src/app/step/hf-markdown.component.ts
index 93fe6ead..f6b8f839 100644
--- a/src/app/step/hf-markdown.component.ts
+++ b/src/app/step/hf-markdown.component.ts
@@ -143,6 +143,14 @@ ${token}`;
// Return a placeholder with the unique ID
return `Loading mermaid graph... `;
},
+
+ verifyTask(code: string, target: string, taskName: string) {
+ return ``;
+ },
};
private renderHighlightedCode(
diff --git a/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.html b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.html
new file mode 100644
index 00000000..2abb6d8e
--- /dev/null
+++ b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.html
@@ -0,0 +1,45 @@
+
+
+
+
+ Verify: {{ taskName }} on {{ target }}
+
+
+
+
+
+ -
+
Description: {{ task.description }}
+ Command: {{ task.command }}
+
+ -
+
Expected Output: {{ task.expected_output_value }}
+ Actual Output: ''
+
+ -
+
Expected Returncode: {{ task.expected_return_code }}
+ Actual Returncode: ''
+
+ -
+
Success: true/false
+
+
+
diff --git a/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.scss b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.scss
new file mode 100644
index 00000000..4881b2c3
--- /dev/null
+++ b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.scss
@@ -0,0 +1,74 @@
+.task-verification-box {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border: 0;
+ border-left: 3px solid;
+ border-radius: 5px;
+ }
+
+ .down {
+ transform: rotate(180deg);
+ }
+
+ .sideways {
+ transform: rotate(90deg);
+ }
+
+ .flex-container {
+ display: flex !important;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+
+ .label-container {
+ display: flex !important;
+ flex-direction: row;
+ justify-content: space-between;
+ width: fit-content;
+ }
+
+ .label {
+ margin: auto;
+ margin-left: 4px;
+ margin-bottom: 7%;
+ }
+ .label:hover {
+ cursor: default;
+ }
+
+ #refreshButton:hover {
+ cursor: pointer !important;
+ }
+
+ .list-group {
+ width: 100%;
+ }
+
+ .greenBorder {
+ border-left-color: var(--clr-color-success-500, green) !important;
+ }
+
+ .redBorder {
+ border-left-color: #f0ad4e !important;
+ }
+
+ details {
+ border: 1px solid var(--clr-color-neutral-400, #cccccc);
+ border-left: 3px solid;
+ border-radius: 4px;
+ padding: 0.5em 0.5em 0;
+ &[open] {
+ padding: 0.5em;
+ summary {
+ margin-bottom: 0.5em;
+ }
+ }
+ }
+
+ summary {
+ font-weight: bold;
+ margin: -0.5em -0.5em 0;
+ padding: 0.5em;
+ display: list-item;
+ cursor: pointer;
+ }
\ No newline at end of file
diff --git a/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.ts b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.ts
new file mode 100644
index 00000000..ea5866cf
--- /dev/null
+++ b/src/app/step/single-task-verification-markdown/single-task-verification-markdown.component.ts
@@ -0,0 +1,51 @@
+import {
+ animate,
+ state,
+ style,
+ transition,
+ trigger,
+} from '@angular/animations';
+import { Component, Input } from '@angular/core';
+import { ReturnType, Task } from '../../data/vm-tasks';
+
+@Component({
+ selector: 'app-single-task-verification-markdown',
+ templateUrl: './single-task-verification-markdown.component.html',
+ animations: [
+ trigger('rotatedState', [
+ state('default', style({ transform: 'rotate(0)' })),
+ state('rotating', style({ transform: 'rotate(360deg)' })),
+ transition('default => rotating', animate('1500ms')),
+ ]),
+ ],
+ styleUrls: ['./single-task-verification-markdown.component.scss'],
+})
+export class SingleTaskVerificationMarkdownComponent {
+ @Input() target: string;
+ @Input() message: string;
+ @Input() taskName: string;
+
+ detailsOpen = false;
+
+ rotationState = 'default';
+
+ task: Task = {
+ name: 'Placeholder Name',
+ description: 'Placeholder Description',
+ command: 'Placeholder command',
+ expected_output_value: 'Expected Output',
+ expected_return_code: 0,
+ return_type: ReturnType.Return_Code_And_Text,
+ };
+
+ isOfReturnType(task: Task, returnTypes: string[]): boolean {
+ return returnTypes.includes(task.return_type);
+ }
+
+ elementClicked() {
+ this.rotationState = 'rotating';
+ setTimeout(() => {
+ this.rotationState = 'default';
+ }, 1500);
+ }
+}
|