diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 09ce80b4..782fdc3a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -14,11 +14,11 @@ import { PrintableComponent } from './printable/printable.component'; import { VmtemplatesComponent } from './configuration/vmtemplates/vmtemplates.component'; import { DashboardsComponent } from './dashboards/dashboards.component'; import { StepComponent } from './step/step-component/step.component'; -import { TerminalComponent } from './step/terminal/terminal.component'; import { RolesComponent } from './configuration/roles/roles/roles.component'; import { SessionStatisticsComponent } from './session-statistics/session-statistics.component'; import { SettingsComponent } from './configuration/settings/settings.component'; import { DashboardDetailsComponent } from './dashboards/dashboard-details/dashboard-details.component'; +import { TerminalViewComponent } from './step/terminal/terminal-view.component'; const routes: Routes = [ { path: '', redirectTo: '/home', pathMatch: 'full' }, @@ -101,7 +101,7 @@ const routes: Routes = [ path: 'session/:session/steps/:step', component: StepComponent, }, - { path: 'terminal', component: TerminalComponent }, + { path: 'terminal/:vmId/:wsEndpoint', component: TerminalViewComponent, canActivate: [AuthGuard] }, { path: 'scenario/:scenario/printable', component: PrintableComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b48058a9..b6768e55 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -55,6 +55,7 @@ import { ProgressDashboardComponent } from './dashboards/progress-dashboard/prog import { DashboardsComponent } from './dashboards/dashboards.component'; import { VmDashboardComponent } from './dashboards/vm-dashboard/vm-dashboard.component'; import { UsersDashboardComponent } from './dashboards/users-dashboard/users-dashboard.component'; +import { SharedVmDashboardComponent } from './dashboards/shared-vm-dashboard/shared-vm-dashboard.component'; import { StepComponent } from './step/step-component/step.component'; import { CtrService } from './data/ctr.service'; import { SessionService } from './data/session.service'; @@ -159,6 +160,7 @@ import { downloadIcon, plusCircleIcon, exclamationTriangleIcon, + refreshIcon } from '@cds/core/icon'; import { ReadonlyTaskComponent } from './scenario/task/readonly-task/readonly-task.component'; import { HiddenMdComponent } from './step/hidden-md-component/hidden-md.component'; @@ -172,6 +174,8 @@ import { TooltipComponent } from './tooltip/tooltip.component'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { AuthnService } from './data/authn.service'; import { SessionProgressService } from './progress/session-progress.service'; +import { TerminalViewComponent } from './step/terminal/terminal-view.component'; +import { WebinterfaceWindowComponent } from './step/terminal/webinterface-window/webinterface-window.component'; ClarityIcons.addIcons( plusIcon, @@ -214,6 +218,7 @@ ClarityIcons.addIcons( downloadIcon, plusCircleIcon, exclamationTriangleIcon, + refreshIcon ); const appInitializerFn = (appConfig: AppConfigService) => { @@ -222,6 +227,19 @@ const appInitializerFn = (appConfig: AppConfigService) => { }; }; + +//TODO: Check if this still works after resolving all merge conflicts considering Angular 17 upgrade! +export const jwtAllowedDomains = [ + environment.server.replace(/(^\w+:|^)\/\//, ''), +]; + +export function addJwtAllowedDomain(domain: string) { + const newDomain = domain.replace(/(^\w+:|^)\/\//, ''); + if (!jwtAllowedDomains.includes(newDomain)) { + jwtAllowedDomains.push(newDomain); + } +} + export function jwtOptionsFactory(): JwtConfig { const allowedDomainsRegex = environment.server.match(/.*\:\/\/?([^\/]+)/); let allowedDomains: string[] | undefined; @@ -279,9 +297,11 @@ export function jwtOptionsFactory(): JwtConfig { DashboardsComponent, VmDashboardComponent, UsersDashboardComponent, + SharedVmDashboardComponent, StepComponent, HfMarkdownComponent, TerminalComponent, + TerminalViewComponent, CtrComponent, RbacDirective, ClarityDisableSelectionDirective, @@ -318,6 +338,7 @@ export function jwtOptionsFactory(): JwtConfig { TaskFormComponent, ReadonlyTaskComponent, SingleTaskVerificationMarkdownComponent, + WebinterfaceWindowComponent, GlossaryMdComponent, HiddenMdComponent, MermaidMdComponent, diff --git a/src/app/configuration/vmtemplates/vmtemplates.component.html b/src/app/configuration/vmtemplates/vmtemplates.component.html index e78c45b2..5731e042 100644 --- a/src/app/configuration/vmtemplates/vmtemplates.component.html +++ b/src/app/configuration/vmtemplates/vmtemplates.component.html @@ -75,3 +75,4 @@

VM Templates

#deleteConfirmation (delete)="doDelete()" > + \ No newline at end of file diff --git a/src/app/dashboards/dashboard-details/dashboard-details.component.html b/src/app/dashboards/dashboard-details/dashboard-details.component.html index 47baaf36..05d46ff5 100644 --- a/src/app/dashboards/dashboard-details/dashboard-details.component.html +++ b/src/app/dashboards/dashboard-details/dashboard-details.component.html @@ -8,6 +8,8 @@

VMs for {{ selectedEvent.event_name }}

Users participating in {{ selectedEvent.event_name }}

} @else if (statisticsDashboardActive && rbacSuccessSessions) {

Statistics for {{ selectedEvent.event_name }}

+ } @else if (sharedVmDashboardActive && rbacSuccessVms) { +

VMs for {{ selectedEvent.event_name }}

} @@ -50,6 +52,18 @@

Statistics for {{ selectedEvent.event_name }}

} + @if (rbacSuccessVms) { + + + + +
+ +
+
+
+
+ } @if (rbacSuccessSessions) { @@ -74,7 +88,7 @@

Statistics for {{ selectedEvent.event_name }}

- } + } } diff --git a/src/app/dashboards/dashboard-details/dashboard-details.component.ts b/src/app/dashboards/dashboard-details/dashboard-details.component.ts index ec5aa847..0319b8cd 100644 --- a/src/app/dashboards/dashboard-details/dashboard-details.component.ts +++ b/src/app/dashboards/dashboard-details/dashboard-details.component.ts @@ -17,6 +17,7 @@ export class DashboardDetailsComponent implements OnInit, OnDestroy { public statisticsDashboardActive: boolean = false; public usersDashboardActive: boolean = false; + public sharedVmDashboardActive: boolean = false; public selectedEvent?: DashboardScheduledEvent; public loggedInAdminEmail: string; diff --git a/src/app/dashboards/dashboards.component.ts b/src/app/dashboards/dashboards.component.ts index 2fc02e1f..da3d85bd 100644 --- a/src/app/dashboards/dashboards.component.ts +++ b/src/app/dashboards/dashboards.component.ts @@ -6,7 +6,7 @@ import { UserService } from '../data/user.service'; import { RbacService } from '../data/rbac.service'; import { ProgressCount } from '../data/progress'; import { ProgressService } from '../data/progress.service'; -import { VmService } from '../data/vm.service'; +import { AdminVmService } from '../data/admin-vm.service'; import { Router } from '@angular/router'; @Component({ @@ -33,7 +33,7 @@ export class DashboardsComponent implements OnInit, OnDestroy { private userService: UserService, private rbacService: RbacService, private progressService: ProgressService, - private vmService: VmService, + private vmService: AdminVmService, private router: Router ) {} diff --git a/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.html b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.html new file mode 100644 index 00000000..07544fd0 --- /dev/null +++ b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.html @@ -0,0 +1,100 @@ +
+ @if (vmSets.length < 1) { +
No active VMs
+ } + + @for(set of vmSets; track $index) { +
+ + + Environment: {{ set.environment }}   Count: {{ set.count }} + + +
+
+ + Access Terminal + Status + Name + IP + VM-Template + + VM Id + Hostname + + + + + @if (vm.status === 'running' && !vm.tainted) { + {{ vm.status }} + } + @if (vm.status !== 'running' && !vm.tainted) { + {{ vm.status }} + } + @if (!!vm.tainted) { + tainted + } + + {{ vm.name }} + {{ + vm.public_ip + }} + {{ + vm.vm_template_id + }} + {{ vm.id }} + {{ + vm.hostname + }} + + +
+
+
+
+
+ } +
+
diff --git a/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.scss b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.scss new file mode 100644 index 00000000..18ccbfec --- /dev/null +++ b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.scss @@ -0,0 +1,12 @@ +.dashboardCell { + display: flex; + align-items: center; +} + +.joinButton { + justify-content: center; +} + +.badge{ + padding: 10px; +} \ No newline at end of file diff --git a/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.ts b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.ts new file mode 100644 index 00000000..0d46dc01 --- /dev/null +++ b/src/app/dashboards/shared-vm-dashboard/shared-vm-dashboard.component.ts @@ -0,0 +1,138 @@ +import { ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { ScheduledEvent } from 'src/app/data/scheduledevent'; +import { + VirtualMachine, + VirtualMachineTypeShared, +} from 'src/app/data/virtualmachine'; +import { AdminVmService } from 'src/app/data/admin-vm.service'; +import { VmSet } from 'src/app/data/vmset'; + +interface DashboardVm extends VirtualMachine { + name?: string; +} + +interface dashboardVmSet extends VmSet { + setVMs: DashboardVm[]; + stepOpen: boolean; + dynamic: boolean; +} + +@Component({ + selector: 'shared-vm-dashboard', + templateUrl: './shared-vm-dashboard.component.html', + styleUrls: ['./shared-vm-dashboard.component.scss'], +}) +export class SharedVmDashboardComponent implements OnChanges { + @Input() + selectedEvent: ScheduledEvent; + + constructor( + public vmService: AdminVmService, + private router: Router, + private cd: ChangeDetectorRef + ) {} + + public vms: VirtualMachine[] = []; + public vmSets: dashboardVmSet[] = []; + + public selectedVM: VirtualMachine = new VirtualMachine(); + public openPanels: Set = new Set(); + + ngOnChanges() { + this.getVmList(); + } + + setStepOpen(set) { + this.openPanels.has(set.base_name) + ? this.openPanels.delete(set.base_name) + : this.openPanels.add(set.base_name); + } + + getVmList() { + this.vmService + .listByScheduledEvent(this.selectedEvent.id) + .subscribe((vmList) => { + this.vms = vmList + .filter((vm) => vm.vm_type == VirtualMachineTypeShared) + .map((vm) => ({ + ...vm, + })); + if (this.vms.length > 0) { + this.loadVmsFromScheduledEvent(); + } + this.cd.detectChanges(); + }); + } + + // Used to load either dynamic or shared virtualMachines for an event + private loadVmsFromScheduledEvent() { + // dynamic machines have no associated vmSet + if (this.vms.length > 0) { + let groupedVms: Map = this.groupByEnvironment( + this.vms + ); + // (shared) vms grouped by environment + groupedVms.forEach((element, environment) => { + element.forEach((vm) => this.setVmName(vm)); + let vmSet: dashboardVmSet = { + ...new VmSet(), + base_name: environment, + stepOpen: this.openPanels.has(environment), + dynamic: false, + setVMs: element + }; + vmSet.count = element.length; + vmSet.available = element.filter((vm) => vm.status == 'running').length; + vmSet.environment = environment; + this.vmSets.push(vmSet); + }); + } + } + + setVmName(vm: DashboardVm) { + vm.name = + this.selectedEvent.shared_vms.find((sVM) => sVM.vm_id == vm.id)?.name ?? + ''; + } + + openTerminal(vm: DashboardVm) { + //build url with params, then use router to navigate to it + if (!vm.name) this.setVmName(vm) + const queryParams = { + vmName: vm.name, + vmId: vm.id, + wsEndpoint: vm.ws_endpoint, + }; + + const url = this.router.createUrlTree( + ['/terminal', vm.id, vm.ws_endpoint], + { queryParams } + ); + const serializedUrl = this.router.serializeUrl(url); + + window.open(serializedUrl, '_blank'); + return; + + // const url = this.router.serializeUrl( + // this.router.createUrlTree(['/terminal', vm.id, vm.ws_endpoint]) + // ); + // window.open(url, '_blank'); + // return; + } + + groupByEnvironment(vms: VirtualMachine[]) { + let envMap = new Map(); + vms.forEach((element) => { + if (envMap.has(element.environment_id)) { + let envVms = envMap.get(element.environment_id)!; + envVms.push(element); + envMap.set(element.environment_id, envVms); + } else { + let envVms: VirtualMachine[] = [element]; + envMap.set(element.environment_id, envVms); + } + }); + return envMap; + } +} diff --git a/src/app/dashboards/vm-dashboard/vm-dashboard.component.html b/src/app/dashboards/vm-dashboard/vm-dashboard.component.html index 85dec10a..363ad140 100644 --- a/src/app/dashboards/vm-dashboard/vm-dashboard.component.html +++ b/src/app/dashboards/vm-dashboard/vm-dashboard.component.html @@ -1,7 +1,7 @@
-
- +
+
@if (vmSets.length < 1) { @@ -74,9 +74,11 @@ - @if (vm.status === "running") { + @if (!!vm.tainted) { + tainted + } @else if (vm.status === "running") { {{ vm.status }} - } @else { + } @else if (vm.status !== 'running'){ {{ vm.status }} } diff --git a/src/app/dashboards/vm-dashboard/vm-dashboard.component.ts b/src/app/dashboards/vm-dashboard/vm-dashboard.component.ts index 93f59530..cdcf7b24 100644 --- a/src/app/dashboards/vm-dashboard/vm-dashboard.component.ts +++ b/src/app/dashboards/vm-dashboard/vm-dashboard.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { Router } from '@angular/router'; import { combineLatest } from 'rxjs'; import { Progress } from 'src/app/data/progress'; import { ProgressService } from 'src/app/data/progress.service'; import { ScheduledEventBase } from 'src/app/data/scheduledevent'; import { UserService } from 'src/app/data/user.service'; -import { VirtualMachine } from 'src/app/data/virtualmachine'; -import { VmService } from 'src/app/data/vm.service'; +import { VirtualMachine, VirtualMachineTypeShared } from 'src/app/data/virtualmachine'; +import { AdminVmService } from 'src/app/data/admin-vm.service'; import { VmSet } from 'src/app/data/vmset'; import { VmSetService } from 'src/app/data/vmset.service'; @@ -21,12 +21,12 @@ interface dashboardVmSet extends VmSet { templateUrl: './vm-dashboard.component.html', styleUrls: ['./vm-dashboard.component.scss'], }) -export class VmDashboardComponent implements OnInit { +export class VmDashboardComponent implements OnChanges { @Input() selectedEvent: ScheduledEventBase; constructor( - public vmService: VmService, + public vmService: AdminVmService, public vmSetService: VmSetService, public userService: UserService, public progressService: ProgressService, @@ -40,10 +40,6 @@ export class VmDashboardComponent implements OnInit { public selectedVM: VirtualMachine = new VirtualMachine(); public openPanels: Set = new Set(); - ngOnInit(): void { - this.getVmList(); - } - ngOnChanges() { this.getVmList(); } @@ -54,54 +50,65 @@ export class VmDashboardComponent implements OnInit { : this.openPanels.add(set.base_name); } - getVmList() { - combineLatest([ - this.vmService.listByScheduledEvent(this.selectedEvent.id), - this.vmSetService.getVMSetByScheduledEvent(this.selectedEvent.id), - this.userService.list(), - ]).subscribe(([vmList, vmSet, users]) => { - const userMap = new Map(users.map((u) => [u.id, u.email])); - this.vms = vmList.map((vm) => ({ - ...vm, - user: userMap.get(vm.user) ?? '-', - })); + getVmList() { - this.vmSets = vmSet.map((set) => ({ - ...set, - setVMs: this.vms.filter((vm) => vm.vm_set_id === set.id), - stepOpen: this.openPanels.has(set.base_name), - dynamic: false, - available: this.vms.filter( - (vm) => vm.vm_set_id === set.id && vm.status == 'running' - ).length, - })); - // dynamic machines have no associated vmSet - if (this.vms.filter((vm) => vm.vm_set_id == '').length > 0) { - let groupedVms: Map = this.groupByEnvironment( - this.vms.filter((vm) => vm.vm_set_id == '') - ); - groupedVms.forEach((element, environment) => { - let vmSet: dashboardVmSet = { - ...new VmSet(), - base_name: environment, - stepOpen: this.openPanels.has(environment), - dynamic: true, - }; - vmSet.setVMs = element; - vmSet.count = element.length; - vmSet.available = element.filter( - (vm) => vm.status == 'running' - ).length; - vmSet.environment = environment; - this.vmSets.push(vmSet); - }); - } - this.cd.detectChanges(); //The async Code above updates values after Angulars usual change-detection so we call this Method to prevent Errors - }); + combineLatest([ + this.vmService.listByScheduledEvent(this.selectedEvent.id), + this.vmSetService.getVMSetByScheduledEvent(this.selectedEvent.id), + this.userService.list(), + ]).subscribe(([vmList, vmSet, users]) => { + const userMap = new Map(users.map((u) => [u.id, u.email])); + this.vms = vmList.filter(vm => vm.vm_type != VirtualMachineTypeShared ) + .map((vm) => ({ + ...vm, + user: userMap.get(vm.user) ?? '-', + })); + + this.vmSets = vmSet.map((set) => ({ + ...set, + setVMs: this.vms.filter((vm) => vm.vm_set_id === set.id), + stepOpen: this.openPanels.has(set.base_name), + dynamic: false, + available: this.vms.filter( + (vm) => vm.vm_set_id === set.id && vm.status == 'running' + ).length, + })); + // dynamic machines have no associated vmSet + if (this.vms.filter((vm) => vm.vm_set_id == '').length > 0) { + this.loadVmsFromScheduledEvent(true); + } + this.cd.detectChanges(); //The async Code above updates values after Angulars usual change-detection so we call this Method to prevent Errors + }); + } + + // Used to load either dynamic or shared virtualMachines for an event + private loadVmsFromScheduledEvent(dynamic: boolean) { + // dynamic machines have no associated vmSet + if (this.vms.filter((vm) => vm.vm_set_id == '').length > 0) { + // -> is always true for shared vms... + let groupedVms: Map = this.groupByEnvironment( + this.vms.filter((vm) => vm.vm_set_id == '') + ); + // (shared) vms grouped by environment + groupedVms.forEach((element, environment) => { + let vmSet: dashboardVmSet = { + ...new VmSet(), + base_name: environment, + stepOpen: this.openPanels.has(environment), + dynamic: dynamic, + }; + vmSet.setVMs = element; + vmSet.count = element.length; + vmSet.available = element.filter( + (vm) => vm.status == 'running' + ).length; + vmSet.environment = environment; + this.vmSets.push(vmSet); + }); + } } openUsersTerminal(vm: VirtualMachine) { - if (!vm.user) return; let userId: string | undefined; //get the Users ID who has the VM allocated to him this.userService.list().subscribe((users) => { userId = users.filter((user) => user.email === vm.user)[0]?.id; diff --git a/src/app/data/Session.ts b/src/app/data/Session.ts index f4dd9f6b..8d15515b 100644 --- a/src/app/data/Session.ts +++ b/src/app/data/Session.ts @@ -5,4 +5,5 @@ export class Session { keep_course_vm: boolean; user: string; vm_claim: string[]; + access_code: string; } diff --git a/src/app/data/admin-vm.service.ts b/src/app/data/admin-vm.service.ts new file mode 100644 index 00000000..fa76af22 --- /dev/null +++ b/src/app/data/admin-vm.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { catchError, switchMap } from 'rxjs/operators'; +import { ServerResponse } from './serverresponse'; +import { of, throwError } from 'rxjs'; +import { atou } from '../unicode'; +import { ResourceClient, GargantuaClientFactory } from './gargantua.service'; +import { VirtualMachine } from './virtualmachine'; + +@Injectable({ + providedIn: 'root', +}) +export class AdminVmService extends ResourceClient { + constructor(gcf: GargantuaClientFactory) { + super(gcf.scopedClient('/a/vm')); + } + + public list() { + return this.garg.get('/list').pipe( + catchError((e: HttpErrorResponse) => { + return throwError(() => e.error); + }), + switchMap((s: ServerResponse) => { + return of(JSON.parse(atou(s.content))); + }) + ); + } + public listByScheduledEvent(id: String) { + return this.garg + .get('/scheduledevent/' + id) + .pipe( + catchError((e: HttpErrorResponse) => { + return throwError(() => e.error); + }), + switchMap((s: ServerResponse) => { + return of(JSON.parse(atou(s.content))); + }) + ); + } + + public count() { + return this.garg.get('/count').pipe( + catchError((e: HttpErrorResponse) => { + return throwError(() => e.error); + }), + switchMap((s: ServerResponse) => { + return of(JSON.parse(atou(s.content))); + }) + ); + } +} diff --git a/src/app/data/scheduledevent.service.ts b/src/app/data/scheduledevent.service.ts index e37d62c1..85df17c4 100644 --- a/src/app/data/scheduledevent.service.ts +++ b/src/app/data/scheduledevent.service.ts @@ -63,6 +63,7 @@ export class ScheduledeventService extends ListableResourceClient { - return of(JSON.parse(atou(s.content))); - }), - ); - } - public listByScheduledEvent(id: String) { - return this.gargAdmin.get(`/scheduledevent/${id}`).pipe( - switchMap((s: ServerResponse) => { - return of(JSON.parse(atou(s.content))); - }), - ); - } - - public getVmById(id: number) { - return this.garg.get(`?${id}`).pipe( - switchMap((s: ServerResponse) => { - return of(JSON.parse(atou(s.content))); - }), - ); - } - - public count() { - return this.gargAdmin.get('/count').pipe( - switchMap((s: ServerResponse) => { - return of(JSON.parse(atou(s.content))); - }), - ); - } -} diff --git a/src/app/event/new-scheduled-event/new-scheduled-event.component.html b/src/app/event/new-scheduled-event/new-scheduled-event.component.html index 0585e65f..8aa3fd2a 100644 --- a/src/app/event/new-scheduled-event/new-scheduled-event.component.html +++ b/src/app/event/new-scheduled-event/new-scheduled-event.component.html @@ -5,7 +5,7 @@ (clrWizardOnFinish)="save()" (clrWizardOnCancel)="close()" class="customized" -> + > New Scheduled Event Cancel @@ -33,6 +33,9 @@ Select Virtual Machines + + Select Shared Virtual Machines + Finalize @@ -49,853 +52,965 @@ name="event_name" formControlName="event_name" required - /> + /> Event name is required - Event name must be longer than 4 characters - Event name must be unique - - - - - Event description is required - Event description must be longer than 4 characters - - - - - - Access code is required - Access code must be longer than 5 characters - Access code must -
    -
  • - contain only lowercase alphanumeric characters, '-', or '.' -
  • -
  • start and end with an alphanumber character
  • -
-
- Access code is already in use -
- - - - - - Restricted bind prevents users from reserving VM resources not - associated with this Scheduled Event. - - - - - - - - On demand allocates VM resources when requested by a user instead of - pre-provisioning. - - - - - - - - - Printable enables an option for users to print scenario content or - save it as PDF file. - - - - - + Event name must be longer than 4 characters + Event name must be unique + + + + + Event description is required + Event description must be longer than 4 characters + + + + + + Access code is required + Access code must be longer than 5 characters + Access code must +
    +
  • + contain only lowercase alphanumeric characters, '-', or '.' +
  • +
  • start and end with an alphanumber character
  • +
+
+ Access code is already in use +
+ + + + + + Restricted bind prevents users from reserving VM resources not + associated with this Scheduled Event. + + + + + + + + On demand allocates VM resources when requested by a user instead of + pre-provisioning. + + + + + + + + + Printable enables an option for users to print scenario content or + save it as PDF file. + + + +
+ - Event Times -
-
- Start Time: - {{ se.start_time ? (se.start_time | date: "long") : "" }}
- - - - - - -
-
-
- -
-
-
- End Time: {{ se.end_time ? (se.end_time | date: "long") : "" }} -
- - - - - - -
-
-
-
- -
+ Event Times +
+
+ Start Time: + {{ se.start_time ? (se.start_time | date: "long") : "" }}
+ + + + + + +
+
+
+ +
+
+
+ End Time: {{ se.end_time ? (se.end_time | date: "long") : "" }} +
+ + + + + + +
+
+ +
+ +
-
-
- - -
- -
- @if (quicksetRequired) { - Please specify when your event should end. - } @else if (quicksetEndtimeForm.errors?.invalidQuicksetAmount) { - Invalid event duration. - } @else if (quicksetEndtimeForm.errors?.invalidNumber) { - - Please enter an integral number. - - } -
-
- -

- All times are in {{ tz }} (browser detected) -

- @if (se.start_time && se.end_time && se.start_time >= se.end_time) { - - - - Start time must occur before end time! - - - - } - - - Select Course(s) - Expand a row to view scenarios within that course - - Id - Name - Description - - {{ c.id }} - {{ c.name }} - {{ c.description }} - - - - - - - - @for (s of c.scenarios; track s) { - - - - - } - -
ScenarioDescription
{{ s.name }}{{ s.description }}
-
-
-
-
- +
+
+ + +
+ +
+ @if (quicksetRequired) { + Please specify when your event should end. + } @else if (quicksetEndtimeForm.errors?.invalidQuicksetAmount) { + Invalid event duration. + } @else if (quicksetEndtimeForm.errors?.invalidNumber) { + + Please enter an integral number. + + } +
+
+ +

+ All times are in {{ tz }} (browser detected) +

+ @if (se.start_time && se.end_time && se.start_time >= se.end_time) { + + + + Start time must occur before end time! + + + + } +
+ + Select Course(s) + Expand a row to view scenarios within that course + + Id + Name + Description + + {{ c.id }} + {{ c.name }} + {{ c.description }} + + + + + + + + @for (s of c.scenarios; track s) { + + + + + } + +
ScenarioDescription
{{ s.name }}{{ s.description }}
+
+
+
+
+ - Select Scenario(s) - - - Id - Name - Description - - {{ s.id }} - {{ s.name }} - {{ s.description }} - - - @if (selectedcourses.length == 0 && selectedscenarios.length == 0) { - - - - You must select at least one course or scenario to proceed - - - - } - - + Select Scenario(s) + + + Id + Name + Description + + {{ s.id }} + {{ s.name }} + {{ s.description }} + + + @if (selectedcourses.length == 0 && selectedscenarios.length == 0) { + + + + You must select at least one course or scenario to proceed + + + + } + + - Select Environment(s) - @if (checkingEnvironments) { - Please wait... - } - @if (noEnvironmentsAvailable && noVirtualMachinesNeeded) { - VirtualMachines are not needed. Only scenarios/courses without - Machines were selected. - } @else { - No suitable environments found. - } - @if (unavailableVMTs.length > 0) { - - No suitable environments found for the following VM Templates: - - - - ID - - - - @for (vmt of unavailableVMTs; track vmt) { - - - {{ vmt }} - - - } - - } - @if ( - !checkingEnvironments && - !noEnvironmentsAvailable && - unavailableVMTs.length == 0 - ) { - - -  Environment - - -  Count - - - {{ ea.environment }} - - @for (item of ea.available_count | keyvalue; track item) { - {{ getVirtualMachineTemplateName(item.key) }} - {{ - item.value - }} - } - - - - } - - + Select Environment(s) + @if (checkingEnvironments) { + Please wait... + } + @if (noEnvironmentsAvailable && noVirtualMachinesNeeded) { + VirtualMachines are not needed. Only scenarios/courses without + Machines were selected. + } @else { + No suitable environments found. + } + @if (unavailableVMTs.length > 0) { + + No suitable environments found for the following VM Templates: + + + + ID + + + + @for (vmt of unavailableVMTs; track vmt) { + + + {{ vmt }} + + + } + + } + @if ( + !checkingEnvironments && + !noEnvironmentsAvailable && + unavailableVMTs.length == 0 + ) { + + +  Environment + + +  Count + + + {{ ea.environment }} + + @for (item of ea.available_count | keyvalue; track item) { + {{ getVirtualMachineTemplateName(item.key) }} + {{ + item.value + }} + } + + + + } + + - Select Virtual Machines - @if (noVirtualMachinesNeeded) { - VirtualMachines are not needed. Only scenarios/courses without Machines - were selected. - } @else { - - - - - @if (simpleMode) { - In simple mode, define the number of users per environment. Virtual - machines are calculated for you. - - - - - - - - - @for ( - fc of simpleModeVmCounts["controls"].envs["controls"].slice( - 0, - selectedEnvironments.length - ); - track fc; - let it = $index - ) { - @if (selectedEnvironments.length > it) { - - - - - } - } - - - - - -
EnvironmentUsers
- {{ - getEnvironmentName(selectedEnvironments[it].environment) - }} - - - - User count higher than max - - (max - {{ maxUserCounts[selectedEnvironments[it].environment] }}) -
- Total - {{ simpleUserTotal() }} users
-
- @if (invalidSimpleEnvironments.length != 0) { - The following environments are incompatible with simple mode: - - - -

Simple Mode Compatibility

-

- An environment must be able to support the creation of all - types of required VMs, as well as have enough capacity for at - least one user (and all their associated VMs). -

-
-
-
    - @for (s of invalidSimpleEnvironments; track s) { -
  • {{ s }}
  • - } -
- } - } @else { - In advanced mode, define the number of virtual machines per - environment. Remember to account for the number of expected users, - thus number_of_users * number_of_required_vms. - -

- The following VMs are required per user: - @for (item of requiredVmCounts | keyvalue; track item) { -

  • {{ item.key }}: {{ item.value }}
  • - } -

    - - - - - - - - - - @for (groupName of controls(""); track groupName) { - - @for (controlName of controls(groupName); track controlName) { - - - - - - } - - } - -
    EnvironmentVM TemplateCount
    {{ getEnvironmentName(groupName) }}{{ getVirtualMachineTemplateName(controlName) }} - - - VM count higher than max - -
    - } - } -
    - @if (!isEditMode) { - - Finalize -

    Confirm the following details before finishing:

    -

    Basic Information

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    OptionValue
    Name{{ se.event_name }}
    Description{{ se.description }}
    Access code{{ se.access_code }}
    Restricted Bind{{ !se.disable_restriction }}
    On demand{{ se.on_demand }}
    Start Time{{ se.start_time | date: "long" }}
    End Time{{ se.end_time | date: "long" }}
    Courses - @for (c of se.courses; track c) { - {{ c }} - } -
    Scenarios - @for (s of se.scenarios; track s) { - {{ s }} - } -
    -

    VM Information

    - @if (noVirtualMachinesNeeded) { - VirtualMachines are not needed for this ScheduledEvent. - } @else { - - - - - - - - - @for (i of se.required_vms | keyvalue; track i) { - - - - - } - -
    EnvironmentVirtual Machines
    {{ getEnvironmentName(i.key) }} - @for (q of i.value | keyvalue; track q) { - {{ getVirtualMachineTemplateName(q.key) }} - {{ - q.value - }} - } -
    - } -
    - } - @if (isEditMode) { - - Finalize -

    Confirm the following details before finishing:

    -

    Basic Information

    - - - - - - - - - - - @if (se.event_name == uneditedScheduledEvent.event_name) { - - } @else { - - } - - - - @if (se.description == uneditedScheduledEvent.description) { - - } @else { - - } - - - - @if (se.access_code == uneditedScheduledEvent.access_code) { - - } @else { - - } - - - - @if ( - se.disable_restriction == - uneditedScheduledEvent.disable_restriction - ) { - - } @else { - - } - - - - @if (se.on_demand == uneditedScheduledEvent.on_demand) { - - } @else { - - } - - - - @if (!isStartDateAsEditedCheck()) { - - } @else { - - } - - - - @if (!isEndDateAsEditedCheck()) { - - } @else { - - } - - - - - - - - - - -
    OptionValue
    Name - {{ se.event_name }} - - {{ - uneditedScheduledEvent.event_name - }} - {{ se.event_name }} -
    Description - {{ se.description }} - - {{ - uneditedScheduledEvent.description - }} - {{ se.description }} -
    Access code - {{ se.access_code }} - - {{ - uneditedScheduledEvent.access_code - }} - {{ se.access_code }} -
    Restricted Bind - {{ !se.disable_restriction }} - - {{ - !uneditedScheduledEvent.disable_restriction - }} - {{ !se.disable_restriction }} -
    On demand - {{ se.on_demand }} - - {{ - uneditedScheduledEvent.on_demand - }} - {{ se.on_demand }} -
    Start Time - {{ se.start_time | date: "long" }} - - {{ - uneditedScheduledEvent.start_time | date: "long" - }} - {{ - se.start_time | date: "long" - }} -
    End Time - {{ se.end_time | date: "long" }} - - {{ - uneditedScheduledEvent.end_time | date: "long" - }} - {{ se.end_time | date: "long" }} -
    Courses - @for (s of uneditedScheduledEvent.courses; track s) { - @if (isCourseInList(s, se.courses)) { - {{ s }} - } @else { - {{ s }} - - } - } - @for (s of se.courses; track s) { - @if (!isCourseInList(s, uneditedScheduledEvent.courses)) { - {{ s }} - - } - } -
    Scenarios - @for (s of uneditedScheduledEvent.scenarios; track s) { - @if (isScenarioInList(s, se.scenarios)) { - {{ s }} - } @else { - {{ s }} - } - } - @for (s of se.scenarios; track s) { - @if (!isScenarioInList(s, uneditedScheduledEvent.scenarios)) { - {{ s }} - } - } -
    - @if (!se.on_demand) { - - - VMs will be started right away - - - } -

    VM Information

    - @if (noVirtualMachinesNeeded) { - VirtualMachines are not needed for this ScheduledEvent. - } @else { - - - - - - - - - @for (i of se.required_vms | keyvalue; track i) { - - - - - } - -
    EnvironmentVirtual Machines
    {{ getEnvironmentName(i.key) }} - @for (q of i.value | keyvalue; track q) { - - {{ getVirtualMachineTemplateName(q.key) }}  - @if ( - getUneditedScheduledEventVMCount(i.key, q.key) == - q.value - ) { - - {{ q.value }} - - } @else { - - - {{ - getUneditedScheduledEventVMCount(i.key, q.key) - }} - - {{ q.value }} - - } - - } -
    - } -
    - } - } - + Select Virtual Machines + @if (noVirtualMachinesNeeded) { + VirtualMachines are not needed. Only scenarios/courses without Machines + were selected. + } @else { + + + + + @if (simpleMode) { + In simple mode, define the number of users per environment. Virtual + machines are calculated for you. + + + + + + + + + @for ( + fc of simpleModeVmCounts["controls"].envs["controls"].slice( + 0, + selectedEnvironments.length + ); + track fc; + let it = $index + ) { + @if (selectedEnvironments.length > it) { + + + + + } + } + + + + + +
    EnvironmentUsers
    + {{ + getEnvironmentName(selectedEnvironments[it].environment) + }} + + + + User count higher than max + + (max + {{ maxUserCounts[selectedEnvironments[it].environment] }}) +
    + Total + {{ simpleUserTotal() }} users
    +
    + @if (invalidSimpleEnvironments.length != 0) { + The following environments are incompatible with simple mode: + + + +

    Simple Mode Compatibility

    +

    + An environment must be able to support the creation of all + types of required VMs, as well as have enough capacity for at + least one user (and all their associated VMs). +

    +
    +
    +
      + @for (s of invalidSimpleEnvironments; track s) { +
    • {{ s }}
    • + } +
    + } + } @else { + In advanced mode, define the number of virtual machines per + environment. Remember to account for the number of expected users, + thus number_of_users * number_of_required_vms. + +

    + The following VMs are required per user: + @for (item of requiredVmCounts | keyvalue; track item) { +

  • {{ item.key }}: {{ item.value }}
  • + } +

    + + + + + + + + + + @for (groupName of controls(""); track groupName) { + + @for (controlName of controls(groupName); track controlName) { + + + + + + } + + } + +
    EnvironmentVM TemplateCount
    {{ getEnvironmentName(groupName) }}{{ getVirtualMachineTemplateName(controlName) }} + + + VM count higher than max + +
    + } + } + + + Select Shared Virtual Machines + +
    + + + + + + + + + + + + + + + + + +
    NameEnvironmentVirtual Machine Template
    + + + VM name is required + VM name must be longer than 4 characters + + VM name must be unique + + VM name must not contain whitespace + + + + + + + + + + VM Template not available in Environment + + +
    + +
    + +

    SharedVM Information

    + + + + + + + + + + + + + + @for (sharedVm of se.shared_vms; track sharedVm; let i = $index) { + + + + + + + } + +
    NameEnvironmentVirtual Machine Template
    {{ sharedVm.name }}{{ sharedVm.environment }}{{ sharedVm.vm_template }}
    +
    +
    + @if (!isEditMode) { + + Finalize +

    Confirm the following details before finishing:

    +

    Basic Information

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptionValue
    Name{{ se.event_name }}
    Description{{ se.description }}
    Access code{{ se.access_code }}
    Restricted Bind{{ !se.disable_restriction }}
    On demand{{ se.on_demand }}
    Start Time{{ se.start_time | date: "long" }}
    End Time{{ se.end_time | date: "long" }}
    Courses + @for (c of se.courses; track c) { + {{ c }} + } +
    Scenarios + @for (s of se.scenarios; track s) { + {{ s }} + } +
    +

    VM Information

    + @if (noVirtualMachinesNeeded) { + VirtualMachines are not needed for this ScheduledEvent. + } @else { + + + + + + + + + @for (i of se.required_vms | keyvalue; track i) { + + + + + } + +
    EnvironmentVirtual Machines
    {{ getEnvironmentName(i.key) }} + @for (q of i.value | keyvalue; track q) { + {{ getVirtualMachineTemplateName(q.key) }} + {{ + q.value + }} + } +
    + } +
    + } + @if (isEditMode) { + + Finalize +

    Confirm the following details before finishing:

    +

    Basic Information

    + + + + + + + + + + + @if (se.event_name == uneditedScheduledEvent.event_name) { + + } @else { + + } + + + + @if (se.description == uneditedScheduledEvent.description) { + + } @else { + + } + + + + @if (se.access_code == uneditedScheduledEvent.access_code) { + + } @else { + + } + + + + @if ( + se.disable_restriction == + uneditedScheduledEvent.disable_restriction + ) { + + } @else { + + } + + + + @if (se.on_demand == uneditedScheduledEvent.on_demand) { + + } @else { + + } + + + + @if (!isStartDateAsEditedCheck()) { + + } @else { + + } + + + + @if (!isEndDateAsEditedCheck()) { + + } @else { + + } + + + + + + + + + + +
    OptionValue
    Name + {{ se.event_name }} + + {{ + uneditedScheduledEvent.event_name + }} + {{ se.event_name }} +
    Description + {{ se.description }} + + {{ + uneditedScheduledEvent.description + }} + {{ se.description }} +
    Access code + {{ se.access_code }} + + {{ + uneditedScheduledEvent.access_code + }} + {{ se.access_code }} +
    Restricted Bind + {{ !se.disable_restriction }} + + {{ + !uneditedScheduledEvent.disable_restriction + }} + {{ !se.disable_restriction }} +
    On demand + {{ se.on_demand }} + + {{ + uneditedScheduledEvent.on_demand + }} + {{ se.on_demand }} +
    Start Time + {{ se.start_time | date: "long" }} + + {{ + uneditedScheduledEvent.start_time | date: "long" + }} + {{ + se.start_time | date: "long" + }} +
    End Time + {{ se.end_time | date: "long" }} + + {{ + uneditedScheduledEvent.end_time | date: "long" + }} + {{ se.end_time | date: "long" }} +
    Courses + @for (s of uneditedScheduledEvent.courses; track s) { + @if (isCourseInList(s, se.courses)) { + {{ s }} + } @else { + {{ s }} + + } + } + @for (s of se.courses; track s) { + @if (!isCourseInList(s, uneditedScheduledEvent.courses)) { + {{ s }} + + } + } +
    Scenarios + @for (s of uneditedScheduledEvent.scenarios; track s) { + @if (isScenarioInList(s, se.scenarios)) { + {{ s }} + } @else { + {{ s }} + } + } + @for (s of se.scenarios; track s) { + @if (!isScenarioInList(s, uneditedScheduledEvent.scenarios)) { + {{ s }} + } + } +
    + @if (!se.on_demand) { + + + VMs will be started right away + + + } +

    VM Information

    + @if (noVirtualMachinesNeeded) { + VirtualMachines are not needed for this ScheduledEvent. + } @else { + + + + + + + + + @for (i of se.required_vms | keyvalue; track i) { + + + + + } + +
    EnvironmentVirtual Machines
    {{ getEnvironmentName(i.key) }} + @for (q of i.value | keyvalue; track q) { + + {{ getVirtualMachineTemplateName(q.key) }}  + @if ( + getUneditedScheduledEventVMCount(i.key, q.key) == + q.value + ) { + + {{ q.value }} + + } @else { + + + {{ + getUneditedScheduledEventVMCount(i.key, q.key) + }} + + {{ q.value }} + + } + + } +
    + } +

    SharedVM Information

    + + + + + + + + + + + + @for(sharedVm of se.shared_vms;track sharedVm;let i = $index) { + + + + + + } + + +
    NEnvironmentVirtual Machines
    {{ sharedVm.name }}{{ sharedVm.environment }}{{ sharedVm.vm_template }}
    + + +
    + } } + diff --git a/src/app/event/new-scheduled-event/new-scheduled-event.component.ts b/src/app/event/new-scheduled-event/new-scheduled-event.component.ts index 28d5b849..a39a12a9 100644 --- a/src/app/event/new-scheduled-event/new-scheduled-event.component.ts +++ b/src/app/event/new-scheduled-event/new-scheduled-event.component.ts @@ -116,6 +116,34 @@ export class NewScheduledEventComponent private onCloseFn: Function; private wizardSubscription: Subscription; + public newSharedVM: Record>; + + public sharedVmForm = new FormGroup({ + vm_name: new FormControl('', { + validators: [ + Validators.required, + Validators.minLength(4), + this.noWhitespaceValidator(), + this.uniqueSharedVMNameValidator(), + ], + nonNullable: true, + }), + vm_env: new FormControl('', { + validators: [ + Validators.required, + ], + nonNullable: true, + }), + vm_template: new FormControl('', { + validators: [ + Validators.required, + this.templateMatchesEnvValidator(), + ], + nonNullable: true, + }), + }); + + constructor( private _fb: NonNullableFormBuilder, @@ -159,6 +187,14 @@ export class NewScheduledEventComponent wizardPages.first.makeCurrent(); }); }); + + this.sharedVmForm.controls.vm_env.valueChanges.subscribe((env) => { + this.sharedVmForm.controls.vm_template.setValue(this.getTemplates(env)[0] ?? "") + }) + + this.sharedVmForm.valueChanges.subscribe(() => { + this.sharedVmForm.controls.vm_template.updateValueAndValidity() + }) } public eventDetails: FormGroup<{ @@ -309,6 +345,68 @@ export class NewScheduledEventComponent return null; } + private uniqueSharedVMNameValidator(): ( + control: AbstractControl, + ) => { notUnique: boolean } | null { + return (control: AbstractControl) => this.uniqueSharedVMName(control); + } + + private uniqueSharedVMName( + control: AbstractControl, + ): { notUnique: boolean } | null { + if ( + !control.value || + this.scheduledEvents.filter((el) => + el.shared_vms.map((vm) => vm.name).includes(control.value) + ).length > 0 + ) { + return { + notUnique: true, + }; + } + return null; + } + + private noWhitespaceValidator(): ( + control: AbstractControl, + ) => { whitespace: boolean } | null { + return (control: AbstractControl) => this.noWhitespace(control); + } + + private noWhitespace( + control: AbstractControl, + ): { whitespace: boolean } | null { + if (control.value.includes(' ')) { + return { + whitespace: true, + }; + } + return null; + } + + private templateMatchesEnvValidator(): ( + control: AbstractControl, + ) => { matchEnv: boolean } | null { + return (control: AbstractControl) => this.templateMatchesEnv(control); + } + + private templateMatchesEnv( + control: AbstractControl, + ): { matchEnv: boolean } | null { + if ( + !control.value || + !this.sharedVmForm.controls.vm_env || + !(this.getTemplates(this.sharedVmForm.controls.vm_env.value).includes(control.value)) + ) { + return { + matchEnv: true, + }; + } + return null; + } + + + @ViewChild('wizard', { static: true }) wizard: ClrWizard; @ViewChildren(ClrWizardPage) wizardPages: QueryList; @ViewChild('startTimeSignpost') startTimeSignpost: ClrSignpostContent; @@ -1155,4 +1253,29 @@ export class NewScheduledEventComponent this.uneditedScheduledEvent.required_vms[environment]?.[vmtemplate] ?? 0 ); } + + getTemplatesForEnv() { + const templates = this.getTemplates(this.sharedVmForm.controls.vm_env.value) + let availableTemplates = new Map() + this.virtualMachineTemplateList.forEach((k, v) => { + if (templates.includes(k)) availableTemplates.set(k, v) + }) + return availableTemplates; + } + + public addSharedVM() { + if (this.se.shared_vms == null) { + this.se.shared_vms = []; + } + this.se.shared_vms.push({ + vm_id: '', + name: this.sharedVmForm.controls.vm_name.value, + environment: this.sharedVmForm.controls.vm_env.value, + vm_template: this.sharedVmForm.controls.vm_template.value, + }); + } + + deleteSharedVm(index: number) { + this.se.shared_vms.splice(index, 1); + } } diff --git a/src/app/scenario/md-editor/markdownActions.ts b/src/app/scenario/md-editor/markdownActions.ts index bd301ae2..ca71ae51 100644 --- a/src/app/scenario/md-editor/markdownActions.ts +++ b/src/app/scenario/md-editor/markdownActions.ts @@ -105,11 +105,18 @@ export const ACTIONS: MDEditorAction[] = [ }, { name: 'VM-Info', - actionBefore: '${vmInfo::}', + actionBefore: '${vminfo::}', actionAfter: '', actionEmpty: '', icon: 'host', }, + { + name: 'Shared-VM-Info', + actionBefore: '${shared::}', + actionAfter: '', + actionEmpty: '', + icon: 'rack-server', + }, { name: 'Task', actionBefore: '```verifyTask::', diff --git a/src/app/session-statistics/session-statistics.component.ts b/src/app/session-statistics/session-statistics.component.ts index 900d2a62..94c14f1b 100644 --- a/src/app/session-statistics/session-statistics.component.ts +++ b/src/app/session-statistics/session-statistics.component.ts @@ -425,7 +425,6 @@ export class SessionStatisticsComponent implements OnInit, OnChanges { event?: ChartEvent; active?: {}[]; }): void { - // console.log(event, active); } public chartHovered({ @@ -435,7 +434,6 @@ export class SessionStatisticsComponent implements OnInit, OnChanges { event?: ChartEvent; active?: {}[]; }): void { - // console.log(event, active); } private setupScenariosWithSessions(progressData: Progress[]) { diff --git a/src/app/session-statistics/session-time-statistics/session-time-statistics.component.ts b/src/app/session-statistics/session-time-statistics/session-time-statistics.component.ts index 8eb7d8a9..81dbe1b1 100644 --- a/src/app/session-statistics/session-time-statistics/session-time-statistics.component.ts +++ b/src/app/session-statistics/session-time-statistics/session-time-statistics.component.ts @@ -252,7 +252,6 @@ export class SessionTimeStatisticsComponent implements OnInit { event?: ChartEvent; active?: {}[]; }): void { - // console.log(event, active); } public chartHovered({ @@ -262,7 +261,6 @@ export class SessionTimeStatisticsComponent implements OnInit { event?: ChartEvent; active?: {}[]; }): void { - // console.log(event, active); } private prepareBarchartDatasets() { diff --git a/src/app/step/hf-markdown.component.ts b/src/app/step/hf-markdown.component.ts index afbad29b..946b660c 100644 --- a/src/app/step/hf-markdown.component.ts +++ b/src/app/step/hf-markdown.component.ts @@ -139,7 +139,7 @@ ${token}`; private renderHighlightedCode( code: string, language: string, - fileName?: string, + fileName?: string ) { const fileNameTag = fileName ? `

    ${fileName}

    ` @@ -212,7 +212,7 @@ ${token}`; } const contentWithReplacedTokens = this.replaceSessionToken( - this.replaceVmInfoTokens(this.content), + this.replaceVmInfoTokens(this.replaceSharedVmInfoTokens(this.content)) ); // the parse method internally uses the Angular Dom Sanitizer and is therefore safe to use const parsedContent = this.markdownService.parse(contentWithReplacedTokens); @@ -234,8 +234,22 @@ ${token}`; /\$\{vminfo:([^:]*):([^}]*)\}/g, (match, vmName, propName) => { const vm = this.context.vmInfo?.[vmName.toLowerCase()]; - return String(vm?.[propName as keyof VM] ?? match); - }, + return String( + vm?.vm_type != 'SHARED' ? vm?.[propName as keyof VM] : match + ); + } + ); + } + + private replaceSharedVmInfoTokens(content: string) { + return content.replace( + /\$\{shared:([^:]*):([^}]*)\}/g, + (match, vmName, propName) => { + const vm = this.context.vmInfo?.[vmName.toLowerCase()]; + return String( + vm?.vm_type == 'SHARED' ? vm?.[propName as keyof VM] : match + ); + } ); } diff --git a/src/app/step/step-component/step.component.html b/src/app/step/step-component/step.component.html index 2ea61ba5..a8e01283 100644 --- a/src/app/step/step-component/step.component.html +++ b/src/app/step/step-component/step.component.html @@ -1,98 +1,168 @@
    - - - @@ -147,7 +217,7 @@
    @@ -157,7 +227,7 @@