From c7aabdd65a8355b6a948e2851885ebfd058f8e37 Mon Sep 17 00:00:00 2001 From: kaw67872 <kawtar.laariche@iais.fraunhofer.de> Date: Mon, 27 May 2024 16:33:46 +0200 Subject: [PATCH] #22: implement onboarding page --- src/app/core/config/api-config.ts | 2 + .../shared-data/shared-data.service.ts | 26 +- .../on-boarding-model.component.html | 237 +++++++++++++++- .../on-boarding-model.component.scss | 121 +++++++++ .../on-boarding-model.component.ts | 257 +++++++++++++++++- .../local-login/local-login.component.ts | 2 +- .../marketplace/marketplace.component.html | 4 +- .../marketplace/marketplace.component.ts | 2 + .../model-details.component.html | 4 +- .../breadcrumb-navigation.component.html | 38 ++- .../breadcrumb-navigation.component.scss | 4 + .../breadcrumb-navigation.component.ts | 6 +- .../create-edit-license-profile.component.ts | 2 + ...er-dialog-confirmation-action.component.ts | 4 +- .../headline/headline.component.html | 3 + .../headline/headline.component.scss | 14 + .../headline/headline.component.spec.ts | 23 ++ .../components/headline/headline.component.ts | 13 + .../components/home/home.component.html | 1 - .../shared/components/home/home.component.ts | 8 - ...del-details-license-profile.component.html | 7 +- ...model-details-license-profile.component.ts | 21 +- .../model-management.component.html | 4 +- .../upload-license-profile.component.html | 6 - .../upload-license-profile.component.ts | 88 ++++-- src/app/shared/models/index.ts | 1 + src/app/shared/models/message-status.model.ts | 15 + .../shared/models/navigation-label.model.ts | 4 + src/assets/images/ico-checkmark.png | Bin 0 -> 1159 bytes src/assets/images/ico_add_to_repository.png | Bin 0 -> 1685 bytes .../images/ico_add_to_repository_green.png | Bin 0 -> 1693 bytes .../images/ico_add_to_repository_red.png | Bin 0 -> 2155 bytes .../images/ico_create_micro_service.png | Bin 0 -> 1003 bytes .../images/ico_create_micro_service_green.png | Bin 0 -> 983 bytes .../images/ico_create_micro_service_red.png | Bin 0 -> 1460 bytes src/assets/images/ico_not_yet_onboarded.png | Bin 0 -> 1526 bytes src/assets/images/loading_deactive.png | Bin 0 -> 620 bytes .../images/stepper_progress_completed.png | Bin 0 -> 1302 bytes 38 files changed, 835 insertions(+), 82 deletions(-) create mode 100644 src/app/shared/components/headline/headline.component.html create mode 100644 src/app/shared/components/headline/headline.component.scss create mode 100644 src/app/shared/components/headline/headline.component.spec.ts create mode 100644 src/app/shared/components/headline/headline.component.ts create mode 100644 src/app/shared/models/message-status.model.ts create mode 100644 src/app/shared/models/navigation-label.model.ts create mode 100644 src/assets/images/ico-checkmark.png create mode 100644 src/assets/images/ico_add_to_repository.png create mode 100644 src/assets/images/ico_add_to_repository_green.png create mode 100644 src/assets/images/ico_add_to_repository_red.png create mode 100644 src/assets/images/ico_create_micro_service.png create mode 100644 src/assets/images/ico_create_micro_service_green.png create mode 100644 src/assets/images/ico_create_micro_service_red.png create mode 100644 src/assets/images/ico_not_yet_onboarded.png create mode 100644 src/assets/images/loading_deactive.png create mode 100644 src/assets/images/stepper_progress_completed.png diff --git a/src/app/core/config/api-config.ts b/src/app/core/config/api-config.ts index b2da3f1..01952b4 100644 --- a/src/app/core/config/api-config.ts +++ b/src/app/core/config/api-config.ts @@ -55,4 +55,6 @@ export const apiConfig = { urlComment: '/api/comments', urlGetActiveUsers: '/api/users/activeUserDetails', urlShareWithTeam: '/api/solution/userAccess', + urlAddToCatalog: '/api/webBasedOnBoarding/addToCatalog', + urlMessagingStatus: '/api/webBasedOnBoarding/messagingStatus', }; diff --git a/src/app/core/services/shared-data/shared-data.service.ts b/src/app/core/services/shared-data/shared-data.service.ts index 8cf26e5..bc50def 100644 --- a/src/app/core/services/shared-data/shared-data.service.ts +++ b/src/app/core/services/shared-data/shared-data.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { AuthorPublisherModel, + LicenseProfileModel, PublicSolution, Revision, } from 'src/app/shared/models'; @@ -47,19 +48,20 @@ export class SharedDataService { private _authorListSubject: BehaviorSubject<AuthorPublisherModel[]> = new BehaviorSubject<AuthorPublisherModel[]>([]); - private _selectedRevisionSubject: BehaviorSubject<{ - version: string; - revisionId: string; - }> = new BehaviorSubject<Revision>({ - version: '', - revisionId: '', - onBoarded: '', - }); + private _selectedRevisionSubject: BehaviorSubject<Revision> = + new BehaviorSubject<Revision>({ + version: '', + revisionId: '', + onBoarded: '', + }); private _ratingSubject: BehaviorSubject<number> = new BehaviorSubject<number>( 0, ); + private _licenseProfileSubject = + new BehaviorSubject<LicenseProfileModel | null>(null); + constructor() {} get versionId$(): Observable<string> { @@ -117,4 +119,12 @@ export class SharedDataService { set rating(value: number) { this._ratingSubject.next(value); } + + get licenseProfile$(): Observable<LicenseProfileModel | null> { + return this._licenseProfileSubject.asObservable(); + } + + set licenseProfile(value: LicenseProfileModel | null) { + this._licenseProfileSubject.next(value); + } } diff --git a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.html b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.html index b19248e..d2fb782 100644 --- a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.html +++ b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.html @@ -1 +1,236 @@ -<p>on-boarding-model works!</p> +<div style="display: flex; flex-direction: column; margin: 40px"> + <gp-headline [headlineTitle]="'On-Boarding Model'"></gp-headline> + <gp-breadcrumb-navigation + [firstNavigationLabel]="{ label: 'Home' }" + [secondNavigationLabel]="{ label: 'On-Boarding Model', disabled: true }" + (firstNavigationClicked)="onHomeClick()" + ></gp-breadcrumb-navigation> +</div> +<mat-divider></mat-divider> +<div style="display: flex; flex-direction: column; margin: 40px; width: 100%"> + <div></div> + <mat-divider></mat-divider> + <div style="display: flex; flex-direction: column; width: 100%"> + <mat-divider></mat-divider> + <!-- on boarding--> + <div> + <gp-headline + [headlineTitle]="'ON-BOARD DOCKERIZED MODEL URI'" + ></gp-headline> + </div> + <!--process model--> + <div> + @if (solutionTrackId) { + <mat-list role="list" style="display: flex"> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span + class="image-content micro-service-process-completed" + alt="" + title="" + ></span> + + <span>Create solution</span> + </div></mat-list-item + > + <mat-list-item + ><span class="progress-status progress-status-completed"></span + ></mat-list-item> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span + class="image-content add-repository-completed" + alt="Add Artifacts" + title="Add Artifacts" + ></span> + <span>Add Artifacts </span> + </div></mat-list-item + > + <mat-list-item + ><span class="progress-status progress-status-completed"></span + ></mat-list-item> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span + class="image-content onboarded-completed" + alt="Onboarding status" + title="Onboarding status" + ></span> + <button>View model</button> + <span>Model is on-boarded and available in Private catalog </span> + </div></mat-list-item + > + </mat-list> + } @else { + <mat-list role="list" style="display: flex"> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span class="image-content micro-service" alt="" title=""></span> + + <span>Create solution</span> + </div></mat-list-item + > + <mat-list-item + ><span class="progress-status process-status-not-yet-started"></span + ></mat-list-item> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span + class="image-content add-repository" + alt="Add Artifacts" + title="Add Artifacts" + ></span> + <span>Add Artifacts </span> + </div></mat-list-item + > + <mat-list-item + ><span class="progress-status process-status-not-yet-started"></span + ></mat-list-item> + <mat-list-item role="listitem" + ><div style="display: flex; flex-direction: column"> + <span + class="image-content not-yet-onboarded" + alt="Not yet on-boarded" + title="Not yet on-boarded" + ></span> + <span>Not yet on-boarded </span> + </div></mat-list-item + > + </mat-list> + } + </div> + <div style="display: flex"> + <div style="display: flex; flex-direction: column; width: 100%"> + <!-- onboarding model form--> + <form style="width: 100" [formGroup]="onboardingModelForm"> + <!--model name--> + <div style="display: flex; flex-direction: column"> + <mat-label>Model name</mat-label> + <mat-form-field> + <input matInput formControlName="name" /> + @if ( + onboardingModelForm.controls["name"].errors?.["pattern"] && + onboardingModelForm.controls["name"].touched + ) { + <mat-error class="modal-error" + >Name must consist of lower case alphanumeric characters, '-' + or '.', and must start and end with an alphanumeric + character</mat-error + > + } + @if ( + onboardingModelForm.controls["name"].errors?.["required"] && + onboardingModelForm.controls["name"].touched + ) { + <mat-error class="modal-error">Name is required</mat-error> + } + </mat-form-field> + </div> + <!--docker URI--> + <div style="display: flex; flex-direction: column"> + <mat-label>Docker URI </mat-label> + <mat-hint class="modal-note green" + ><strong>Dockerhub image example:</strong> + docker.io/myimage:latest + </mat-hint> + <mat-hint class="modal-note green" + ><strong>General public registry image example:</strong> + cicd.ai4eu-dev.eu:7444/myimage:v1 + </mat-hint> + <mat-form-field> + <input matInput formControlName="dockerURI" /> + @if ( + onboardingModelForm.controls["dockerURI"].errors?.[ + "required" + ] && onboardingModelForm.controls["dockerURI"].touched + ) { + <mat-error class="modal-error" + >Docker URI is required</mat-error + > + } + </mat-form-field> + </div> + <!--protobuf file--> + <div style="display: flex; flex-direction: column; width: 100%"> + <mat-label>Upload Protobuf File </mat-label> + <div style="display: flex; width: 100%; gap: 10px"> + <mat-form-field style="height: 70px"> + <input matInput [value]="fileName" /> + <mat-hint class="modal-note full-width no-padding"> + Supported files type: .proto</mat-hint + > + + <input + #fileDropRef + type="file" + id="fileInput" + name="fileInput" + (change)="selectFile($event)" + hidden + formControlName="protobufFile" + /> + </mat-form-field> + + <button + style="height: 48px" + color="primary" + mat-raised-button + (click)="uploadFile()" + > + Upload file + </button> + </div> + @if (message !== "") { + <span class="modal-error">.proto file is required.</span> + } + <!-- @if ( + onboardingModelForm.controls["protobufFile"].errors?.[ + "required" + ] && onboardingModelForm.controls["protobufFile"].touched + ) { + <span class="modal-error">.proto file is required.</span> + } --> + </div> + <!--license profile--> + <div style="display: flex; flex-direction: column"> + <mat-checkbox + color="primary" + formControlName="addLicenseProfile" + (change)="onClickUpload($event)" + >Add License Profile</mat-checkbox + > + + <!-- <mat-radio-group color="primary"> + <mat-radio-button value="auto" (click)="onCreateLicenseProfile()" + >Create license profile</mat-radio-button + > + <mat-radio-button + value="always" + (click)="onClickUploadLicenseProfile()" + >Upload license profile</mat-radio-button + > + </mat-radio-group> --> + </div> + @if ( + onboardingModelForm.controls["addLicenseProfile"].value === true + ) { + <gp-model-details-license-profile></gp-model-details-license-profile> + } + + <div style="display: flex"> + <button mat-raised-button (click)="resetData()">Reset form</button> + <button + mat-raised-button + color="primary" + [disabled]="!enableSubmit" + > + On-board model + </button> + </div> + </form> + </div> + <!--onboarding history--> + <div></div> + </div> + </div> +</div> diff --git a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.scss b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.scss index e69de29..36effac 100644 --- a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.scss +++ b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.scss @@ -0,0 +1,121 @@ +.flex-column { + display: flex; + flex-direction: column; +} +#fileInput { + position: absolute; + cursor: pointer; + z-index: 10; + opacity: 0; + height: 100%; + left: 0px; + top: 0px; +} +.asterisk--after:after { + content: "*"; + color: red; +} + +.modal-error { + color: red; + font-size: 12px; + font-family: "Open Sans", sans-serif !important; + padding: 0; +} + +.modal-note { + color: #888; + font-size: 12px; + font-family: "Open Sans", sans-serif !important; +} + +.full-width { + width: 100%; +} + +.green { + color: green; +} +::ng-deep.mat-mdc-form-field-hint-wrapper, +::ng-deep.mat-mdc-form-field-error-wrapper { + padding: 0 !important; +} +.micro-service { + background-image: url(../../../../assets/images/ico_create_micro_service.png); + background-repeat: no-repeat; + background-position: center; +} + +.micro-service-process-completed { + background-image: url(../../../../assets/images/ico_create_micro_service_green.png); + background-repeat: no-repeat; + background-position: center; +} + +/* .progress-status:after { + content: "" !important; + background: url("../../../../assets/images/loading_deactive.png") no-repeat + left center; + display: inline-block; + width: 45px; + height: 12px; + transform: scale(0.8); +} + */ +.process-status-not-yet-started { + background: url("../../../../assets/images/loading_deactive.png") no-repeat + left center; +} + +.progress-status-completed { + background: url("../../../../assets/images/stepper_progress_completed.png") + no-repeat left center; +} + +.progress-status { + display: inline-block; + width: 47px; + height: 11px; + transform: scale(0.8); +} + +.add-repository { + background-image: url(../../../../assets/images/ico_add_to_repository.png); + background-repeat: no-repeat; + background-position: center; +} + +.add-repository-completed { + background-image: url(../../../../assets/images/ico_add_to_repository_green.png); + background-repeat: no-repeat; + background-position: center; +} + +.not-yet-onboarded { + background-image: url(../../../../assets/images/ico_not_yet_onboarded.png); + background-repeat: no-repeat; + background-position: center; +} + +.onboarded-completed { + background-image: url(../../../../assets/images/ico-checkmark.png); + background-repeat: no-repeat; + background-position: center; +} + +.image-content { + width: 62px; + height: 62px; + border-radius: 62px; + + border: 3px solid #fff; + background-color: #ebebeb; + display: inline-block; + position: relative; + text-align: center; + line-height: 68px; +} + +.mdc-list-item.mdc-list-item--with-one-line { + height: 90px; +} diff --git a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.ts b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.ts index 26137a8..69ca6e2 100644 --- a/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.ts +++ b/src/app/features/dashboard/on-boarding-model/on-boarding-model.component.ts @@ -1,13 +1,262 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { HeadlineComponent } from 'src/app/shared/components/headline/headline.component'; +import { BreadcrumbNavigationComponent } from 'src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component'; +import { MatDividerModule } from '@angular/material/divider'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { + MatCheckboxChange, + MatCheckboxModule, +} from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { UploadLicenseProfileComponent } from 'src/app/shared/components/upload-license-profile/upload-license-profile.component'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { CreateEditLicenseProfileComponent } from 'src/app/shared/components/create-edit-license-profile/create-edit-license-profile.component'; +import { MatRadioModule } from '@angular/material/radio'; +import { ModelDetailsLicenseProfileComponent } from 'src/app/shared/components/model-details-license-profile/model-details-license-profile.component'; +import { SharedDataService } from 'src/app/core/services/shared-data/shared-data.service'; +import { Alert, AlertType, LicenseProfileModel } from 'src/app/shared/models'; +import { HttpEventType } from '@angular/common/http'; +import { AlertService } from 'src/app/core/services/alert.service'; +import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; +import { BrowserStorageService } from 'src/app/core/services/storage/browser-storage.service'; +import { Observable, Subject, Subscription, map, takeUntil } from 'rxjs'; +import { MatListModule } from '@angular/material/list'; @Component({ selector: 'gp-on-boarding-model', standalone: true, - imports: [CommonModule], + imports: [ + CommonModule, + HeadlineComponent, + BreadcrumbNavigationComponent, + MatDividerModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + UploadLicenseProfileComponent, + MatRadioModule, + ModelDetailsLicenseProfileComponent, + MatListModule, + ], templateUrl: './on-boarding-model.component.html', - styleUrl: './on-boarding-model.component.scss' + styleUrl: './on-boarding-model.component.scss', }) -export class OnBoardingModelComponent { +export class OnBoardingModelComponent implements OnInit { + @ViewChild('fileDropRef') fileDropRef!: ElementRef<HTMLInputElement>; + currentFile?: File; + fileSize = 0; + message = ''; + fileName = 'No chosen file'; + onboardingModelForm!: FormGroup; + modelUploadError: boolean = false; + modelUploadErrorMsg: string[] = []; + userId$: Observable<string | undefined>; + solutionTrackId!: string; + enableSubmit: boolean = false; + private onDestroy = new Subject<void>(); + private subscription: Subscription = new Subscription(); + modelLicense: LicenseProfileModel | null = null; + constructor( + private formBuilder: FormBuilder, + private sharedDataService: SharedDataService, + public dialog: MatDialog, + private alertService: AlertService, + private privateCatalogsService: PrivateCatalogsService, + private browserStorageService: BrowserStorageService, + ) { + this.onboardingModelForm = this.formBuilder.group({ + name: [ + '', + [ + Validators.required, + Validators.pattern('^(?=.*[a-z])[A-Za-z0-9]+[-.][A-Za-z0-9]+$'), + // this.notSameAsPublisher(this.publisherName), + ], + // [], + ], + dockerURI: ['', [Validators.required]], + protobufFile: [null, [Validators.required]], + addLicenseProfile: [false], + licenseProfile: [null], + }); + + this.userId$ = this.browserStorageService + .getUserDetails() + .pipe(map((details) => details?.userId)); + } + ngOnInit(): void { + this.subscription.add( + this.sharedDataService.licenseProfile$.subscribe((licenseProfile) => { + this.modelLicense = licenseProfile; + this.onboardingModelForm.patchValue({ + licenseProfile: licenseProfile, + }); + /** + * this.cd.detectChanges(); // Manually trigger change detection + */ + if (licenseProfile) { + console.log('License profile updated:', licenseProfile); + } else { + console.log('License profile reset to null'); + } + }), + ); + + this.onboardingModelForm.valueChanges + .pipe(takeUntil(this.onDestroy)) + .subscribe(() => { + this.checkFormValidity(); + }); + } + + checkFormValidity() { + const { name, dockerURI, addLicenseProfile, protobufFile } = + this.onboardingModelForm.value; + console.log({ protobufFile }); + if (!addLicenseProfile) { + console.log('currentFile', this.currentFile); + this.enableSubmit = name && dockerURI && protobufFile; + } else { + this.enableSubmit = + name && dockerURI && this.currentFile && this.modelLicense; + } + } + + onHomeClick() {} + + ngOnDestroy() { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + selectFile(event: any): void { + this.message = ''; + + if (event.target.files && event.target.files[0]) { + const file: File = event.target.files[0]; + this.currentFile = file; + this.fileName = this.currentFile.name; + this.onboardingModelForm.patchValue({ protobufFile: file }); + console.log('file info', this.currentFile); + const extensionFile = this.getFilenameExtension(this.fileName); + if (extensionFile !== 'proto') { + this.message = '.proto file is required.'; + } else if (this.currentFile && extensionFile === 'proto') { + console.log('inside proto'); + this.privateCatalogsService + .uploadProtoBufFile(this.currentFile) + .subscribe({ + next: (event) => this.processEvent(event), + error: (error) => {}, // To catch any errors not caught by catchError + complete: () => {}, + }); + } + } else { + this.fileName = 'No chosen file'; + } + } + /* + handleFileInput(event: Event | File) { + let file: File | null = null; + if (event instanceof File) { + file = event; + } else { + const element = event.target as HTMLInputElement; + file = element.files?.[0] || null; + } + if (file) { + this.currentFile = file; + if (this.currentFile.size / 1024 / 1024 < 1) { + this.fileSize = Math.ceil(file.size / 1024); + } + } + } */ + + uploadFile() { + event?.preventDefault(); + this.fileDropRef.nativeElement.click(); + if (this.currentFile) { + console.log('current file', this.currentFile); + this.privateCatalogsService + .uploadProtoBufFile(this.currentFile) + .subscribe({ + next: (event) => this.processEvent(event), + error: (error) => {}, // To catch any errors not caught by catchError + complete: () => {}, + }); + } + } + + getFilenameExtension(filename: string): string { + // Split the filename by dot (.) and get the last element of the array + const parts = filename.split('.'); + return parts[parts.length - 1]; + } + + onClickUpload(event: MatCheckboxChange) {} + + onCreateLicenseProfile() { + const dialogRef: MatDialogRef<CreateEditLicenseProfileComponent> = + this.dialog.open(CreateEditLicenseProfileComponent, { + data: { + dataKey: { + isEditMode: false, + solutionId: '', + revisionId: '', + }, + }, + }); + dialogRef.afterClosed().subscribe((result) => { + // This will be executed when the dialog is closed + // Reload data to fetch the updated license profile + }); + } + + onClickUploadLicenseProfile() { + const dialogRef: MatDialogRef<UploadLicenseProfileComponent> = + this.dialog.open(UploadLicenseProfileComponent); + } + + resetData() { + this.onboardingModelForm.reset(); + this.sharedDataService.licenseProfile = null; + } + + processEvent(event: any): void { + if (event && event.type === HttpEventType.Response) { + this.handleUploadSuccess(event.body); + } + } + + private handleUploadSuccess(response: any) { + const alert: Alert = { + message: 'File uploaded successfully', + type: AlertType.Success, + }; + this.alertService.notify(alert, 5000); + } + + private handleUploadError(error: any) { + // Error handling logic here + this.modelUploadError = true; + if (error) { + this.modelUploadErrorMsg = [error]; + } else if (error.error) { + const messageError = error.error.response_detail ?? error.error; + this.modelUploadErrorMsg = [messageError]; + } + + // Additional error handling + } } diff --git a/src/app/features/login/local-login/local-login.component.ts b/src/app/features/login/local-login/local-login.component.ts index 45e4c6b..866f1d4 100644 --- a/src/app/features/login/local-login/local-login.component.ts +++ b/src/app/features/login/local-login/local-login.component.ts @@ -19,7 +19,7 @@ import { import { AuthService } from 'src/app/core/services/auth/auth.service'; import { LocalLoginService } from 'src/app/core/services/auth/local-login.service'; -import { NavigationStart, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { diff --git a/src/app/features/marketplace/marketplace.component.html b/src/app/features/marketplace/marketplace.component.html index b40fe12..c6b476c 100644 --- a/src/app/features/marketplace/marketplace.component.html +++ b/src/app/features/marketplace/marketplace.component.html @@ -1,9 +1,7 @@ <section class="pageheadsection mob-pageheadsection1"> <div class="mdl-grid mdl-grid.mdl-grid--no-spacing"> <div> - <div class="headline"> - <span>Marketplace | </span> - </div> + <gp-headline [headlineTitle]="'Marketplace |'"></gp-headline> <div style="display: flex; flex-direction: row; align-items: center" *ngIf="loginUserId" diff --git a/src/app/features/marketplace/marketplace.component.ts b/src/app/features/marketplace/marketplace.component.ts index 069ef68..da672de 100644 --- a/src/app/features/marketplace/marketplace.component.ts +++ b/src/app/features/marketplace/marketplace.component.ts @@ -34,6 +34,7 @@ import { BrowserStorageService } from 'src/app/core/services/storage/browser-sto import { Observable, Subscription, map } from 'rxjs'; import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; import { MatButtonModule } from '@angular/material/button'; +import { HeadlineComponent } from 'src/app/shared/components/headline/headline.component'; @Component({ selector: 'gp-marketplace', @@ -50,6 +51,7 @@ import { MatButtonModule } from '@angular/material/button'; SolutionsToggleViewComponent, ListItemComponent, MatButtonModule, + HeadlineComponent, ], templateUrl: './marketplace.component.html', styleUrl: './marketplace.component.scss', diff --git a/src/app/features/model-details/model-details.component.html b/src/app/features/model-details/model-details.component.html index 4b6be61..92898d7 100644 --- a/src/app/features/model-details/model-details.component.html +++ b/src/app/features/model-details/model-details.component.html @@ -89,8 +89,8 @@ <div class="md-head-container2" style="margin-top: 12px"> <gp-breadcrumb-navigation [solution]="data.solution" - [firstNavigationLabel]="'Home'" - [secondNavigationLabel]="'MarketPlace'" + [firstNavigationLabel]="{ label: 'Home' }" + [secondNavigationLabel]="{ label: 'MarketPlace' }" (firstNavigationClicked)="onHomeClick()" (secondNavigationClicked)="onMarketPlaceClick()" ></gp-breadcrumb-navigation> diff --git a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.html b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.html index f6cb9cd..ee84a48 100644 --- a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.html +++ b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.html @@ -1,18 +1,26 @@ <ul class="c-breadcrumb"> - <li> - <a (click)="onFirstNavigationLabelClick()">{{ firstNavigationLabel }}</a> - </li> - <li> - <a (click)="onSecondNavigationClick()">{{ secondNavigationLabel }}</a> - </li> - <li> - <span - class="md-breadcrumb-item" - matTooltip="{{ solution?.name?.toString()?.split('_')?.join(' ') }}" - >{{ solution?.name?.toString()?.split("_")?.join(" ") }}</span - > - </li> - <li> - <gp-solution-id [solutionId]="solution.solutionId"></gp-solution-id> + <li [ariaDisabled]="firstNavigationLabel.disabled"> + <a (click)="onFirstNavigationLabelClick()">{{ + firstNavigationLabel.label + }}</a> </li> + @if (secondNavigationLabel) { + <li [ariaDisabled]="secondNavigationLabel.disabled"> + <a (click)="onSecondNavigationClick()">{{ + secondNavigationLabel.label + }}</a> + </li> + } + @if (solution) { + <li> + <span + class="md-breadcrumb-item" + matTooltip="{{ solution?.name?.toString()?.split('_')?.join(' ') }}" + >{{ solution?.name?.toString()?.split("_")?.join(" ") }}</span + > + </li> + <li> + <gp-solution-id [solutionId]="solution.solutionId"></gp-solution-id> + </li> + } </ul> diff --git a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.scss b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.scss index 6124c00..bbae9c7 100644 --- a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.scss +++ b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.scss @@ -54,3 +54,7 @@ display: inline-block; padding: 0 5px; } + +[aria-disabled="true"] { + pointer-events: none; +} diff --git a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.ts b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.ts index 2fbc2db..be996a5 100644 --- a/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.ts +++ b/src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SolutionIdComponent } from '../solution-id/solution-id.component'; -import { PublicSolutionDetailsModel } from '../../models'; +import { NavigationLabelModel, PublicSolutionDetailsModel } from '../../models'; import { MatTooltipModule } from '@angular/material/tooltip'; @Component({ @@ -12,8 +12,8 @@ import { MatTooltipModule } from '@angular/material/tooltip'; styleUrl: './breadcrumb-navigation.component.scss', }) export class BreadcrumbNavigationComponent { - @Input() firstNavigationLabel!: string; - @Input() secondNavigationLabel!: string; + @Input() firstNavigationLabel!: NavigationLabelModel; + @Input() secondNavigationLabel!: NavigationLabelModel; @Input() solution!: PublicSolutionDetailsModel; @Output() firstNavigationClicked = new EventEmitter<void>(); diff --git a/src/app/shared/components/create-edit-license-profile/create-edit-license-profile.component.ts b/src/app/shared/components/create-edit-license-profile/create-edit-license-profile.component.ts index 9dc835d..6328626 100644 --- a/src/app/shared/components/create-edit-license-profile/create-edit-license-profile.component.ts +++ b/src/app/shared/components/create-edit-license-profile/create-edit-license-profile.component.ts @@ -117,6 +117,7 @@ export class CreateEditLicenseProfileComponent implements OnInit { this.isEditMode = this.data.dataKey.isEditMode; this.solutionId = this.data.dataKey.solutionId; this.revisionId = this.data.dataKey.revisionId; + this.initializeForm(); this.setupFormValueChangesSubscription(); if (this.modelLicense) this.updateFormValues(); @@ -235,6 +236,7 @@ export class CreateEditLicenseProfileComponent implements OnInit { next: (event) => { if (event.type === HttpEventType.Response) { this.handleUploadSuccess(event.body); + this.sharedDataService.licenseProfile = licenseProfile; this.dialogRef.close(); } }, diff --git a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts index fbfaa3a..ac06ec9 100644 --- a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts +++ b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts @@ -8,9 +8,7 @@ import { import { MatButtonModule } from '@angular/material/button'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatIconModule } from '@angular/material/icon'; -import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; -import { SharedDataService } from 'src/app/core/services/shared-data/shared-data.service'; -import { Alert, AlertType, UserDetails } from '../../models'; +import { AlertType } from '../../models'; import { AlertService } from 'src/app/core/services/alert.service'; @Component({ diff --git a/src/app/shared/components/headline/headline.component.html b/src/app/shared/components/headline/headline.component.html new file mode 100644 index 0000000..1448e86 --- /dev/null +++ b/src/app/shared/components/headline/headline.component.html @@ -0,0 +1,3 @@ +<div class="headline"> + <span>{{ headlineTitle }} </span> +</div> diff --git a/src/app/shared/components/headline/headline.component.scss b/src/app/shared/components/headline/headline.component.scss new file mode 100644 index 0000000..f370b20 --- /dev/null +++ b/src/app/shared/components/headline/headline.component.scss @@ -0,0 +1,14 @@ +.headline { + font-size: 20px; + height: 30px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.02em; + margin: 3px 0; + padding: 0; + color: #671c9d; + text-overflow: ellipsis; + float: left; + overflow: hidden; + white-space: nowrap; +} diff --git a/src/app/shared/components/headline/headline.component.spec.ts b/src/app/shared/components/headline/headline.component.spec.ts new file mode 100644 index 0000000..e3fa01a --- /dev/null +++ b/src/app/shared/components/headline/headline.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeadlineComponent } from './headline.component'; + +describe('HeadlineComponent', () => { + let component: HeadlineComponent; + let fixture: ComponentFixture<HeadlineComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HeadlineComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HeadlineComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/headline/headline.component.ts b/src/app/shared/components/headline/headline.component.ts new file mode 100644 index 0000000..4979684 --- /dev/null +++ b/src/app/shared/components/headline/headline.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'gp-headline', + standalone: true, + imports: [CommonModule], + templateUrl: './headline.component.html', + styleUrl: './headline.component.scss', +}) +export class HeadlineComponent { + @Input() headlineTitle!: string; +} diff --git a/src/app/shared/components/home/home.component.html b/src/app/shared/components/home/home.component.html index 68855e5..00ac690 100644 --- a/src/app/shared/components/home/home.component.html +++ b/src/app/shared/components/home/home.component.html @@ -15,7 +15,6 @@ > Marketplace </button> - <button mat-button (click)="onClickUpload()">Onboard Model</button> </div> </div> </div> diff --git a/src/app/shared/components/home/home.component.ts b/src/app/shared/components/home/home.component.ts index ef2cba0..6129ac5 100644 --- a/src/app/shared/components/home/home.component.ts +++ b/src/app/shared/components/home/home.component.ts @@ -35,7 +35,6 @@ export class HomeComponent { constructor( private publicSolutionsService: PublicSolutionsService, private router: Router, - public dialog: MatDialog, private browserStorageService: BrowserStorageService, ) { this.userId$ = this.browserStorageService @@ -77,11 +76,4 @@ export class HomeComponent { }), ); } - - onClickUpload() { - if (this.userId$) { - const dialogRef: MatDialogRef<UploadLicenseProfileComponent> = - this.dialog.open(UploadLicenseProfileComponent); - } - } } diff --git a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.html b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.html index 15b3f0c..8bb11d0 100644 --- a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.html +++ b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.html @@ -35,7 +35,9 @@ <div style="margin-left: 20px" *ngIf="!modelLicense"> {{ modelLicenseError }} </div> -<div style="margin-left: 20px"> +<!--<div *ngIf="(sharedDataService.licenseProfile$ | async) as modelLicense; else noLicenseTemplate"> +--> +<div style="margin-left: 20px" *ngIf="modelLicense; else noLicenseTemplate"> <pre class="licensedisplay"> <span *ngIf="modelLicense?.keyword"><strong>keyword: </strong></span>{{ modelLicense?.keyword }} <span *ngIf="modelLicense?.licenseName"><strong>licenseName: </strong></span>{{ modelLicense?.licenseName }} @@ -53,3 +55,6 @@ <span *ngIf="modelLicense?.additionalInfo"><strong>additionalInfo: </strong></span>{{ modelLicense?.additionalInfo }} </pre> </div> +<ng-template #noLicenseTemplate> + <p>No license found.</p> +</ng-template> diff --git a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts index 138f5ed..fefd446 100644 --- a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts +++ b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedDataService } from 'src/app/core/services/shared-data/shared-data.service'; import { PublicSolutionsService } from 'src/app/core/services/public-solutions.service'; @@ -19,7 +19,7 @@ import { LicenseProfileModel } from '../../models'; styleUrl: './model-details-license-profile.component.scss', }) export class ModelDetailsLicenseProfileComponent { - modelLicense!: LicenseProfileModel; + modelLicense: LicenseProfileModel | null = null; isLicenseFound = false; isLoadingLicense = true; isLicenseJson = false; @@ -29,13 +29,15 @@ export class ModelDetailsLicenseProfileComponent { versionId: string = ''; versionIdSubscription: Subscription | undefined; isUserIdAvailable$: Observable<boolean | undefined>; + private subscription: Subscription = new Subscription(); constructor( private activatedRoute: ActivatedRoute, private publicSolutionsService: PublicSolutionsService, - private sharedDataService: SharedDataService, + protected sharedDataService: SharedDataService, public dialog: MatDialog, private browserStorageService: BrowserStorageService, + private cd: ChangeDetectorRef, ) { this.isUserIdAvailable$ = this.browserStorageService .getUserDetails() @@ -43,6 +45,19 @@ export class ModelDetailsLicenseProfileComponent { } ngOnInit() { + this.subscription.add( + this.sharedDataService.licenseProfile$.subscribe((profile) => { + this.modelLicense = profile; + /** + * this.cd.detectChanges(); // Manually trigger change detection + */ + if (profile) { + console.log('License profile updated:', profile); + } else { + console.log('License profile reset to null'); + } + }), + ); this.activatedRoute.parent?.paramMap.subscribe((paramMap) => { this.solutionId = paramMap.get('solutionId') || ''; this.revisionId = paramMap.get('revisionId') || ''; diff --git a/src/app/shared/components/model-management/model-management.component.html b/src/app/shared/components/model-management/model-management.component.html index caf10af..5d8139d 100644 --- a/src/app/shared/components/model-management/model-management.component.html +++ b/src/app/shared/components/model-management/model-management.component.html @@ -74,8 +74,8 @@ <div> <gp-breadcrumb-navigation [solution]="data" - [firstNavigationLabel]="'Home'" - [secondNavigationLabel]="'Manage my model'" + [firstNavigationLabel]="{ label: 'Home' }" + [secondNavigationLabel]="{ label: 'Manage my model' }" (firstNavigationClicked)="onHomeClick()" (secondNavigationClicked)="onManageMyModelClick()" ></gp-breadcrumb-navigation> diff --git a/src/app/shared/components/upload-license-profile/upload-license-profile.component.html b/src/app/shared/components/upload-license-profile/upload-license-profile.component.html index 9935a52..8161fa9 100644 --- a/src/app/shared/components/upload-license-profile/upload-license-profile.component.html +++ b/src/app/shared/components/upload-license-profile/upload-license-profile.component.html @@ -57,12 +57,6 @@ mode="determinate" [value]="progressBarValue" ></mat-progress-bar> - <!-- <div class="pro-status" *ngIf="progressBarValue > 0"> - <span *ngIf="progressBarValue != 100 && progressBarValue != 0" - >Uploading</span - > - <span *ngIf="progressBarValue === 100">Done</span> - </div>--> </div> <div style=" diff --git a/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts b/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts index a8739d7..079e1d0 100644 --- a/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts +++ b/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts @@ -2,6 +2,7 @@ import { Component, ElementRef, Inject, + Input, OnDestroy, OnInit, ViewChild, @@ -29,6 +30,7 @@ import { of, switchMap, takeUntil, + tap, throwError, } from 'rxjs'; import { BrowserStorageService } from 'src/app/core/services/storage/browser-storage.service'; @@ -56,8 +58,8 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { filename: string | null = null; progressBar = 0; modelUploadError: boolean = false; - solutionId: string = ''; - revisionId: string = ''; + @Input() solutionId: string = ''; + @Input() revisionId: string = ''; versionId: string = ''; versionIdSubscription: Subscription | undefined; modelLicense!: LicenseProfileModel; @@ -67,6 +69,7 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { modelUploadErrorMsg: string[] = []; fileSize = 0; userId$: Observable<string | undefined>; + licenseProfile!: LicenseProfileModel; private onDestroy = new Subject<void>(); constructor( @@ -107,18 +110,36 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { } uploadLicenseFile(): void { + event?.preventDefault(); combineLatest([this.userId$, of(this.file)]) .pipe( filter(([userId, file]) => !!userId && !!file), switchMap(([userId, file]) => { - if (userId && file) { - return this.privateCatalogsService.uploadFileToUrl( + if ( + userId && + file && + this.versionId && + this.solutionId && + this.versionId + ) { + return this.privateCatalogsService.uploadExistingModelLicenseProfile( userId, this.solutionId, this.revisionId, this.versionId, file, ); + } else if ( + userId && + file && + !this.versionId && + !this.solutionId && + !this.versionId + ) { + return this.privateCatalogsService.uploadNewModelLicenseProfile( + userId, + file, + ); } else { return throwError(() => new Error('Missing user ID or file')); } @@ -126,26 +147,28 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { takeUntil(this.onDestroy), catchError((error) => { console.error('Upload failed', error); + this.handleUploadError(error.error); // Handle the error and continue return of(null); }), finalize(() => this.resetProgress()), ) - .subscribe((event) => { - if ( - event && - event.type === HttpEventType.UploadProgress && - event.total - ) { - this.progressBarValue = Math.round( - (100 * event.loaded) / event.total, - ); - } else if (event && event.type === HttpEventType.Response) { - this.handleUploadSuccess(event.body); - } + .subscribe({ + next: (event) => this.processEvent(event), + error: (error) => {}, // To catch any errors not caught by catchError + complete: () => {}, }); } + processEvent(event: any): void { + if (event && event.type === HttpEventType.UploadProgress && event.total) { + this.progressBarValue = Math.round((100 * event.loaded) / event.total); + } else if (event && event.type === HttpEventType.Response) { + this.handleUploadSuccess(event.body); + this.sharedDataService.licenseProfile = this.licenseProfile; + } + } + private handleUploadSuccess(response: any) { const alert: Alert = { message: 'License uploaded successfully', @@ -157,10 +180,13 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { private handleUploadError(error: any) { // Error handling logic here this.modelUploadError = true; - const messageError = error.error.response_detail - ? error.error.response_detail - : error.error; - this.modelUploadErrorMsg = [messageError]; + if (error) { + this.modelUploadErrorMsg = [error]; + } else if (error.error) { + const messageError = error.error.response_detail ?? error.error; + this.modelUploadErrorMsg = [messageError]; + } + // Additional error handling } @@ -174,7 +200,26 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { this.filename = file.name; } - handleFileInput(event: Event | File) { + readFile(file: File) { + const reader = new FileReader(); + reader.onload = () => { + try { + this.licenseProfile = JSON.parse(reader.result as string); + console.log('License Profile Loaded:', this.licenseProfile); + // Proceed with any operations now that the license profile is loaded + } catch (e) { + console.error('Error parsing the license profile:', e); + // Handle errors in reading or parsing the file + } + }; + reader.onerror = () => { + console.error('Error reading the file:', reader.error); + }; + reader.readAsText(file); + } + + handleFileInput(event: any) { + event.preventDefault(); let file: File | null = null; if (event instanceof File) { file = event; @@ -184,6 +229,7 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { } if (file) { this.file = file; + this.readFile(this.file); if (this.file.size / 1024 / 1024 < 1) { this.fileSize = Math.ceil(file.size / 1024); } diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index d9d61d6..937eb62 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -3,3 +3,4 @@ export * from './filter.model'; export * from './user-details'; export * from './public-solution.model'; export * from './comment'; +export * from './navigation-label.model'; diff --git a/src/app/shared/models/message-status.model.ts b/src/app/shared/models/message-status.model.ts new file mode 100644 index 0000000..93761dc --- /dev/null +++ b/src/app/shared/models/message-status.model.ts @@ -0,0 +1,15 @@ +export interface MessageStatusModel { + taskId: string; + stepResultId: string; + trackingId: string; + stepCode: string; + solutionId: string; + revisionId: string; + artifactId: string; + userId: string; + name: string; + statusCode: string; + result: string; + startDate: string; + endDate: string; +} diff --git a/src/app/shared/models/navigation-label.model.ts b/src/app/shared/models/navigation-label.model.ts new file mode 100644 index 0000000..c4c4dd9 --- /dev/null +++ b/src/app/shared/models/navigation-label.model.ts @@ -0,0 +1,4 @@ +export interface NavigationLabelModel { + label: string; + disabled?: boolean; +} diff --git a/src/assets/images/ico-checkmark.png b/src/assets/images/ico-checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..68f1686279b683ee53e82a4a2a29f2b5e758a368 GIT binary patch literal 1159 zcmaJ>PiPcZ93HJHsU~C;N;PPGoq#B2=g%f_Gh^1w?#!;PWYT3_NQ7FQoq5TQ$;=yP zUffAera@UT)f|LK3$2ydv=pkIEA(L0iV8hgXnT+lJP0bb7Y!1`H!&MMxDL#`KYZW! z`}4jxC$rtV>sL3frYNdDvq#O5aUFT<YCj}jwC>4OGHk%9Uc3+W;gaq_Dq*4m1Q}cJ zhdHR5!$)qx4vP9<i<R%iy;_%KAe+@g3>(-kVN+B`XW;6_0K}jG`z<HVJpJu=23TgC z>1oq=%~fF0+B4$8eIwm@V`RV(O{Q}f=m;btU_-2fz#epbDTp(Rx)Pa(+Z+QHA$TCp zyiTfD%K`;?5VWy7B8Je?4q_rJw2AG})@Bgpg&iC(azbl_7bISkcmXUu42kBMeNs+M zFU2COI8(&fl{l_cDzT*&7J2=gAc`W#N4aP;LJ$#u*ui=bar~x=f(m`Zvs`Q;2ZV}x z0S)0eLo$6`g6%HLI{s3dNW-{5cR7LO!;&gMO?yAowwKX9&cQeNzEjxG54(`dK_3ly z206ICrZAK%DIU}@^704`R=SuiB8>bZa)F|RU5kOOJ(gpllD}<{qiIsc@v-h0Fr&s9 zqQY91De=*Sl4ucAsicw;gj7sa1tFmb>4X?dtLgSsg{vZC$c7HCaLqT|bX9I>2iql) zRp?nqp_%rO4HnCmtm?TaRrQv*X7yaus$7mF!-a?Y)?rt+h=aoEvUSN~+5FHUj`xVQ z?`@u(CfDM5Mor{{e+y?1{&-j3@?TBib8x?5=9m3{@YK|FL)rgfp$i;s`U~jb%bJ<) z;@pX@?1>otl$!flzH{apy$S64`uYTtuWoq2d<5=OL($r;O8v`IQ|(lFk`iLyeAY1f z_vZ4YmPCE>*@c~SDf!R1{6)jeqbut@x~v_WXPRf_@oVznsi~jVm(keDwdJwPf!|1f zLj8}<QBAEcu8+>w(YNW^!HF9i!FR{m6>A<({^`!2zB$n&KOVnJn`829Gd(@aKiEir zn;bhfI*|aE>$aWO57o3Te7bh;#fRq`%bhE)w9#nQoG34RpWRzi%Tv2Q-VQo99XK5R O8ZxPF_1EN~uU-K*D0?pe literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_add_to_repository.png b/src/assets/images/ico_add_to_repository.png new file mode 100644 index 0000000000000000000000000000000000000000..832ab796b44a493bd18f89433e4fd3688b902be1 GIT binary patch literal 1685 zcmX|C3sBP89{!{FA~juKY^(VXBsCv^Z%EW`Xo#Xk-h70n0`gFS0EMiK<lbwH?Ure- zTt`i_aP2CZT`_alyqaXb$6B>wd(Nn(o3*ytn(Y4F+w7S+=XZYR{O0@4_swtSsP-r9 zvq5Y_003YUOCi&t=nBnkmgdmfloYN409YI^DMgw>izhOL`TkifVK&=eo-cy#0U$h5 zF3Mu&vZY`)o5K^3P;YMap+Fvsgh~#k;b@}0Y%Y&dBxWBhN=RZB<uVB@RAdAqTuy`l z^4ZcXP@bPBkPzi0)StLSXuo-kMS*{oNOMW3J)4AJ3T;2QS14wK!TuqBOnhJ%7)tQR z2NS{qf_%XM96kh#BVh4CemFc4M<C+x;OBvgK!D+5Rt}L)j`>UmWh4|=Dism2SeZ=b zFAMY+iaA(3fk43G0<ZxAeo%#<<d{I3CHE6Z(3=qd#~`yMOfgR+<p~AgCT3Q)us}*e zp|G1I*v~AGJnUckpyV%y*aFBFF{B@iZ6gAJnOQ7(PZC9SvFb?tbh4v9GQXi~aSh#t zaf*kBHlt~nGF)d)3@zm1&D6`o7uezIwf5@K!RWL0U8JZJcFfV|1KTM%UdoN5Da}eu z%uW`{139z8*<W%<IgEh~o>Ba|)Ua{UB>JsKXkU$R{KJViqz`qHQ#~fhRtlG9lb_7! z$G-Oilu?+KQgd6w>teqH?hflVBJF{*)%9D}+`?|eBrsNXN4XqsC4`(Z;PjsgZtH}o zW*^bTQT?GjkALq1dIUFqxUkm$^Yx_6?I{aAQ+&p?OLcdG{Cn>my;)%3KKL|KWDPX= zS{T<qjPt;;oX#=r|6+F+t#XAl+Su_(<Nh*(z_i+Qrvf)=#^{*KlLeR1cP0Gj)J-MV z=;8nT`{*+f<LBk&0_4|26J)aHSvdt>n^2meodIEOCQoMbx~9nePP^e$Ro(mue028y z{g<=<&7EO`4wrPgZ==B5N{g*XcXz!&_d-ha-nA=R|BV~*+Jhp?+1AS6T)D>|*e3rc z%KN>&9bmm>%N9nj+`fK7U*VZ(XOAkkoT!hSzf<M{$iuhu`Fs(vcF;NFs_IoT`CMHo z7wzM7ElVtZ=W=`U@#A<N&wGBn*7WIY2b0lPT-(sFL-14YusV?A<?Gwj*;(;If6l?$ z9A+JbZZ}hyB&!C^JKBD6nY8$1YJH?fy7	dLp9U(b*XRmTq;g%EWn5y{*iZ&p)hR z>FYaENe>7L^2pF819LYe8mUZHMyFQNVONsq{eww8%yD{px}QS+ea4qa<YdWRm4hez z{Hs^5!hT(ty4=0+-9gRsZaV+)^~yT)meUWry1KNmot~a=*Edppo$rl|bfmQ&onKv* z0%T{bZi?H~+v{37um`lGoZX1NemJd_HZ(L80|*Q~(KsaQcjHE$p{GD4p;CkXF_)K7 z@@-Ys^^@y`ZRxkFERSEXDmqgYeXn0>@fsM?>(y#?|8il4S{=(~W8B=_SS;4c%Co(I zKchQ}$wUVR213TRw6utBGDgfK6;_43ilwC`8^Bx%J9CvvM!LE-_4S>y@^-a#uq}nP zr?rlbj}INQ%9-w*-COZ~Py>AY__0Z&nVy-^cXB40wOUU{L_=j`W21=z%HOTMWyb8= z$K!HAG+J3+UhedEmF!MeAa36n&4Oo6U!}c#`Eqh@F4oHt6&xH~oEm@C>rQq@CJsr7 ziyPnuePM0BxVX3@G&B^SK}lRCIXpd{8PX8nWSl?p<lL32v6@imRXm}PQ(WAk%{>|L zg~~UTCy@jW3=Aw8ZiU(#AGPn;%>lYC!eBmD29B7&?TuzVRVQ$RY*}4hwX=k`r}#R$ zxaegp-J`iq&ep<PZi<Hw)5hx~6N`%@JQM3!FNVHOyQ`f<M5I2`7fv=h3DVPR!(44) ztS3L5eE!QvR$g9wC0+cx@g$GWzhG)%d!|40{QK84WfSvpzQ1ub_T?C~>Z$XU`;{?y zJf6K~+FMaQmNPsuQqkCmf`UWEu!o}CAb?sym3@%<ovf0+P*t`d?O`>#o3@g_!cR|6 z-_Yr%XJ_v&KhmjdY6e&V2N;aea`ZkgA0Mdy<1Y8XBVb1OAp}1JkMD4OXA%h$VlW;$ zO%!2DkE9sGTXT|6=XbnmIc0WjO}|VltmZyaU{7y7ty3Az;$b^3nRc5zrp0`6PqfK; VeEwhR%bUMRY;*#-?QfYSe*l+o*>nH^ literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_add_to_repository_green.png b/src/assets/images/ico_add_to_repository_green.png new file mode 100644 index 0000000000000000000000000000000000000000..a53a33ce31ee54497da50b09ecf0668bc092dd5e GIT binary patch literal 1693 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_add_to_repository_red.png b/src/assets/images/ico_add_to_repository_red.png new file mode 100644 index 0000000000000000000000000000000000000000..36369caee5882085b7431e43caa7bf08eebd6b2e GIT binary patch literal 2155 zcmbVNX;c&E8cq=dMbUy3kg`mI3bce|LIRnTh*{WWksU-O%MfxQnUDk$xQdoV5Df<P zS}7a_iU)69z$!>Vpb`)(C?HTR%2H*uUZ9nND7JS{u=j_jKkl3}^L^j@J<q$pXAT7g zco`a47+^3MLmzKWFuGcyuZ2Dq{ra7ZxP~qp6&{g_5F}2a<jP<SQ-JVbz(>p#!oe_C zkec)Z?1sUtl88bh6_Nhi>3l@	hKTsl-y0jlsCNtE60h0;~Xduuvpn;HT@G@PJ6b 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 RH^8OezmG=%=MpP6<G<qJb$S2* literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_create_micro_service.png b/src/assets/images/ico_create_micro_service.png new file mode 100644 index 0000000000000000000000000000000000000000..7385f982364e66f7161fee248bfe5103b1bdb225 GIT binary patch literal 1003 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i1|)m0d<kV>VD!!mi71Ki^|4CM&(%vz$xlkv ztH><?$}=$7*jE%JCTFLXC?ut(XXe=|z2CiGNg*@ERw>-n*TA>HIW;5GqpB!1xXLdi zxhgx^GDXSWj?1RP3TQxXYDuC(MQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}<Os4*A;LF6 z!8yMuRl!uxOgGuk*h0bFQqR!T(!$6@N5ROz&`jUJQs2--*TB%qz|zXVPyq^*?6?$c zic-?7f?V97mH~OTN*N_31y=g{<>lpi<;HsXMd|v6mX?<K21fcuM!G;1y2X`wC5aWf zdBw^gBOqqDq!uR^WfqiV=I1GZ%uGzmFD<cEQql)mq7StN=skV7|9~8L2&Lu$gQW=Q ze+AEVRtyYG(Vi}jAs(G~r#X6v28y)pzvzC^OFLGjDd5<eIdgd&69ilyJ`?=X!lB|3 z?J-+pk;ami_SrmlSl)|<D0l`YxNx$rjXG1Nkx?aiMM>OEFJ_0wT(M#`{p&kFKU3QB zqW!q__c{OX)So+JS$?eZv4r;u&OYCy?zNZ70=#&ye=hC}lt}qC`{~M^u3EPld3MgT ze_i#@-8VyQ1;=ttFW*P%jhYLN3&>BpZ~j2(gH671aH{SBl@C6`zw#b@=O{Ny_fJlc z+M)D~t$_bU_aTS%8~AtV%c!K^a$CdH)47+4he<h5qUJ*9BWv|JqJ{m8%ey+>vgCE| zWt_wI%r|^hf}F+SwASDhlU}_r#$8A2IQHl$OnJom!Q)NdP3`bCYi@*^KB)RPZRV*N zhNjEcu>apyC&IR4-hKVk=Ygi4ys@L^#|wq*{nuCA+toKqI|C@gz{m9Wm*ax-k<<U^ zzX(@dp{x6s<-xJDs~QhAb(FqjRNrRN$<HhE{Q4&&2HEQGAO78ay=Y~_Q%4)ax~b>R z@s(}gZfsb0){8?&`?j8!#(S0PpW7SRH?46`G+1?nq2rK3SWVxLOQmyDPB6ADHt=v~ z^w0`6zwP>|;XC8&1Dp?fcPZXF9h~_;`GEEU(I1Rom{w=4`*$X+@pRI5CjUmpzhx;8 zI6k-(G3YII(%K}I!nQ-ZO!|Y^ik<WBuNFQUF6=jZ$9snU&FfBF=@oC-bwf3wB_+W= zb5GlmOI=wx_hueSdVN6S<ilMne#A1@2kv;TdfiLsC{F={1@{@o_^m(Y9Qnp=E?#9W o6}Uip_OpMN{VkvG|LgIWdH;m9F1BLtE`su?r>mdKI;Vst0Aco(XaE2J literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_create_micro_service_green.png b/src/assets/images/ico_create_micro_service_green.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd720776498a4daa90eaefd2e6bd62b3cfc1e80 GIT binary patch literal 983 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i1|)m0d<kV>VD!!mi71Ki^|4CM&(%vz$xlkv ztH><?$}=$7*jE%JCTFLXC?ut(XXe=|z2CiGNg*@ERw>-n*TA>HIW;5GqpB!1xXLdi zxhgx^GDXSWj?1RP3TQxXYDuC(MQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}<Os4*A;LF6 z!8yMuRl!uxOgGuk*h0bFQqR!T(!$6@N5ROz&`jUJQs2--*TB%qz|zXVPyq^*?6?$c zic-?7f?V97mH~OTN*N_31y=g{<>lpi<;HsXMd|v6mX?<K21fcuM!G;1y2X`wC5aWf zdBw^gBOqqDq!uR^WfqiV=I1GZ%uGzmFD<cEQql)mq7StN=skV7|9~8L2&Lu$gQW=Q ze+AEVRtyYG?w&4=As(G~r)|s@aTGaLuWxzf#u84>auz13jb1a&HVPeiCa|YL!_gpF zFvsPi{R9&?59KqJ3l1g(h$tyBuKgn|a>TevEKVWy0EeQv*zar7$L8*hWfH3^xHG%* z{_OmFZ>l$IpR@Ej#6Q8PkvVlLU$a+M$JzT!jBl!6&_CC&JL#rig5R1o-BbG;FIZg& z@QV-m^+TWG^Z}P6_RII@AMkyUdM|0?8MX%12d)ZVY!A&3co%eTR^V~20{6Fi9|UF; z9cz{JW7K`Rz)(B&h}3~aw{{<>OgJd{T~X!L9n+_8UVTw2*w}F+@WH%Wu@8hcNZwxA z?)$)KFVFOo8nXh{F!rqeD^U^apm9_?A@prwtakXCcQ?yx+arFiUT#!%rhjGoIs2vW zdz{z!y#BD+_?`E$Ltk%Amim3SHfpo(p9t6JHCq~k50q30w$z4Q{=c>$oa^?_n~Vqc zm<w`6YlU&kKJvPAWbx|7Z~ok3OvuUTd8>Wf)wyw!_{O;#L;AxdEzO$dZd}^3ASJEc zmG%8xm9S@x?p8Tgk9(doADDUX*^#anF;;n#Oe_!VG1kj%Ju+WySvT)Qn+IA240D+8 z{o)qdW3%^@J@+0)wnq5_DjS|3Z#$pEzhmt;g%2__c>9%^(;L#Im<zN;KTQl($UeZl zwY{;tK~#IPV&L;NMQx?O>^_)eyKl_e!)S7K9Rs%t?{CcqTdz(}OL;lhCvhY5feA6| zWb^-(wA>GTSK6zqIxUIUf-{EItYLj)y>qSll-b=Iq!w!g^wuW-f4O|lpY8H1_cOMK Wt?i3f->?&uA3a_DT-G@yGywpetBoW8 literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_create_micro_service_red.png b/src/assets/images/ico_create_micro_service_red.png new file mode 100644 index 0000000000000000000000000000000000000000..d044b5d2e91fe65f3547854ad68948c5609fb41d GIT binary patch literal 1460 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i1|)m0d<g|ok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+nDa72B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxOgGuk*h0bFQqR!T z(!$6@N5ROz&`jUJQs2--*TB%qz|zXVPyq^*fVLH-q*(>IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD<cE0=g99h1>#PU%0_}#n6BP2AO_EVu8M)o`HUDF34YC)x{-2sR(CaRb3oXS&*t9 zlv<o$T9gcok2GbNW?<Oiw+N&V=o%~kqD)|<B<8r}Czs}?=9PH5*eU^K^incYtlSLT z91V?~ERCEjj9twQ4K19ET-;nO4b3eLolKl94Pa(q)oW?tY;I<5>1J-`YG~+c?r3RX zW@+N;>}+i2=3-{#4AbkGS6q^qmz)Z-HxpzpM6U&2y;jaesfi`|MIrh5Ij|HEkda@K zU!0L&py2Ebig9xVjquF8l>G8yO;8j;eC(21lv$RV;#QQOs{jsTt4u6zNHQ@=G)*;5 z(lt#qP1H3uF|p81vNTB11qvBiSeRHErzM#xLG`DQ6L$JQ2kC<n2~whg2?0|ghzZXE zKn^?;rRD*1P7yGRsO<ii#lXOL$J50zq~g||u!COAh627>E34w8zQyJLW#6kI>f*9N zN$Xp2)u#}a+l#KtiQMY0>s-(jAX=TF|7hz}i_Em$lW&wvc9pJ3-Cdb~FJ-dNp;m!2 zn*$lY3#Q#*iZ(lEsOWiH`~hFH^V-uViW;0bZn4=&xaIy>t8n~ih{l5yi5u)+T2A>k zZu?QozjO2B1?^{bm{(+~ajh5I%wc+^YUWkXhI|L%04F_DZN{h*oj*$DH**QrF`f!h z=;802WmKemx3T-C(Sx}?8OKE;&Zv}~<Vfv_nRHY0<h!h}vf?u;>pK6boP0BFLie<( zuFucBiC6w5y?drnjq{_)-|YO-{96_ZI4Zbo*G}t+`d6=}9sb(5Gg;+c<QcZhXSi8s z-?S}qOY2z^7V&t}mRymTtJBXLo5{QqJs5XwQyR~Hr#l%2Vr@OFjaA=rVwPAWU)shq z&y#CTxO!;J!ptw4FIp|C#0#Tb>X*G#?P1-(_2Mzh>7$!oZaly31k>*W5uHz8&3H4_ z|G=*1n`}T|O!QaWr*KGcf#J<3`?(7Re;;e9e;56Ax<N-}i;bF$;q(<>jTX<}^1h(r z+r-(gJ}j9a9Lrk5px+kB{yXg1i{)}PyUyOxxA_(Nu3z8gf5BAmXb+nbZrPjt49pC# WGcwuZ=Vt8zm7|`nelF{r5}E+2a1b*9 literal 0 HcmV?d00001 diff --git a/src/assets/images/ico_not_yet_onboarded.png b/src/assets/images/ico_not_yet_onboarded.png new file mode 100644 index 0000000000000000000000000000000000000000..672cccf271b839c48e6c860f2641a8d2f1d1ac9a GIT binary patch literal 1526 zcmX|BeK?bA7=O*=Yno0{3B6WU)b?(^vb7;rTG{68;2<9{t1TOAo4kmODeBCnP^ie& zS*1cLiimSMeV$6CMLIII;>a>qDCg)*r>p0>?&o>#=f1Ds@BaP%cnX4<fmRlF761TP z(KpiAh7+0%vpI%cvZDAr02poNhlESR89`)DvJfAakQ~p&!$Ogv4geHiSQN)e<VwMK z?sk3>6*GFR9Ru<csF<HU83cwXfXm}=Oc!&1N@s>}(i1tP1dOkb1qCJ>0EAp=90&^q zNfI(l#eBsj8}75u5C;6(B2A=X*3A-v;f!D~AX&@>J@H;R4$<8kTtmVWJxShf9;-n& z0?`X1kRYN5jzA<6NMr&L{I)PY79d5Ou#L>7ZTLoJ$fy{eR4O7vP-<!_KGhwcEZz<g zNhA_PaD&|3aE1<?BrQoA2jh|?%Vr_|$DnZ~95G)c<tHbBvzT%5$thAQ1_RBKK;Kvl z@}Tef49RzgxJd?E#0LE!y^ah3$Zd4mx)7dBRmR{g3k7cPNgL(1ToqRIYF(u*j3)$3 zTX_r(oxX)tw?988uznZ%V{V&SooV!f4-N2#SD5^F>s%@+X+uixnDHu)_~N19@GU$q z;ibR-Y^~YHf7awxPPgmQpwWl!TI#1COqWf@2>AShbau^CYn{Dr!3lG^Pt^+B1J#Wi zHWd)DBanl_TbFi6-<q0zv;)^M>`GrUuO`IeMbqQR*p%o=t4=*osYD}(Sy|5-&Yz1+ zjOtd#B9Xc??BV^w7xx-gf2R8Z?MACMxd>jIszM^wMi4^(FE)5oU*Tip?HxLCJllJ` ztsYtUdjL(gN!eg5K<GO{CX2o>1euv_kCSb6o}%Y^N>umyL<KjZr%A!KaB*&oRK#Bl zpUC{OfO9{!FTi)^Zuk6N#H~do>>w6vh1@z8SlE*5@atmAh~>!p_b>U&)zdzz9i;^Y z1#)ggH||_UfD_8mufDeS=Ib%<2gp}rFzV`MUu?^vo>#BpOVX9)<>doft&q}r^6?_V zt)kAs)Xe_mylN!HL=%~}Fe4*_=JN6B^rxLTP`Ui*gCugH<l)X^?tUd=WB5|22yl{v z{-2to@XM6T%Y!g--b#Ri`e!bRT$pQJP1VmO^w_pOt1x?0>tB7rlR(HWIYh(Ei#XwM z=8kUOJDeToWo%5$%eoL(;2`oBVygM)55J8sB>ZI#tNi=WXH~=6($Z2E=VA{`aXfOX zY<OyFDneP(Ae^27cST?E$zP8xKA7h}uy-)+BXtaWL1jL5hIPGi)F?19le>NUU|3^h z8>qB5vb%07x6Z8z*%9Uv>I9FMGPAE3vueXQOV#+`IZZM_e#aw?Mw8<7ho{=EXvlSU zZP;25t|R?Sb_qM-(({bM*HQM17rT%OyQote?!FOg#H4JkN~MAn#H>3Y-sts;Hb@Fw zuZ+U)8%*oBIdBu5+27B9f)EP?BVL5EAS_GTxod#a0TfZ#S>u$^lM036Ku13#QthNh z=h(t0q6@=L-OD8zx1yrR?{BkfXw6YC8K=88xHvBfIyo2^7<k}~xLGqj1NF(<O?S_| z?{!u^J=46~XOCpZj$xwmX|5GA0b5*9Usso8nD?+#uhwg?XiPpj&9bq`BUP|Y76f7S zDB)`7cuPx5ZAX=9mdRmRANC_w=9_2jcAV+!qP%Q}J8f>ui^$<m?#VNSk#ObP+DSXz z;^5Ft5eDO!X2c$vI#9h6V_#ooGqcslD2Dp>b41RSF~@A{94VQ)Txr8VzBQkCSdBPd zF&I18-nH$A{Q2j>(b!`4+^z%^aKxO6^IvnrA%osn^hk-EXTmggM95IO8+0Vo(lO_v z&f@{=ZH2`#EQPi<Qm|6gs}mt)G1>dPOwvspd##6@Vcw4?y46lC9h=+C&;y92#{Cnn z6njX`ukDvaPeyM&cVuATg5Sd?ElDk$K-c#6(No;*VA)plEZ(&#v}XRY;kC}04YS`h MeLa&_=f5@QKc(`F_5c6? literal 0 HcmV?d00001 diff --git a/src/assets/images/loading_deactive.png b/src/assets/images/loading_deactive.png new file mode 100644 index 0000000000000000000000000000000000000000..5d737e07b46bccaa738e2511d13cabe3b70d3625 GIT binary patch literal 620 zcmeAS@N?(olHy`uVBq!ia0vp^HbBh7!3HGl+V0(FU|{sl42dX-@b$4u&d=3LOvz75 z)vL%Y0LlZ^+E)}LCTFLXC?ut(XXe=|z2CiGNg*@ERw>-n*TA>HIW;5GqpB!1xXLdi zxhgx^GDXSWj?1RP3TQxXYDuC(MQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}<Os4*A;LF6 z!8yMuRl!uxOgGuk*h0bFQqR!T(!$6@N5ROz&`jUJQs2--*TB%qz|zXVPyq^*?6?$c zic-?7f?V97mH~OTN*N_31y=g{<>lpi<;HsXMd|v6mX?<K21fcuM!G;1y2X`wC5aWf zdBw^gBOqqDq!uR^WfqiV=I1GZ%uGzmFD<cEQql)mq7StN=skV7|9~8L2&Lu$gQW=Q ze+AEVR=}{k<LTlU;?a5a+J0YVLy5MJ^J7`c5}G_b7B<N?usU4%Gn0eOA&GI?0!J3t zq^6SlOzgZz@0uPIpQBfI>iYZS)0bY_E$=WeIBdJFz$Qg|&jZf;+b*}vZ{wDKD5@?z zyY_+5>&~xfe?$b0_vC8!>lC)-Dd(R$zhkw=VW#b^do=AI?^<LN^vdk%@{Si@=DYIB z@^MER*R9j(`}5|;eQW#2wd{$ZCuV)xT<3QcqRQgJ|7+s^r`Q+1dLNN->il6g@m~*Z zjtV_*J-p@F^4{f7nez<fH|g6v4!nCg@BQES_BBnl>)RIifD(zPtDnm{r-UW|d%5EM literal 0 HcmV?d00001 diff --git a/src/assets/images/stepper_progress_completed.png b/src/assets/images/stepper_progress_completed.png new file mode 100644 index 0000000000000000000000000000000000000000..e2ef453bedbf98b8b6c3ce86716d0292fd383c7e GIT binary patch literal 1302 zcmeAS@N?(olHy`uVBq!ia0vp^-ayR9!3HGLZ$C5zQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?@QuLn2Bde0{8v^K<nQL2C3WatnaE85nHrD+&^mvr|hH zl2X$%^K6yg@7}MZkeOnu6mIHk;9KCFnvv;IRg@ZB<rk7%m7Q#vqGWHUU{hfQG$1#% zBvGLvHz%*ys=`(YY_1iM4HDK@QUEI{$+lI3I4Z(7K*2e`C{@8!&rCPj(AYx3+)~fb z)Y8JpL`T8Mz|c(Jz*67PMAyL3%D~dfz)%4Slz_GsrKDK}xwt{?0`hE?GD=Dctn~HE z%ggo3jrH=2()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij`p|xTF>*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09sGbq%|6~Z&~ zQu51-!8&|>tvvIJOA_;vQ$1a5m4IgGWoD*WnLC=BI~%*0JGnU-8ydQrJ6ak$x>>k5 zSy)<{n_D_N!t}c2Czs}?=9R$orXciM;M5CB47mkBn_W_iGRsm^+=}vZ6~Lah%Eav! zQ=H~O^`_u<iy2P6`as9%gQ6BGs$oLF^aEnT6E2VgPx`5O!1P`OOxO}DFE=nSFm`yl zIEGZ*N=lh=(qPfS|6BeSm_62Ub5~GhJF>d*(f<oqjSPF3A{-<cSJpc0Vd_}U@U!O! z(`oTlKa541Z`h^Wu)c9dQLI3DqjBR7ZrcM1jaQkD%x;jF!c)0G;>bOosmfwJX2l+i z?hQNmJL8#-eAQUVACdOcLAH_qhv34r4KtnnX0m#DZf>w(_4Jes@HN-j@XaxqSN;b7 z5kHnIK&=-V#RX;yF6@)+_}nmq-&03gBH^+_<5$iT4^<Nmq???*aUj8%U2+BEM_(@C zg;pE4Dk^W)n#*YN+@AGY<1E4Y$c7`5N|XL(D)GqVJX2_nc6h^iWSYwgR@DVD3StgI z-ppx-cP}{bO8rpbUzZ4@8)qEZtryM+V6+$3UGS+f>)9bE!I0Hk9&KDQ^~9`npkuQZ zPZ5;jT<LM>>Vbw?E%(l?Voa*f(ETjh_1$^(3n`&DKuabt<v6b7=)BLzv%)NeRmbCv zZ-9@cM9wn-ZaYV9#sIF_JYJ!TYn|`(UgmZ<#n;ii;Hik)TTX!lhOeBxKc23=_!(4E NdAj<!taD0e0sw1(zd`^2 literal 0 HcmV?d00001 -- GitLab