# Generated dependency list
direct-dependencies.txt
.coverage
+out-tsc
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ui</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ui</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ui</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
path: 'resource-dictionary',
loadChildren: './modules/feature-modules/resource-dictionary/resource-dictionary.module#ResourceDictionaryModule'
},
+ {
+ path: 'execute',
+ loadChildren: './modules/feature-modules/execution/execution.module#ExecutionModule'
+ },
// { path: '', component: MainAppComponent },
{
path: '',
})
export class AppRoutingModule {
}
-
getMetaDate: '/controllerblueprint/meta-data/',
countOfAllBluePrints: '/controllerblueprint/list/count',
getMetaDatePageable: '/controllerblueprint/metadata/paged',
- getBlueprintByName: '/controllerblueprint/by-name/'
+ getBlueprintByName: '/controllerblueprint/by-name/',
+ getWorkflows: '/controllerblueprint/workflows/',
+ getWorkflowSpec: '/controllerblueprint/workflow-spec'
};
export const ResourceDictionaryURLs = {
getDerivedFrom: '/controllercatalog/model-type/by-derivedfrom'
};
+export const ExecutionURLs = {
+ execute: '/controllerblueprint/execute',
+};
+
export const ActionElementTypeName = 'app.ActionElement';
--- /dev/null
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { ApiService } from '../../../common/core/services/api.typed.service';
+import { ExecutionURLs, BlueprintURLs } from '../../../common/constants/app-constants';
+import { BluePrintPage } from '../packages/model/BluePrint.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ExecutionApiService {
+
+ constructor(private api: ApiService<any>) {
+ }
+
+ executeBlueprint(payload: any): Observable<any> {
+ return this.api.post(ExecutionURLs.execute, payload);
+ }
+
+ getPagedPackages(pageNumber: number, pageSize: number, sortBy: string): Observable<BluePrintPage[]> {
+ const sortType = sortBy.includes('DATE') ? 'DESC' : 'ASC';
+ return this.api.get(BlueprintURLs.getPagedBlueprints, {
+ offset: pageNumber,
+ limit: pageSize,
+ sort: sortBy,
+ sortType,
+ });
+ }
+
+ getBlueprintByNameAndVersion(name: string, version: string): Observable<any> {
+ return this.api.get(BlueprintURLs.getBlueprintByName + name + '/version/' + version);
+ }
+
+ getWorkflows(name: string, version: string): Observable<any> {
+ return this.api.getCustomized(BlueprintURLs.getWorkflows + name + '/' + version);
+ }
+
+ getWorkflowSpec(name: string, version: string, workflowName: string): Observable<any> {
+ return this.api.post(BlueprintURLs.getWorkflowSpec, {
+ blueprintName: name,
+ version,
+ workflowName,
+ });
+ }
+}
--- /dev/null
+.tab-content {
+ padding: 20px 0;
+}
+
+.nav-tabs .nav-link {
+ cursor: pointer;
+}
+
+.nav-tabs .nav-link.active {
+ font-weight: 600;
+}
--- /dev/null
+<app-header></app-header>
+
+<div class="new-wrapper">
+ <div class="container-fluid main-container">
+ <header class="page-title" style="padding: 16px 30px 12px;">
+ <div class="row">
+ <h2 class="col m-0">
+ <ul class="breadcrumb-header">
+ <li><a routerLink="/packages">CBA Packages</a></li>
+ <i class="fa fa-angle-right ml-2 mr-2"></i>
+ <li>Execute</li>
+ </ul>
+ </h2>
+ </div>
+ </header>
+ <div class="container-fluid body-container">
+ <div class="container">
+ <nav class="row">
+ <div class="col">
+ <div class="nav nav-tabs" id="execution-nav-tab" role="tablist">
+ <a class="nav-item nav-link"
+ [class.active]="activeTab === 'setup'"
+ (click)="selectTab('setup')"
+ id="nav-setup-tab"
+ role="tab">
+ <i class="fa fa-cog mr-1"></i> Execution Setup
+ </a>
+ <a class="nav-item nav-link"
+ [class.active]="activeTab === 'history'"
+ (click)="selectTab('history')"
+ id="nav-history-tab"
+ role="tab">
+ <i class="fa fa-history mr-1"></i> Execution History
+ </a>
+ <a class="nav-item nav-link"
+ [class.active]="activeTab === 'liveview'"
+ (click)="selectTab('liveview')"
+ id="nav-liveview-tab"
+ role="tab">
+ <i class="fa fa-terminal mr-1"></i> Live View
+ </a>
+ </div>
+ </div>
+ </nav>
+
+ <div class="tab-content" id="execution-tab-content">
+ <div *ngIf="activeTab === 'setup'" class="tab-pane fade show active" role="tabpanel">
+ <app-execution-setup
+ [prefilledName]="prefilledName"
+ [prefilledVersion]="prefilledVersion"
+ (executionCompleted)="selectTab('liveview')">
+ </app-execution-setup>
+ </div>
+ <div *ngIf="activeTab === 'history'" class="tab-pane fade show active" role="tabpanel">
+ <app-execution-history></app-execution-history>
+ </div>
+ <div *ngIf="activeTab === 'liveview'" class="tab-pane fade show active" role="tabpanel">
+ <app-live-view></app-live-view>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+@Component({
+ selector: 'app-execution-dashboard',
+ templateUrl: './execution-dashboard.component.html',
+ styleUrls: ['./execution-dashboard.component.css'],
+})
+export class ExecutionDashboardComponent implements OnInit {
+
+ activeTab = 'setup';
+
+ // Pre-filled from query params when navigating from package detail
+ prefilledName = '';
+ prefilledVersion = '';
+
+ constructor(private route: ActivatedRoute) {
+ }
+
+ ngOnInit() {
+ this.route.queryParams.subscribe(params => {
+ if (params.name) {
+ this.prefilledName = params.name;
+ }
+ if (params.version) {
+ this.prefilledVersion = params.version;
+ }
+ });
+ }
+
+ selectTab(tab: string) {
+ this.activeTab = tab;
+ }
+}
--- /dev/null
+.execution-history-container {
+ padding: 15px 0;
+}
+
+.history-row {
+ cursor: pointer;
+}
+
+.request-id {
+ font-family: monospace;
+ font-size: 12px;
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.empty-state i {
+ display: block;
+}
--- /dev/null
+<div class="execution-history-container">
+ <div class="row mb-3" *ngIf="hasHistory">
+ <div class="col text-right">
+ <button class="btn btn-sm btn-outline-secondary" (click)="clearHistory()">
+ <i class="fa fa-trash mr-1"></i> Clear History
+ </button>
+ </div>
+ </div>
+
+ <div *ngIf="!hasHistory" class="empty-state text-center p-5">
+ <i class="fa fa-history fa-3x text-muted mb-3"></i>
+ <p class="text-muted">No execution history yet.</p>
+ <p class="text-muted small">Execute a blueprint from the Execution Setup tab to see results here.</p>
+ </div>
+
+ <table class="table table-hover" *ngIf="hasHistory">
+ <thead>
+ <tr>
+ <th>Request ID</th>
+ <th>Blueprint</th>
+ <th>Version</th>
+ <th>Action</th>
+ <th>Status</th>
+ <th>Timestamp</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let exec of executions"
+ (click)="selectExecution(exec)"
+ [class.table-active]="selectedExecution === exec"
+ class="history-row">
+ <td class="request-id">{{ exec.requestId }}</td>
+ <td>{{ exec.blueprintName }}</td>
+ <td>{{ exec.blueprintVersion }}</td>
+ <td>{{ exec.actionName }}</td>
+ <td>
+ <span class="badge"
+ [class.badge-success]="exec.status === 'success'"
+ [class.badge-danger]="exec.status === 'failure'"
+ [class.badge-warning]="exec.status === 'pending'">
+ {{ exec.status }}
+ </span>
+ </td>
+ <td>{{ exec.timestamp }}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+interface ExecutionRecord {
+ requestId: string;
+ blueprintName: string;
+ blueprintVersion: string;
+ actionName: string;
+ status: string;
+ timestamp: string;
+}
+
+@Component({
+ selector: 'app-execution-history',
+ templateUrl: './execution-history.component.html',
+ styleUrls: ['./execution-history.component.css'],
+})
+export class ExecutionHistoryComponent implements OnInit {
+
+ executions: ExecutionRecord[] = [];
+ selectedExecution: ExecutionRecord = null;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ this.loadHistory();
+ }
+
+ loadHistory() {
+ // History is populated from local session storage for now.
+ // API-based audit history can be added when the backend enhancement is available.
+ const raw = sessionStorage.getItem('cds-execution-history');
+ if (raw) {
+ try {
+ this.executions = JSON.parse(raw);
+ } catch (_) {
+ this.executions = [];
+ }
+ }
+ }
+
+ selectExecution(exec: ExecutionRecord) {
+ this.selectedExecution = exec;
+ }
+
+ clearHistory() {
+ this.executions = [];
+ sessionStorage.removeItem('cds-execution-history');
+ this.selectedExecution = null;
+ }
+
+ get hasHistory(): boolean {
+ return this.executions.length > 0;
+ }
+}
--- /dev/null
+.execution-setup-container {
+ padding: 15px 0;
+}
+
+.setup-card,
+.editor-card,
+.response-card {
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+.setup-card .card-title,
+.editor-card .card-title,
+.response-card .card-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 15px;
+}
+
+.form-group label {
+ font-size: 12px;
+ font-weight: 600;
+ color: #666;
+ margin-bottom: 4px;
+}
+
+.btn-execute {
+ min-width: 120px;
+}
+
+.ace-editor-wrapper {
+ border: 1px solid #e0e0e0;
+ border-radius: 3px;
+}
+
+/* Action Inputs */
+.action-inputs-section {
+ border-top: 1px solid #e0e0e0;
+ padding-top: 12px;
+}
+
+.section-label {
+ font-size: 13px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 10px;
+ display: block;
+}
+
+.input-field-label {
+ font-size: 11px;
+ font-weight: 600;
+ color: #555;
+ margin-bottom: 2px;
+}
+
+.complex-input-group {
+ margin-bottom: 8px;
+}
+
+.complex-group-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: #444;
+ margin-bottom: 6px;
+ display: block;
+}
+
+.complex-fields {
+ padding-left: 10px;
+ border-left: 2px solid #e0e0e0;
+}
+
+.complex-fields .form-group {
+ margin-bottom: 8px;
+}
+
+.input-group-block {
+ margin-bottom: 4px;
+}
+
+.action-inputs-loading,
+.action-inputs-error {
+ padding: 6px 0;
+}
--- /dev/null
+<div class="execution-setup-container">
+ <div class="row">
+ <!-- Left column: form fields -->
+ <div class="col-md-4">
+ <div class="card setup-card">
+ <div class="card-body">
+ <h5 class="card-title">Blueprint Selection</h5>
+
+ <div class="form-group">
+ <label for="blueprintName">Blueprint Name</label>
+ <select id="blueprintName"
+ class="form-control"
+ [(ngModel)]="blueprintName"
+ (ngModelChange)="onBlueprintNameChange()">
+ <option value="" disabled>Select a blueprint...</option>
+ <option *ngFor="let name of uniquePackageNames" [value]="name">
+ {{ name }}
+ </option>
+ </select>
+ </div>
+
+ <div class="form-group">
+ <label for="blueprintVersion">Version</label>
+ <select id="blueprintVersion"
+ class="form-control"
+ [(ngModel)]="blueprintVersion"
+ (ngModelChange)="onBlueprintVersionChange()"
+ [disabled]="availableVersions.length === 0">
+ <option value="" disabled>Select version...</option>
+ <option *ngFor="let ver of availableVersions" [value]="ver">
+ {{ ver }}
+ </option>
+ </select>
+ </div>
+
+ <div class="form-group">
+ <label for="actionName">Action Name</label>
+ <select id="actionName"
+ class="form-control"
+ [(ngModel)]="actionName"
+ (ngModelChange)="onActionChange()"
+ [disabled]="availableActions.length === 0">
+ <option value="" disabled>Select an action...</option>
+ <option *ngFor="let action of availableActions" [value]="action">
+ {{ action }}
+ </option>
+ </select>
+ </div>
+
+ <!-- Action Inputs: loading state -->
+ <div *ngIf="loadingSpec" class="action-inputs-loading mt-2">
+ <i class="fa fa-spinner fa-spin mr-1"></i>
+ <small class="text-muted">Loading action inputs...</small>
+ </div>
+
+ <!-- Action Inputs: spec load failed -->
+ <div *ngIf="actionName && !loadingSpec && specLoadFailed" class="action-inputs-error mt-2">
+ <small class="text-muted">
+ <i class="fa fa-exclamation-triangle mr-1"></i>
+ Could not load input schema. Edit the JSON payload manually.
+ </small>
+ </div>
+
+ <!-- Action Inputs: dynamic form -->
+ <div *ngIf="actionName && !loadingSpec && inputGroups.length > 0"
+ class="action-inputs-section mt-3" id="actionInputs">
+ <label class="section-label">Action Inputs</label>
+
+ <div *ngFor="let group of inputGroups" class="input-group-block">
+ <!-- Primitive input (not a data type reference) -->
+ <div *ngIf="!group.isComplex" class="form-group">
+ <label [for]="'input-' + group.inputName" class="input-field-label">
+ {{ group.inputName }}
+ <span class="text-danger" *ngIf="group.required">*</span>
+ </label>
+ <input *ngIf="group.type !== 'boolean'"
+ [id]="'input-' + group.inputName"
+ class="form-control form-control-sm"
+ [type]="group.type === 'integer' ? 'number' : 'text'"
+ [(ngModel)]="group.value"
+ (ngModelChange)="onFieldChange()"
+ [placeholder]="group.description"
+ [attr.data-input-name]="group.inputName">
+ <select *ngIf="group.type === 'boolean'"
+ [id]="'input-' + group.inputName"
+ class="form-control form-control-sm"
+ [(ngModel)]="group.value"
+ (ngModelChange)="onFieldChange()"
+ [attr.data-input-name]="group.inputName">
+ <option value="">-- select --</option>
+ <option value="true">true</option>
+ <option value="false">false</option>
+ </select>
+ </div>
+
+ <!-- Complex input (data type with nested fields) -->
+ <div *ngIf="group.isComplex" class="complex-input-group">
+ <label class="complex-group-label">
+ {{ group.inputName }}
+ <span class="text-danger" *ngIf="group.required">*</span>
+ </label>
+ <div class="complex-fields">
+ <div *ngFor="let field of group.fields" class="form-group">
+ <label [for]="'field-' + group.inputName + '-' + field.name"
+ class="input-field-label">
+ {{ field.name }}
+ <span class="text-danger" *ngIf="field.required">*</span>
+ </label>
+ <input *ngIf="field.type !== 'boolean'"
+ [id]="'field-' + group.inputName + '-' + field.name"
+ class="form-control form-control-sm"
+ [type]="field.type === 'integer' ? 'number' : 'text'"
+ [(ngModel)]="field.value"
+ (ngModelChange)="onFieldChange()"
+ [placeholder]="field.description"
+ [attr.data-field-name]="field.name">
+ <select *ngIf="field.type === 'boolean'"
+ [id]="'field-' + group.inputName + '-' + field.name"
+ class="form-control form-control-sm"
+ [(ngModel)]="field.value"
+ (ngModelChange)="onFieldChange()"
+ [attr.data-field-name]="field.name">
+ <option value="">-- select --</option>
+ <option value="true">true</option>
+ <option value="false">false</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr>
+
+ <div class="execution-actions">
+ <button class="btn btn-primary btn-execute"
+ [disabled]="!canExecute()"
+ (click)="executeBlueprint()">
+ <i class="fa fa-play mr-1" *ngIf="!isExecuting"></i>
+ <i class="fa fa-spinner fa-spin mr-1" *ngIf="isExecuting"></i>
+ {{ isExecuting ? 'Executing...' : 'Execute' }}
+ </button>
+ <button class="btn btn-outline-secondary ml-2"
+ (click)="resetPayload()">
+ <i class="fa fa-refresh mr-1"></i> Reset
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Right column: JSON payload editor -->
+ <div class="col-md-8">
+ <div class="card editor-card">
+ <div class="card-body">
+ <h5 class="card-title">Request Payload (ExecutionServiceInput)</h5>
+ <div class="ace-editor-wrapper">
+ <ace-editor
+ [(text)]="payloadText"
+ [mode]="'json'"
+ [theme]="'eclipse'"
+ [options]="aceOptions"
+ style="min-height: 350px; width: 100%; font-size: 13px;">
+ </ace-editor>
+ </div>
+ </div>
+ </div>
+
+ <!-- Response panel -->
+ <div class="card response-card mt-3" *ngIf="lastResponseText">
+ <div class="card-body">
+ <h5 class="card-title">
+ <i class="fa fa-check-circle text-success mr-1" *ngIf="lastResponse"></i>
+ <i class="fa fa-times-circle text-danger mr-1" *ngIf="!lastResponse"></i>
+ Execution Response
+ </h5>
+ <div class="ace-editor-wrapper">
+ <ace-editor
+ [text]="lastResponseText"
+ [mode]="'json'"
+ [theme]="'eclipse'"
+ [readOnly]="true"
+ [options]="{ maxLines: 20, minLines: 5, showPrintMargin: false }"
+ style="min-height: 150px; width: 100%; font-size: 13px;">
+ </ace-editor>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { ToastrService } from 'ngx-toastr';
+import { ExecutionApiService } from '../execution-api.service';
+
+@Component({
+ selector: 'app-execution-setup',
+ templateUrl: './execution-setup.component.html',
+ styleUrls: ['./execution-setup.component.css'],
+})
+export class ExecutionSetupComponent implements OnInit {
+
+ @Input() prefilledName = '';
+ @Input() prefilledVersion = '';
+ @Output() executionCompleted = new EventEmitter<any>();
+
+ blueprintName = '';
+ blueprintVersion = '';
+ actionName = '';
+ isExecuting = false;
+
+ availablePackages: any[] = [];
+ availableVersions: string[] = [];
+ availableActions: string[] = [];
+
+ // Workflow-spec driven input form
+ inputGroups: Array<{
+ inputName: string;
+ type: string;
+ description: string;
+ required: boolean;
+ isComplex: boolean;
+ value: string;
+ fields: Array<{
+ name: string;
+ type: string;
+ description: string;
+ required: boolean;
+ value: string;
+ }>;
+ }> = [];
+ loadingSpec = false;
+ specLoadFailed = false;
+
+ payloadText = JSON.stringify(this.defaultPayload(), null, 2);
+ aceOptions: any = {
+ maxLines: 30,
+ minLines: 15,
+ autoScrollEditorIntoView: true,
+ showPrintMargin: false,
+ };
+
+ lastResponse: any = null;
+ lastResponseText = '';
+
+ constructor(
+ private executionApiService: ExecutionApiService,
+ private toastService: ToastrService,
+ ) {
+ }
+
+ ngOnInit() {
+ this.loadPackages();
+ if (this.prefilledName) {
+ this.blueprintName = this.prefilledName;
+ }
+ if (this.prefilledVersion) {
+ this.blueprintVersion = this.prefilledVersion;
+ }
+ if (this.prefilledName && this.prefilledVersion) {
+ this.buildPayload();
+ }
+ }
+
+ loadPackages() {
+ this.executionApiService.getPagedPackages(0, 1000, 'DATE').subscribe(
+ (pages: any) => {
+ const page = Array.isArray(pages) ? pages[0] : pages;
+ const content = page && page.content ? page.content : [];
+ this.availablePackages = content;
+ },
+ err => {
+ console.error('Failed to load packages', err);
+ }
+ );
+ }
+
+ onBlueprintNameChange() {
+ const matching = this.availablePackages.filter(p => p.artifactName === this.blueprintName);
+ this.availableVersions = matching.map(p => p.artifactVersion);
+ if (this.availableVersions.length === 1) {
+ this.blueprintVersion = this.availableVersions[0];
+ } else {
+ this.blueprintVersion = '';
+ }
+ this.actionName = '';
+ this.availableActions = [];
+ this.inputGroups = [];
+ this.loadingSpec = false;
+ this.specLoadFailed = false;
+ this.buildPayload();
+ if (this.blueprintVersion) {
+ this.loadActions();
+ }
+ }
+
+ onBlueprintVersionChange() {
+ this.actionName = '';
+ this.availableActions = [];
+ this.inputGroups = [];
+ this.loadingSpec = false;
+ this.specLoadFailed = false;
+ this.buildPayload();
+ if (this.blueprintVersion) {
+ this.loadActions();
+ }
+ }
+
+ loadActions() {
+ this.executionApiService.getWorkflows(this.blueprintName, this.blueprintVersion).subscribe(
+ (result: any) => {
+ if (result && Array.isArray(result.workflows)) {
+ this.availableActions = result.workflows.slice().sort();
+ if (this.availableActions.length === 1) {
+ this.actionName = this.availableActions[0];
+ this.onActionChange();
+ }
+ }
+ },
+ err => {
+ console.error('Failed to load actions', err);
+ }
+ );
+ }
+
+ onActionChange() {
+ if (this.actionName) {
+ this.loadWorkflowSpec();
+ } else {
+ this.inputGroups = [];
+ this.specLoadFailed = false;
+ this.buildPayload();
+ }
+ }
+
+ loadWorkflowSpec() {
+ this.loadingSpec = true;
+ this.specLoadFailed = false;
+ this.inputGroups = [];
+ this.buildPayload();
+ this.executionApiService.getWorkflowSpec(
+ this.blueprintName, this.blueprintVersion, this.actionName
+ ).subscribe(
+ (spec: any) => {
+ this.loadingSpec = false;
+ this.resolveWorkflowSpec(spec);
+ this.buildPayload();
+ },
+ err => {
+ this.loadingSpec = false;
+ this.specLoadFailed = true;
+ console.error('Failed to load workflow spec', err);
+ }
+ );
+ }
+
+ resolveWorkflowSpec(spec: any) {
+ this.inputGroups = [];
+ if (!spec || !spec.workFlowData || !spec.workFlowData.inputs) {
+ return;
+ }
+ const inputs = spec.workFlowData.inputs;
+ const dataTypes = spec.dataTypes || {};
+
+ Object.keys(inputs).forEach(inputName => {
+ const inputDef = inputs[inputName];
+ const typeName = inputDef.type || 'string';
+ const dataType = dataTypes[typeName];
+
+ const group: any = {
+ inputName,
+ type: typeName,
+ description: inputDef.description || '',
+ required: !!inputDef.required,
+ isComplex: !!dataType,
+ value: '',
+ fields: [],
+ };
+
+ if (dataType && dataType.properties) {
+ group.fields = Object.keys(dataType.properties).map(fieldName => {
+ const fieldDef = dataType.properties[fieldName];
+ return {
+ name: fieldName,
+ type: fieldDef.type || 'string',
+ description: fieldDef.description || '',
+ required: !!fieldDef.required,
+ value: '',
+ };
+ });
+ }
+
+ this.inputGroups.push(group);
+ });
+ }
+
+ onFieldChange() {
+ this.buildPayload();
+ }
+
+ get uniquePackageNames(): string[] {
+ const names = new Set(this.availablePackages.map(p => p.artifactName));
+ return Array.from(names).sort();
+ }
+
+ canExecute(): boolean {
+ return this.blueprintName.length > 0
+ && this.blueprintVersion.length > 0
+ && this.payloadText.length > 0
+ && !this.isExecuting;
+ }
+
+ executeBlueprint() {
+ if (!this.canExecute()) {
+ return;
+ }
+
+ let payload: any;
+ try {
+ payload = JSON.parse(this.payloadText);
+ } catch (e) {
+ this.toastService.error('Invalid JSON payload. Please correct the syntax.');
+ return;
+ }
+
+ this.isExecuting = true;
+ this.lastResponse = null;
+ this.lastResponseText = '';
+
+ this.executionApiService.executeBlueprint(payload).subscribe(
+ response => {
+ this.isExecuting = false;
+ this.lastResponse = response;
+ this.lastResponseText = JSON.stringify(response, null, 2);
+ this.toastService.success('Execution completed successfully.');
+ this.executionCompleted.emit(response);
+ },
+ error => {
+ this.isExecuting = false;
+ const msg = (error.error && error.error.message) || error.message || 'Execution failed';
+ this.toastService.error(msg);
+ this.lastResponseText = JSON.stringify(error.error || error, null, 2);
+ }
+ );
+ }
+
+ private buildPayload() {
+ const payload = this.defaultPayload();
+ payload.actionIdentifiers.blueprintName = this.blueprintName;
+ payload.actionIdentifiers.blueprintVersion = this.blueprintVersion;
+ payload.actionIdentifiers.actionName = this.actionName;
+
+ if (this.actionName) {
+ const requestKey = this.actionName + '-request';
+ const requestObj: any = {};
+
+ this.inputGroups.forEach(group => {
+ if (group.isComplex) {
+ const nested: any = {};
+ group.fields.forEach(field => {
+ nested[field.name] = this.convertValue(field.value, field.type);
+ });
+ requestObj[group.inputName] = nested;
+ } else {
+ requestObj[group.inputName] = this.convertValue(group.value, group.type);
+ }
+ });
+
+ payload.payload[requestKey] = requestObj;
+ }
+
+ this.payloadText = JSON.stringify(payload, null, 2);
+ }
+
+ private convertValue(value: string, type: string): any {
+ if (!value) {
+ return (type === 'integer') ? 0 : (type === 'boolean') ? false : '';
+ }
+ if (type === 'integer') {
+ const num = parseInt(value, 10);
+ return isNaN(num) ? 0 : num;
+ }
+ if (type === 'boolean') {
+ return value === 'true';
+ }
+ return value;
+ }
+
+ resetPayload() {
+ this.inputGroups.forEach(group => {
+ group.value = '';
+ group.fields.forEach(field => { field.value = ''; });
+ });
+ this.buildPayload();
+ }
+
+ private defaultPayload(): any {
+ return {
+ commonHeader: {
+ originatorId: 'CDS-UI',
+ requestId: this.generateUUID(),
+ subRequestId: this.generateUUID(),
+ timestamp: new Date().toISOString(),
+ },
+ actionIdentifiers: {
+ blueprintName: '',
+ blueprintVersion: '',
+ actionName: '',
+ mode: 'sync',
+ },
+ payload: {},
+ };
+ }
+
+ private generateUUID(): string {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ // tslint:disable-next-line:no-bitwise
+ const r = Math.random() * 16 | 0;
+ // tslint:disable-next-line:no-bitwise
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+}
--- /dev/null
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { AceEditorModule } from 'ng2-ace-editor';
+import { SharedModulesModule } from '../../shared-modules/shared-modules.module';
+import { ExecutionRoutingModule } from './execution.routing.module';
+import { ExecutionDashboardComponent } from './execution-dashboard/execution-dashboard.component';
+import { ExecutionSetupComponent } from './execution-setup/execution-setup.component';
+import { ExecutionHistoryComponent } from './execution-history/execution-history.component';
+import { LiveViewComponent } from './live-view/live-view.component';
+import { ApiService } from '../../../common/core/services/api.typed.service';
+
+@NgModule({
+ declarations: [
+ ExecutionDashboardComponent,
+ ExecutionSetupComponent,
+ ExecutionHistoryComponent,
+ LiveViewComponent,
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ AceEditorModule,
+ SharedModulesModule,
+ ExecutionRoutingModule,
+ ],
+ providers: [ApiService],
+})
+export class ExecutionModule {
+}
--- /dev/null
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ExecutionDashboardComponent } from './execution-dashboard/execution-dashboard.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ExecutionDashboardComponent,
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class ExecutionRoutingModule {
+}
--- /dev/null
+.live-view-container {
+ padding: 15px 0;
+}
+
+.status-banner {
+ padding: 12px 20px;
+ border-radius: 4px;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.status-success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.status-failure {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.status-completed {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+}
+
+.response-card {
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+}
+
+.response-card .card-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+}
+
+.ace-editor-wrapper {
+ border: 1px solid #e0e0e0;
+ border-radius: 3px;
+}
+
+.empty-state i {
+ display: block;
+}
--- /dev/null
+<div class="live-view-container">
+ <div *ngIf="!lastResponse" class="empty-state text-center p-5">
+ <i class="fa fa-terminal fa-3x text-muted mb-3"></i>
+ <p class="text-muted">No execution response available.</p>
+ <p class="text-muted small">Execute a blueprint from the Execution Setup tab to see live results here.</p>
+ </div>
+
+ <div *ngIf="lastResponse">
+ <div class="status-banner" [ngClass]="{
+ 'status-success': status === 'success',
+ 'status-failure': status === 'failure',
+ 'status-completed': status === 'completed'
+ }">
+ <i class="fa fa-check-circle mr-2" *ngIf="status === 'success'"></i>
+ <i class="fa fa-times-circle mr-2" *ngIf="status === 'failure'"></i>
+ <i class="fa fa-info-circle mr-2" *ngIf="status === 'completed'"></i>
+ {{ statusLabel }}
+ </div>
+
+ <div class="card response-card mt-3">
+ <div class="card-body">
+ <h5 class="card-title">Execution Response (ExecutionServiceOutput)</h5>
+ <div class="ace-editor-wrapper">
+ <ace-editor
+ [text]="lastResponseText"
+ [mode]="'json'"
+ [theme]="'eclipse'"
+ [readOnly]="true"
+ [options]="{ maxLines: 40, minLines: 10, showPrintMargin: false }"
+ style="min-height: 300px; width: 100%; font-size: 13px;">
+ </ace-editor>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-live-view',
+ templateUrl: './live-view.component.html',
+ styleUrls: ['./live-view.component.css'],
+})
+export class LiveViewComponent implements OnInit {
+
+ lastResponse: any = null;
+ lastResponseText = '';
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ // Load the last execution response from session storage
+ const raw = sessionStorage.getItem('cds-last-execution-response');
+ if (raw) {
+ try {
+ this.lastResponse = JSON.parse(raw);
+ this.lastResponseText = JSON.stringify(this.lastResponse, null, 2);
+ } catch (_) {
+ this.lastResponse = null;
+ }
+ }
+ }
+
+ get status(): string {
+ if (!this.lastResponse) {
+ return 'idle';
+ }
+ if (this.lastResponse.status && this.lastResponse.status.code === 200) {
+ return 'success';
+ }
+ if (this.lastResponse.status && this.lastResponse.status.code >= 400) {
+ return 'failure';
+ }
+ return 'completed';
+ }
+
+ get statusLabel(): string {
+ switch (this.status) {
+ case 'success': return 'Execution Successful';
+ case 'failure': return 'Execution Failed';
+ case 'completed': return 'Execution Completed';
+ default: return 'No Execution Data';
+ }
+ }
+}
[title]="isMetadataValid ? 'Deploy package' : 'Fill in all required metadata fields before deploying'"><i
class="fa fa-play-circle"></i> Deploy
</button>
+ <button class="btn btn-sm btn-execute ml-1" (click)="goToExecute()"
+ title="Execute this blueprint">
+ <i class="fa fa-bolt"></i> Execute
+ </button>
</div>
</div>
this.router.navigate(['/packages/designer', id, { actionName: this.customActionName }]);
}
+ goToExecute() {
+ this.router.navigate(['/execute'], {
+ queryParams: {
+ name: this.viewedPackage.artifactName,
+ version: this.viewedPackage.artifactVersion
+ }
+ });
+ }
+
public dropped(files: NgxFileDropEntry[]) {
}
<span class="icon">
<i class="icon-nav-dictionary"></i>
</span>
+ </li>
+ <li>
+ <a routerLink="/execute">Execute</a>
+ <span class="icon">
+ <i class="fa fa-play-circle"></i>
+ </span>
</li>
<!-- <label title="toggle menu" for="settings">
<span class="downarrow">
</ul>-->
</nav>
-</div>
\ No newline at end of file
+</div>
"artifactName": "RT-resource-resolution",
"published": "Y",
"updatedBy": "Selffish",
- "tags": "test, regression"
+ "tags": "test, regression",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} },
+ "config-assign": { "steps": {}, "inputs": {}, "outputs": {} },
+ "config-deploy": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "bf26fbf2-4bf5-482e-9cc1-e48e4878aceb",
"artifactName": "vLB_CDS_KOTLIN",
"published": "Y",
"updatedBy": "PLATANIA, MARCO <platania@research.att.com>",
- "tags": "test, vDNS-CDS, SCALE-OUT, MARCO"
+ "tags": "test, vDNS-CDS, SCALE-OUT, MARCO",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} },
+ "health-check": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "3bca2b79-020d-4c98-ad19-a425a42c17d8",
"artifactName": "vLB_CDS_RESTCONF",
"published": "Y",
"updatedBy": "Seaudi, Abdelmuhaimen <abdelmuhaimen.seaudi@orange.com>",
- "tags": "vLB-CDS"
+ "tags": "vLB-CDS",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "8713ea11-fbdd-4a58-ae01-e5ba4ff42a44",
"artifactName": "vLB_CDS",
"published": "N",
"updatedBy": "Seaudi, Abdelmuhaimen <abdelmuhaimen.seaudi@orange.com>",
- "tags": "vLB_CDS"
+ "tags": "vLB_CDS",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} },
+ "activate-restconf": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "d97bc302-0077-4cf7-a494-9d070767fac5",
"artifactName": "5G_Core",
"published": "Y",
"updatedBy": "Thamlur Raju <TR00568434@TechMahindra.com>, Sangeeta Bellara <sangeeta.bellara@t-systems.com>",
- "tags": "Thamlur Raju, Malinconico Aniello Paolo,Vamshi, 5G_Core"
+ "tags": "Thamlur Raju, Malinconico Aniello Paolo,Vamshi, 5G_Core",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} },
+ "config-assign": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "a63117f8-2aa0-4b96-b8d2-ddaa9fa3b633",
"artifactName": "vFW-CDS",
"published": "Y",
"updatedBy": "PLATANIA, MARCO <platania@research.att.com>",
- "tags": "vFW-CDS"
+ "tags": "vFW-CDS",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "08dbe39c-f3db-40b7-8e77-c5ef069d4f1b",
"artifactName": "pnf_netconf",
"published": "N",
"updatedBy": "Aarna Services",
- "tags": "pnf_netconf"
+ "tags": "pnf_netconf",
+ "workflows": {
+ "config-assign": { "steps": {}, "inputs": {}, "outputs": {} },
+ "config-deploy": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "7b54c252-74a4-4cfa-b4f6-bdaa25c55ee7",
"artifactName": "APACHE",
"published": "Y",
"updatedBy": "Lukasz Rajewski <lukasz.rajewski@orange.com>",
- "tags": "Lukasz Rajewski, CNF"
+ "tags": "Lukasz Rajewski, CNF",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
},
{
"id": "9c254141-2083-47f6-980e-5b2f33563862",
"artifactName": "ubuntu20",
"published": "N",
"updatedBy": "RG, ONES <romain.garel@orange.com>",
- "tags": "ubuntu20"
+ "tags": "ubuntu20",
+ "workflows": {
+ "resource-resolution": { "steps": {}, "inputs": {}, "outputs": {} }
+ }
}
-]
\ No newline at end of file
+]
--- /dev/null
+{
+ "resource-resolution": {
+ "inputs": {
+ "resolution-key": {
+ "type": "string",
+ "required": false,
+ "description": "Resolution key for the resource resolution"
+ },
+ "store-result": {
+ "type": "boolean",
+ "required": false,
+ "description": "Whether to store the resolved resources"
+ },
+ "resource-assignment-properties": {
+ "type": "dt-resource-assignment-properties",
+ "required": true,
+ "description": "Dynamic properties for resource assignment"
+ }
+ },
+ "outputs": {
+ "meshed-template": { "type": "json", "value": {} }
+ },
+ "dataTypes": {
+ "dt-resource-assignment-properties": {
+ "derivedFrom": "tosca.datatypes.Dynamic",
+ "description": "Dynamic DataType for resource assignment",
+ "properties": {
+ "service-instance-id": { "type": "string", "required": true, "description": "Service Instance ID" },
+ "vnf-id": { "type": "string", "required": true, "description": "VNF Instance ID" },
+ "vf-module-id": { "type": "string", "required": false, "description": "VF Module ID" }
+ }
+ }
+ }
+ },
+ "config-assign": {
+ "inputs": {
+ "resolution-key": {
+ "type": "string",
+ "required": false,
+ "description": "Resolution key for config assignment"
+ },
+ "config-assign-properties": {
+ "type": "dt-config-assign-properties",
+ "required": true,
+ "description": "Dynamic properties for config assignment"
+ }
+ },
+ "outputs": {},
+ "dataTypes": {
+ "dt-config-assign-properties": {
+ "derivedFrom": "tosca.datatypes.Dynamic",
+ "description": "Dynamic DataType for config assignment",
+ "properties": {
+ "service-instance-id": { "type": "string", "required": true, "description": "Service Instance ID" },
+ "vnf-id": { "type": "string", "required": true, "description": "VNF Instance ID" },
+ "hostname": { "type": "string", "required": false, "description": "Target hostname" }
+ }
+ }
+ }
+ },
+ "config-deploy": {
+ "inputs": {
+ "resolution-key": {
+ "type": "string",
+ "required": false,
+ "description": "Resolution key for config deployment"
+ },
+ "config-deploy-properties": {
+ "type": "dt-config-deploy-properties",
+ "required": true,
+ "description": "Dynamic properties for config deployment"
+ }
+ },
+ "outputs": {},
+ "dataTypes": {
+ "dt-config-deploy-properties": {
+ "derivedFrom": "tosca.datatypes.Dynamic",
+ "description": "Dynamic DataType for config deployment",
+ "properties": {
+ "service-instance-id": { "type": "string", "required": true, "description": "Service Instance ID" },
+ "vnf-id": { "type": "string", "required": true, "description": "VNF Instance ID" },
+ "ip-address": { "type": "string", "required": false, "description": "Device IP address" },
+ "hostname": { "type": "string", "required": false, "description": "Device hostname" }
+ }
+ }
+ }
+ },
+ "health-check": {
+ "inputs": {
+ "health-check-properties": {
+ "type": "dt-health-check-properties",
+ "required": true,
+ "description": "Health check parameters"
+ }
+ },
+ "outputs": {
+ "health-check-result": { "type": "string", "value": "" }
+ },
+ "dataTypes": {
+ "dt-health-check-properties": {
+ "derivedFrom": "tosca.datatypes.Dynamic",
+ "description": "Dynamic DataType for health check",
+ "properties": {
+ "service-instance-id": { "type": "string", "required": true, "description": "Service Instance ID" },
+ "vnf-id": { "type": "string", "required": true, "description": "VNF Instance ID" },
+ "health-check-type": { "type": "string", "required": false, "description": "Type of health check to perform" }
+ }
+ }
+ }
+ },
+ "activate-restconf": {
+ "inputs": {
+ "activate-restconf-properties": {
+ "type": "dt-activate-restconf-properties",
+ "required": true,
+ "description": "RESTCONF activation parameters"
+ }
+ },
+ "outputs": {},
+ "dataTypes": {
+ "dt-activate-restconf-properties": {
+ "derivedFrom": "tosca.datatypes.Dynamic",
+ "description": "Dynamic DataType for RESTCONF activation",
+ "properties": {
+ "service-instance-id": { "type": "string", "required": true, "description": "Service Instance ID" },
+ "vnf-id": { "type": "string", "required": true, "description": "VNF Instance ID" },
+ "ip-address": { "type": "string", "required": true, "description": "Device IP address" },
+ "port": { "type": "integer", "required": false, "description": "RESTCONF port number" }
+ }
+ }
+ }
+ }
+}
fs.readFileSync(path.join(FIXTURES, 'resource-dictionaries.json'), 'utf8'));
const modelTypes = JSON.parse(
fs.readFileSync(path.join(FIXTURES, 'model-types.json'), 'utf8'));
+const workflowSpecs = JSON.parse(
+ fs.readFileSync(path.join(FIXTURES, 'workflow-specs.json'), 'utf8'));
// ── helpers ───────────────────────────────────────────────────────────────────
return bp ? json(res, bp) : json(res, { error: 'not found' }, 404);
}
+ // GET /api/v1/blueprint-model/workflows/blueprint-name/:name/version/:version
+ if (method === 'GET' &&
+ (m = pathname.match(`^${BASE}/blueprint-model/workflows/blueprint-name/([^/]+)/version/([^/]+)$`))) {
+ const [, name, version] = m;
+ const bp = blueprints.find(
+ b => b.artifactName === decodeURIComponent(name) && b.artifactVersion === decodeURIComponent(version));
+ if (!bp) return json(res, { error: 'not found' }, 404);
+ const workflows = bp.workflows ? Object.keys(bp.workflows) : [];
+ return json(res, { blueprintName: bp.artifactName, version: bp.artifactVersion, workflows });
+ }
+
// GET /api/v1/blueprint-model/download/by-name/:name/version/:version
if (method === 'GET' &&
(m = pathname.match(`^${BASE}/blueprint-model/download/by-name/([^/]+)/version/([^/]+)$`))) {
return json(res, { message: 'deleted', name: m[1] });
}
+ // ── workflow-spec ──────────────────────────────────────────────────────────
+
+ // POST /api/v1/blueprint-model/workflow-spec
+ if (method === 'POST' && (pathname === `${BASE}/blueprint-model/workflow-spec` || pathname === `${BASE}/blueprint-model/workflow-spec/`)) {
+ const raw = await readBody(req);
+ let body;
+ try { body = JSON.parse(raw); } catch (_) { return json(res, { error: 'invalid body' }, 400); }
+ const workflowName = body.workflowName || '';
+ const spec = workflowSpecs[workflowName];
+ if (!spec) {
+ return json(res, {
+ code: 404,
+ status: 'NOT_FOUND',
+ message: 'No workflow spec found for ' + workflowName
+ }, 404);
+ }
+ return json(res, {
+ blueprintName: body.blueprintName || '',
+ version: body.version || '',
+ workFlowData: {
+ workFlowName: workflowName,
+ inputs: spec.inputs || {},
+ outputs: spec.outputs || {}
+ },
+ dataTypes: spec.dataTypes || {}
+ });
+ }
+
+ // ── execution-service ──────────────────────────────────────────────────────
+
+ // POST /api/v1/execution-service/process
+ if (method === 'POST' && (pathname === `${BASE}/execution-service/process` || pathname === `${BASE}/execution-service/process/`)) {
+ const raw = await readBody(req);
+ let input;
+ try { input = JSON.parse(raw); } catch (_) { input = {}; }
+ const header = (input.commonHeader || {});
+ const actionIds = (input.actionIdentifiers || {});
+ const response = {
+ commonHeader: {
+ timestamp: new Date().toISOString(),
+ originatorId: header.originatorId || 'CDS',
+ requestId: header.requestId || 'mock-request-id',
+ subRequestId: header.subRequestId || 'mock-sub-request-id',
+ },
+ actionIdentifiers: {
+ blueprintName: actionIds.blueprintName || '',
+ blueprintVersion: actionIds.blueprintVersion || '',
+ actionName: actionIds.actionName || '',
+ mode: actionIds.mode || 'sync',
+ },
+ status: {
+ code: 200,
+ eventType: 'EVENT_COMPONENT_EXECUTED',
+ timestamp: new Date().toISOString(),
+ message: 'success',
+ },
+ payload: {
+ 'resource-resolution-response': {
+ 'meshed-template': {
+ json: '{"hostname": "mock-host-01", "ip-address": "10.0.0.1"}',
+ },
+ },
+ },
+ };
+ return json(res, response);
+ }
+
// ── fallthrough ───────────────────────────────────────────────────────────────
process.stderr.write(`[mock-processor] 404 – no stub for ${method} ${pathname}\n`);
json(res, { error: `no stub for ${method} ${pathname}` }, 404);
--- /dev/null
+/*
+ * execution.spec.ts
+ *
+ * End-to-end tests for the Blueprint Execution page. These tests verify:
+ * 1. Page structure – execution dashboard component renders with tabs.
+ * 2. Navigation – the Execute link in the sidebar navigates to the page.
+ * 3. Tab switching – Setup / History / Live View tabs work correctly.
+ * 4. Execution Setup – form fields render, blueprint dropdown is populated
+ * from the mock-processor, and the JSON payload editor is visible.
+ * 5. Action selection – selecting a blueprint+version populates the action
+ * dropdown from the blueprint's workflows.
+ * 6. Execution API – submitting the form POSTs to the execution endpoint
+ * and the mock-processor returns a successful response.
+ * 7. Package detail integration – the Execute button on the package detail
+ * page navigates to the execution page with pre-filled parameters.
+ */
+
+import { test, expect } from '@playwright/test';
+
+// ── helpers ────────────────────────────────────────────────────────────────────
+
+/** Wait for the execution dashboard to be rendered. */
+async function waitForExecutionDashboard(page: import('@playwright/test').Page) {
+ await expect(page.locator('app-execution-dashboard')).toBeAttached({ timeout: 20_000 });
+}
+
+/** Wait for the blueprint select dropdown to be populated from mock data. */
+async function waitForBlueprintOptions(page: import('@playwright/test').Page) {
+ await expect(page.locator('#blueprintName option').nth(1)).toBeAttached({ timeout: 20_000 });
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Page structure
+// ─────────────────────────────────────────────────────────────────────────────
+
+test.describe('Execution Page – structure', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/execute');
+ await page.waitForLoadState('networkidle');
+ });
+
+ test('execution dashboard component is rendered', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ });
+
+ test('breadcrumb shows Execute', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ const breadcrumb = page.locator('.breadcrumb-header');
+ await expect(breadcrumb).toContainText('Execute');
+ });
+
+ test('shows the Execution Setup tab', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ const tab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution Setup' });
+ await expect(tab).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('shows the Execution History tab', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ const tab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution History' });
+ await expect(tab).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('shows the Live View tab', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ const tab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Live View' });
+ await expect(tab).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('Execution Setup tab is active by default', async ({ page }) => {
+ await waitForExecutionDashboard(page);
+ const tab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution Setup' });
+ await expect(tab).toHaveClass(/active/, { timeout: 10_000 });
+ });
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Tab switching
+// ─────────────────────────────────────────────────────────────────────────────
+
+test.describe('Execution Page – tab switching', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/execute');
+ await waitForExecutionDashboard(page);
+ });
+
+ test('clicking History tab shows history content', async ({ page }) => {
+ const historyTab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution History' });
+ await historyTab.click();
+ await expect(historyTab).toHaveClass(/active/);
+ await expect(page.locator('app-execution-history')).toBeAttached();
+ });
+
+ test('clicking Live View tab shows live view content', async ({ page }) => {
+ const liveTab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Live View' });
+ await liveTab.click();
+ await expect(liveTab).toHaveClass(/active/);
+ await expect(page.locator('app-live-view')).toBeAttached();
+ });
+
+ test('clicking back to Setup tab re-renders setup form', async ({ page }) => {
+ // Switch away first
+ const historyTab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution History' });
+ await historyTab.click();
+ await expect(historyTab).toHaveClass(/active/);
+
+ // Switch back
+ const setupTab = page.locator('#execution-nav-tab .nav-link', { hasText: 'Execution Setup' });
+ await setupTab.click();
+ await expect(setupTab).toHaveClass(/active/);
+ await expect(page.locator('app-execution-setup')).toBeAttached();
+ });
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Execution Setup – form elements
+// ─────────────────────────────────────────────────────────────────────────────
+
+test.describe('Execution Page – setup form', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/execute');
+ await waitForExecutionDashboard(page);
+ });
+
+ test('blueprint name dropdown is rendered', async ({ page }) => {
+ const select = page.locator('#blueprintName');
+ await expect(select).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('blueprint version dropdown is rendered', async ({ page }) => {
+ const select = page.locator('#blueprintVersion');
+ await expect(select).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('action name dropdown is rendered', async ({ page }) => {
+ const select = page.locator('#actionName');
+ await expect(select).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('Execute button is rendered and initially disabled', async ({ page }) => {
+ const btn = page.locator('.btn-execute');
+ await expect(btn).toBeVisible({ timeout: 10_000 });
+ await expect(btn).toBeDisabled();
+ });
+
+ test('JSON payload editor is rendered', async ({ page }) => {
+ // ace-editor renders a div with class "ace_editor"
+ const editor = page.locator('.ace_editor');
+ await expect(editor.first()).toBeVisible({ timeout: 15_000 });
+ });
+
+ test('blueprint dropdown is populated from mock API', async ({ page }) => {
+ await waitForBlueprintOptions(page);
+ // We expect at least some options from the fixture data
+ const options = page.locator('#blueprintName option');
+ const count = await options.count();
+ // First option is the placeholder, rest are blueprint names
+ expect(count).toBeGreaterThan(1);
+ });
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Execution API integration via mock
+// ─────────────────────────────────────────────────────────────────────────────
+
+/** Helper: select first blueprint + wait for version + wait for actions to load. */
+async function selectBlueprintAndVersion(page: import('@playwright/test').Page) {
+ await page.selectOption('#blueprintName', { index: 1 });
+ // Wait for version dropdown to populate and auto-select
+ await expect(page.locator('#blueprintVersion option').nth(1)).toBeAttached({ timeout: 10_000 });
+ const versionOptions = await page.locator('#blueprintVersion option').count();
+ if (versionOptions > 1) {
+ await page.selectOption('#blueprintVersion', { index: 1 });
+ }
+ // Wait for action dropdown to populate from by-name API
+ await expect(page.locator('#actionName option').nth(1)).toBeAttached({ timeout: 10_000 });
+}
+
+test.describe('Execution Page – API integration', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/execute');
+ await waitForExecutionDashboard(page);
+ await waitForBlueprintOptions(page);
+ });
+
+ test('selecting a blueprint populates the action dropdown', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ const actionOptions = page.locator('#actionName option');
+ const count = await actionOptions.count();
+ // placeholder + at least one real action
+ expect(count).toBeGreaterThan(1);
+ });
+
+ test('selecting a blueprint enables the Execute button', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ // Select an action if not auto-selected
+ const actionOptions = await page.locator('#actionName option').count();
+ if (actionOptions > 1) {
+ await page.selectOption('#actionName', { index: 1 });
+ }
+ await page.waitForTimeout(300);
+ const btn = page.locator('.btn-execute');
+ await expect(btn).toBeEnabled({ timeout: 5_000 });
+ });
+
+ test('executing a blueprint sends request and receives response', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ // Select an action
+ const actionOptions = await page.locator('#actionName option').count();
+ if (actionOptions > 1) {
+ await page.selectOption('#actionName', { index: 1 });
+ }
+ await page.waitForTimeout(300);
+
+ // Click execute and wait for the API round-trip
+ const [apiResponse] = await Promise.all([
+ page.waitForResponse(
+ resp => resp.url().includes('/controllerblueprint/execute') && resp.status() === 200,
+ { timeout: 15_000 },
+ ),
+ page.locator('.btn-execute').click(),
+ ]);
+
+ expect(apiResponse.status()).toBe(200);
+
+ // Verify the response JSON contains expected mock data
+ const body = await apiResponse.json();
+ expect(body).toHaveProperty('status');
+ expect(body.status.code).toBe(200);
+ expect(body.status.message).toBe('success');
+ });
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Dynamic action inputs
+// ─────────────────────────────────────────────────────────────────────────────
+
+test.describe('Execution Page – action inputs', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/#/execute');
+ await waitForExecutionDashboard(page);
+ await waitForBlueprintOptions(page);
+ });
+
+ test('selecting an action shows action input fields', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ // Select the first action
+ await page.selectOption('#actionName', { index: 1 });
+ // Wait for the workflow-spec API call to complete and form to render
+ const inputsSection = page.locator('#actionInputs');
+ await expect(inputsSection).toBeAttached({ timeout: 10_000 });
+ });
+
+ test('complex input group renders nested fields', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ await page.selectOption('#actionName', { index: 1 });
+ // Wait for inputs section
+ await expect(page.locator('#actionInputs')).toBeAttached({ timeout: 10_000 });
+ // Complex group should have nested fields inside .complex-fields
+ const complexFields = page.locator('.complex-fields input, .complex-fields select');
+ const count = await complexFields.count();
+ expect(count).toBeGreaterThan(0);
+ });
+
+ test('typing in a field updates the JSON payload', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ await page.selectOption('#actionName', { index: 1 });
+ await expect(page.locator('#actionInputs')).toBeAttached({ timeout: 10_000 });
+
+ // Find the first text input in the inputs section and type a value
+ const firstInput = page.locator('#actionInputs input[type="text"]').first();
+ await firstInput.fill('test-value-123');
+
+ // Wait for payload to update
+ await page.waitForTimeout(500);
+
+ // Get the ace editor content and verify the typed value appears
+ const editorContent = await page.locator('.ace_editor .ace_text-layer').first().textContent();
+ expect(editorContent).toContain('test-value-123');
+ });
+
+ test('reset clears input field values', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ await page.selectOption('#actionName', { index: 1 });
+ await expect(page.locator('#actionInputs')).toBeAttached({ timeout: 10_000 });
+
+ // Fill a field
+ const firstInput = page.locator('#actionInputs input[type="text"]').first();
+ await firstInput.fill('will-be-cleared');
+ await page.waitForTimeout(300);
+
+ // Click reset
+ await page.locator('button', { hasText: 'Reset' }).click();
+ await page.waitForTimeout(300);
+
+ // Field should be cleared
+ await expect(firstInput).toHaveValue('');
+ });
+
+ test('payload JSON includes action-request structure', async ({ page }) => {
+ await selectBlueprintAndVersion(page);
+ // Select an action
+ await page.selectOption('#actionName', { index: 1 });
+ await expect(page.locator('#actionInputs')).toBeAttached({ timeout: 10_000 });
+ await page.waitForTimeout(500);
+
+ // Check that the payload contains a *-request key
+ const editorContent = await page.locator('.ace_editor .ace_text-layer').first().textContent();
+ expect(editorContent).toMatch(/-request/);
+ });
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Sidebar navigation
+// ─────────────────────────────────────────────────────────────────────────────
+
+test.describe('Execution Page – navigation', () => {
+ test('Execute link is visible in the sidebar', async ({ page }) => {
+ await page.goto('/#/packages');
+ await page.waitForLoadState('networkidle');
+ const executeLink = page.locator('.menu-dropdown a', { hasText: 'Execute' });
+ await expect(executeLink).toBeVisible({ timeout: 10_000 });
+ });
+
+ test('clicking Execute link in sidebar navigates to execute page', async ({ page }) => {
+ await page.goto('/#/packages');
+ await page.waitForLoadState('networkidle');
+ const executeLink = page.locator('.menu-dropdown a', { hasText: 'Execute' });
+ await executeLink.click();
+ await expect(page).toHaveURL(/\/#\/execute/);
+ await waitForExecutionDashboard(page);
+ });
+});
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-aggregator</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ui</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
});
}
+ @post('/controllerblueprint/execute')
+ async executeBlueprint(
+ @requestBody({
+ description: 'ExecutionServiceInput JSON payload',
+ required: true,
+ content: {
+ 'application/json': {
+ schema: { type: 'object' },
+ },
+ },
+ })
+ body: object,
+ @inject(RestBindings.Http.RESPONSE) response: Response,
+ ): Promise<Response> {
+ const options = {
+ url: processorApiConfig.http.url + '/execution-service/process',
+ headers: {
+ Authorization: processorApiConfig.http.authToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ };
+ return new Promise((resolve, reject) => {
+ request_lib.post(options)
+ .on('error', err => {
+ reject(err);
+ })
+ .pipe(response)
+ .once('finish', () => {
+ resolve(response);
+ });
+ });
+ }
+
+ @get('/controllerblueprint/workflows/{name}/{version}')
+ async getWorkflows(
+ @param.path.string('name') name: string,
+ @param.path.string('version') version: string,
+ @inject(RestBindings.Http.RESPONSE) response: Response,
+ ): Promise<Response> {
+ const options = {
+ url: processorApiConfig.http.url + '/blueprint-model/workflows/blueprint-name/' + name + '/version/' + version,
+ headers: {
+ Authorization: processorApiConfig.http.authToken,
+ },
+ };
+ return new Promise((resolve, reject) => {
+ request_lib.get(options)
+ .on('error', err => {
+ reject(err);
+ })
+ .pipe(response)
+ .once('finish', () => {
+ resolve(response);
+ });
+ });
+ }
+
+ @post('/controllerblueprint/workflow-spec')
+ async getWorkflowSpec(
+ @requestBody({
+ description: 'WorkFlowSpecRequest JSON payload',
+ required: true,
+ content: {
+ 'application/json': {
+ schema: { type: 'object' },
+ },
+ },
+ })
+ body: object,
+ @inject(RestBindings.Http.RESPONSE) response: Response,
+ ): Promise<Response> {
+ const options = {
+ url: processorApiConfig.http.url + '/blueprint-model/workflow-spec',
+ headers: {
+ Authorization: processorApiConfig.http.authToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ };
+ return new Promise((resolve, reject) => {
+ request_lib.post(options)
+ .on('error', err => {
+ reject(err);
+ })
+ .pipe(response)
+ .once('finish', () => {
+ resolve(response);
+ });
+ });
+ }
+
@post('/controllerblueprint/deploy-blueprint')
async deploy(
@requestBody({
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../../ms/blueprintsprocessor/parent</relativePath>
</parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>archetype-blueprint</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<packaging>maven-archetype</packaging>
<name>Components Model Catalog - Blueprints Model - Archetype Blueprints</name>
<parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>blueprint-model</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>cba-assembly-descriptor</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>cba-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../../cba-parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>blueprint-model</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>test-blueprint-kotlin-parent</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>test-blueprint-model</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>capability_cli</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>blueprint-model</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>test-blueprint-model</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.components.cba</groupId>
<artifactId>test-blueprint-model</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>resource-audit</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-aggregator</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-functions</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-blueprints</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-blueprints</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-blueprints</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-modules</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>modules-blueprints</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-blueprints</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-modules</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>modules-commons</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-commons</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
workFlowData.inputs = workFlow.inputs
workFlowData.outputs = workFlow.outputs
+ wfRes.workFlowData = workFlowData
+
if (workFlow.inputs != null) {
for ((k, v) in workFlow.inputs!!) {
addPropertyInfo(k, v, blueprintContext, wfRes)
addPropertyInfo(k, v, blueprintContext, wfRes)
}
}
-
- wfRes.workFlowData = workFlowData
return wfRes
}
.jsonPath("$.content.length()").isEqualTo(0)
}
+ /**
+ * Regression test for CCSDK-4184: workFlowData must be set on the response object before the
+ * input/output property loops run, because updatePropertyInfo() accesses
+ * res.workFlowData.workFlowName. Without the fix, every call raises
+ * "lateinit property workFlowData has not been initialized".
+ */
+ @Test
+ fun test07d_getWorkflowSpec_withInputsAndComplexType_returnsWorkFlowDataAndDataTypes() {
+ // 'resource-assignment' has one input of the complex type dt-resource-assignment-properties
+ // which triggers both addPropertyInfo → updatePropertyInfo AND addDataType paths.
+ webTestClient.post()
+ .uri("/api/v1/blueprint-model/workflow-spec")
+ .header(
+ "Authorization",
+ "Basic " + Base64.getEncoder()
+ .encodeToString(("ccsdkapps:ccsdkapps").toByteArray(UTF_8))
+ )
+ .header("Content-Type", "application/json")
+ .bodyValue(
+ """{"blueprintName":"baseconfiguration","version":"1.0.0","workflowName":"resource-assignment"}"""
+ )
+ .exchange()
+ .expectStatus().isOk
+ .expectBody()
+ .jsonPath("$.workFlowData").exists()
+ .jsonPath("$.workFlowData.workFlowName").isEqualTo("resource-assignment")
+ .jsonPath("$.workFlowData.inputs").exists()
+ .jsonPath("$.workFlowData.inputs['resource-assignment-properties']").exists()
+ .jsonPath("$.dataTypes").exists()
+ .jsonPath("$.dataTypes['dt-resource-assignment-properties']").exists()
+ }
+
+ @Test
+ fun test07e_getWorkflowSpec_withNoInputs_returnsEmptyWorkFlowData() {
+ // 'activate-restconf' has no inputs or outputs — the loops are skipped entirely,
+ // but workFlowData must still be populated correctly.
+ webTestClient.post()
+ .uri("/api/v1/blueprint-model/workflow-spec")
+ .header(
+ "Authorization",
+ "Basic " + Base64.getEncoder()
+ .encodeToString(("ccsdkapps:ccsdkapps").toByteArray(UTF_8))
+ )
+ .header("Content-Type", "application/json")
+ .bodyValue(
+ """{"blueprintName":"baseconfiguration","version":"1.0.0","workflowName":"activate-restconf"}"""
+ )
+ .exchange()
+ .expectStatus().isOk
+ .expectBody()
+ .jsonPath("$.workFlowData").exists()
+ .jsonPath("$.workFlowData.workFlowName").isEqualTo("activate-restconf")
+ }
+
+ @Test
+ fun test07f_getWorkflowSpec_unknownWorkflow_returns4xx() {
+ webTestClient.post()
+ .uri("/api/v1/blueprint-model/workflow-spec")
+ .header(
+ "Authorization",
+ "Basic " + Base64.getEncoder()
+ .encodeToString(("ccsdkapps:ccsdkapps").toByteArray(UTF_8))
+ )
+ .header("Content-Type", "application/json")
+ .bodyValue(
+ """{"blueprintName":"baseconfiguration","version":"1.0.0","workflowName":"nonexistent-workflow"}"""
+ )
+ .exchange()
+ .expectStatus().is5xxServerError
+ }
+
@Test
@Throws(JSONException::class)
fun test08_searchBlueprintModels() {
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-modules</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>modules-inbounds</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-inbounds</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-modules</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>modules-outbounds</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-services</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>blueprintsprocessor-modules</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>modules-services</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<artifactId>modules-services</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor.modules</groupId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>blueprintsprocessor</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ms</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ms</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.error.catalog</groupId>
<artifactId>error-catalog</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>error-catalog-application</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.error.catalog</groupId>
<artifactId>error-catalog</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>error-catalog-core</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ms</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.error.catalog</groupId>
<artifactId>error-catalog</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>error-catalog-services</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-aggregator</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ms</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>py-executor</artifactId>
<parent>
<groupId>org.onap.ccsdk.cds.sdclistener</groupId>
<artifactId>sdclistener-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds.sdclistener</groupId>
<artifactId>sdclistener-parent</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>sdclistener</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
</parent>
<groupId>org.onap.ccsdk.cds.sdclistener</groupId>
<parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-ms</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<groupId>org.onap.ccsdk.cds</groupId>
<artifactId>cds-aggregator</artifactId>
- <version>1.9.1-SNAPSHOT</version>
+ <version>1.10.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>ccsdk-cds</name>
release_name=1
-sprint_number=9
-feature_revision=1
+sprint_number=10
+feature_revision=0
base_version=${release_name}.${sprint_number}.${feature_revision}
release_version=${base_version}
snapshot_version=${base_version}-SNAPSHOT
-