diff --git a/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.html b/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.html new file mode 100644 index 00000000..1259c619 --- /dev/null +++ b/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.html @@ -0,0 +1,18 @@ +

Instance not allocated

+
+ There is no analysis instance allocated for the project. Having an instance is required for running analysis +
+

Do you want to allocate it now ?

+ + @if (allocateInstanceHandler.isLoading()) { + + } + + @if (allocateInstanceHandler.error()) { + + } +
+
+ + +
diff --git a/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.scss b/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.ts b/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.ts new file mode 100644 index 00000000..39d68c34 --- /dev/null +++ b/ScriptBeeClient/src/app/components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component.ts @@ -0,0 +1,56 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSelectModule } from '@angular/material/select'; +import { FormsModule } from '@angular/forms'; +import { InstanceService } from '../../../services/instances/instance.service'; +import { ErrorStateComponent } from '../../error-state/error-state.component'; +import { LoadingProgressBarComponent } from '../../loading-progress-bar/loading-progress-bar.component'; +import { apiHandler } from '../../../utils/apiHandler'; + +export interface InstanceNotAllocatedDialogData { + projectId: string; +} + +@Component({ + selector: 'app-instance-not-allocated-dialog', + templateUrl: './instance-not-allocated-dialog.component.html', + styleUrls: ['./instance-not-allocated-dialog.component.scss'], + imports: [ + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatExpansionModule, + MatSelectModule, + MatButtonModule, + FormsModule, + ErrorStateComponent, + LoadingProgressBarComponent, + ], +}) +export class InstanceNotAllocatedDialog { + allocateInstanceHandler = apiHandler( + (params: { projectId: string }) => this.instanceService.allocateInstance(params.projectId), + () => { + this.dialogRef.close(); + } + ); + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: InstanceNotAllocatedDialogData, + public dialogRef: MatDialogRef, + private instanceService: InstanceService + ) {} + + onCloseClick(): void { + this.dialogRef.close(); + } + + onAllocateClick(): void { + this.allocateInstanceHandler.execute({ + projectId: this.data.projectId, + }); + } +} diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.html b/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.html index 3edabbba..8c913c92 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.html +++ b/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.html @@ -13,7 +13,11 @@ - + @if (analysisId()) { + + } @else { +
There are no results since no analysis was run yet
+ }
diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.ts index 91e79118..a4b6c795 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/analysis/analysis.component.ts @@ -6,6 +6,7 @@ import { AnalysisOutputComponent } from './output/analysis-output.component'; import { ActivatedRoute, Router } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TreeNode } from '../../../../types/tree-node'; +import { InstanceService } from '../../../../services/instances/instance.service'; @Component({ selector: 'app-analysis', @@ -16,6 +17,7 @@ import { TreeNode } from '../../../../types/tree-node'; export class AnalysisComponent { projectId = signal(undefined); analysisId = signal(undefined); + instanceId = signal(undefined); selectedFileId = signal(null); @@ -24,7 +26,8 @@ export class AnalysisComponent { constructor( private route: ActivatedRoute, - private router: Router + private router: Router, + private instanceService: InstanceService ) { route.queryParamMap.subscribe((params) => { this.selectedFileId.set(params.get('fileId')); @@ -32,7 +35,16 @@ export class AnalysisComponent { route.parent?.paramMap.pipe(takeUntilDestroyed()).subscribe({ next: (paramMap) => { - this.projectId.set(paramMap.get('id') ?? undefined); + const id = paramMap.get('id'); + this.projectId.set(id ?? undefined); + + if (id) { + this.instanceService.getCurrentInstance(id).subscribe({ + next: (instanceInfo) => { + this.instanceId.set(instanceInfo.id); + }, + }); + } }, }); } diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.html b/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.html index 642a76d7..b36d35c3 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.html +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.html @@ -2,7 +2,8 @@
Used Linkers: - @for (linker of instanceInfo().linkers; track linker) { + + @for (linker of []; track linker) { {{ linker }} }
diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.ts index a13dde3e..81ab65f6 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/currently-loaded-models/currently-loaded-models.component.ts @@ -13,7 +13,8 @@ export class CurrentlyLoadedModelsComponent { instanceInfo = input.required(); loadedFiles = computed(() => { - return convertToTreeNodes(this.instanceInfo().loadedModels); + // TODO FIXIT(#70): populate from api + return convertToTreeNodes({}); }); } diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.html b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.html new file mode 100644 index 00000000..c083cc97 --- /dev/null +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.html @@ -0,0 +1,6 @@ +
Instance Information
+ +
+
Instance Id: {{ instanceInfo().id }}
+
Creation Date: {{ instanceInfo().creationDate | date }}
+
diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.scss b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.scss new file mode 100644 index 00000000..6fdd66d7 --- /dev/null +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.scss @@ -0,0 +1,3 @@ +.instance-info-div { + margin-left: 8px; +} diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.ts new file mode 100644 index 00000000..a9749ba6 --- /dev/null +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/instance-info/instance-info.component.ts @@ -0,0 +1,13 @@ +import { Component, input } from '@angular/core'; +import { InstanceInfo } from '../../../../../types/instance'; +import { DatePipe } from '@angular/common'; + +@Component({ + selector: 'app-instance-info', + templateUrl: './instance-info.component.html', + styleUrls: ['./instance-info.component.scss'], + imports: [DatePipe], +}) +export class InstanceInfoComponent { + instanceInfo = input.required(); +} diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.html b/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.html index 6879a61b..6a93491f 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.html +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.html @@ -8,7 +8,7 @@ Linker - @for (loader of getLinkersResource.value()!; track loader.id) { + @for (loader of [{ id: 'id', name: 'linker' }]!; track loader.id) { {{ loader.name }} @@ -27,6 +27,6 @@ @if (!selectedLinkerId()) { -
A loader must be selected in order to upload models
+
A linker must be selected
}
diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.ts index 5ad31ac1..f13dcac1 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/link-models/link-models.component.ts @@ -17,18 +17,20 @@ import { apiHandler } from '../../../../../utils/apiHandler'; }) export class LinkModelsComponent { projectId = input.required(); + instanceId = input.required(); selectedLinkerId = signal(undefined); getLinkersResource = createRxResourceHandler({ - loader: () => this.linkerService.getAllLinkers(), + request: () => ({ + projectId: this.projectId(), + instanceId: this.instanceId(), + }), + loader: (params) => this.linkerService.getAllLinkers(params.request.projectId, params.request.instanceId), }); - linkModelsHandler = apiHandler( - (params: { projectId: string; linkerId: string }) => this.linkerService.linkModels(params.projectId, params.linkerId), - (data) => { - console.log(data); - } + linkModelsHandler = apiHandler((params: { projectId: string; instanceId: string; linkerId: string }) => + this.linkerService.linkModels(params.projectId, params.instanceId, params.linkerId) ); constructor(private linkerService: LinkerService) {} @@ -39,6 +41,6 @@ export class LinkModelsComponent { return; } - this.linkModelsHandler.execute({ projectId: this.projectId(), linkerId: linkerId }); + this.linkModelsHandler.execute({ projectId: this.projectId(), instanceId: this.instanceId(), linkerId: linkerId }); } } diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/load-models/load-models.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/load-models/load-models.component.ts index 633a652f..4d9c9987 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/load-models/load-models.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/load-models/load-models.component.ts @@ -15,15 +15,14 @@ import { LoaderService } from '../../../../../services/loaders/loader.service'; }) export class LoadModelsComponent { projectId = input.required(); + instanceId = input.required(); savedFiles = signal([]); checkedFiles = signal([]); - loadModelsHandler = apiHandler( - (params: { projectId: string; checkedFiles: TreeNode[] }) => this.loaderService.loadModels(params.projectId, params.checkedFiles), - (data) => { - console.log(data); - } + loadModelsHandler = apiHandler((params: { projectId: string; instanceId: string; checkedFiles: TreeNode[] }) => + // TODO FIXIT(#14): update with the list of loaders + this.loaderService.loadModels(params.projectId, params.instanceId, params.checkedFiles) ); constructor(private loaderService: LoaderService) {} @@ -35,6 +34,7 @@ export class LoadModelsComponent { onLoadFilesClick() { this.loadModelsHandler.execute({ projectId: this.projectId(), + instanceId: this.instanceId(), checkedFiles: this.checkedFiles(), }); } diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.html b/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.html index 441911c3..28937b69 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.html +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.html @@ -6,36 +6,44 @@
} @else {
- - + @if (currentInstanceInfoResource.isLoading()) { + + } @else if (currentInstanceInfoResource.error()) { + + } @else { + + - + - + - + - + - + - @if (currentInstanceInfoResource.isLoading()) { - - } @else if (currentInstanceInfoResource.error()) { - - } @else { - - } + - + @if (currentInstanceInfoResource.isLoading()) { + + } @else if (currentInstanceInfoResource.error()) { + + } @else { + + } - @if (currentInstanceInfoResource.isLoading()) { - - } @else if (currentInstanceInfoResource.error()) { - - } @else { - - } - + + + @if (currentInstanceInfoResource.isLoading()) { + + } @else if (currentInstanceInfoResource.error()) { + + } @else { + + } + + }
} diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.ts index aa377097..797215a9 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/project-model-page.component.ts @@ -14,6 +14,7 @@ import { ProjectContextComponent } from './project-context/project-context.compo import { LoadingProgressBarComponent } from '../../../../components/loading-progress-bar/loading-progress-bar.component'; import { InstanceService } from '../../../../services/instances/instance.service'; import { CenteredSpinnerComponent } from '../../../../components/centered-spinner/centered-spinner.component'; +import { InstanceInfoComponent } from './instance-info/instance-info.component'; @Component({ selector: 'app-project-model-page', @@ -30,6 +31,7 @@ import { CenteredSpinnerComponent } from '../../../../components/centered-spinne ProjectContextComponent, LoadingProgressBarComponent, CenteredSpinnerComponent, + InstanceInfoComponent, ], }) export class ProjectModelPage { diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/model/upload-models/upload-models.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/model/upload-models/upload-models.component.ts index d5d2f955..99585f4a 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/model/upload-models/upload-models.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/model/upload-models/upload-models.component.ts @@ -29,15 +29,20 @@ import { UploadService } from '../../../../../services/upload/upload.service'; }) export class UploadModelsComponent { projectId = input.required(); + instanceId = input.required(); selectedLoaderId = signal(undefined); getLoadersResource = createRxResourceHandler({ - loader: () => this.loaderService.getAllLoaders(), + request: () => ({ + projectId: this.projectId(), + instanceId: this.instanceId(), + }), + loader: (params) => this.loaderService.getAllLoaders(params.request.projectId, params.request.instanceId), }); uploadModelsHandler = apiHandler( - (params: { loaderId: string; projectId: string; files: File[] }) => this.uploadService.uploadModels(params.loaderId, params.projectId, params.files), + (params: { loaderId: string; projectId: string; files: File[] }) => this.uploadService.uploadModels(params.projectId, params.loaderId, params.files), (data) => { console.log(data); } diff --git a/ScriptBeeClient/src/app/pages/projects/project-details/project-details-page.component.ts b/ScriptBeeClient/src/app/pages/projects/project-details/project-details-page.component.ts index 5ad3b008..54df6d05 100644 --- a/ScriptBeeClient/src/app/pages/projects/project-details/project-details-page.component.ts +++ b/ScriptBeeClient/src/app/pages/projects/project-details/project-details-page.component.ts @@ -4,6 +4,11 @@ import { MatTabsModule } from '@angular/material/tabs'; import { filter, first } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatIcon } from '@angular/material/icon'; +import { InstanceService } from '../../../services/instances/instance.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { isNoInstanceAllocatedForProjectError } from '../../../utils/api'; +import { MatDialog } from '@angular/material/dialog'; +import { InstanceNotAllocatedDialog } from '../../../components/dialogs/instance-not-allocated-dialog/instance-not-allocated-dialog.component'; interface TabInfo { link: string; @@ -40,7 +45,9 @@ export class ProjectDetailsPage { constructor( private route: ActivatedRoute, - private router: Router + private router: Router, + private instanceService: InstanceService, + private dialog: MatDialog ) { router.events .pipe( @@ -53,6 +60,24 @@ export class ProjectDetailsPage { const urlPart = urlParts[urlParts.length - 1]; this.activeTab.set(this.tabInfo.find((t) => t.link === urlPart) ?? this.tabInfo[0]); }); + + route.paramMap.pipe(takeUntilDestroyed()).subscribe({ + next: (paramMap) => { + const projectId = paramMap.get('id'); + if (projectId) { + this.instanceService.getCurrentInstance(projectId).subscribe({ + error: (errorResponse: HttpErrorResponse) => { + if (isNoInstanceAllocatedForProjectError(errorResponse)) { + this.dialog.open(InstanceNotAllocatedDialog, { + disableClose: true, + data: { projectId }, + }); + } + }, + }); + } + }, + }); } selectTab(tab: TabInfo) { diff --git a/ScriptBeeClient/src/app/services/instances/instance.service.ts b/ScriptBeeClient/src/app/services/instances/instance.service.ts index 6bdc347e..6b50978a 100644 --- a/ScriptBeeClient/src/app/services/instances/instance.service.ts +++ b/ScriptBeeClient/src/app/services/instances/instance.service.ts @@ -12,4 +12,8 @@ export class InstanceService { getCurrentInstance(projectId: string): Observable { return this.http.get(`/api/projects/${projectId}/instances/current`); } + + allocateInstance(projectId: string): Observable { + return this.http.post(`/api/projects/${projectId}/instances`, null); + } } diff --git a/ScriptBeeClient/src/app/services/linkers/linker.service.ts b/ScriptBeeClient/src/app/services/linkers/linker.service.ts index 4644fb18..574953de 100644 --- a/ScriptBeeClient/src/app/services/linkers/linker.service.ts +++ b/ScriptBeeClient/src/app/services/linkers/linker.service.ts @@ -6,18 +6,15 @@ import { Linker } from '../../types/link-model'; providedIn: 'root', }) export class LinkerService { - private linkersAPIUrl = '/api/linkers'; - constructor(private http: HttpClient) {} - linkModels(projectId: string, linkerId: string) { - return this.http.post(this.linkersAPIUrl, { - projectId: projectId, - linkerId: linkerId, - }); + getAllLinkers(projectId: string, instanceId: string) { + return this.http.get(`/api/projects/${projectId}/instances/${instanceId}/loaders`); } - getAllLinkers() { - return this.http.get(this.linkersAPIUrl); + linkModels(projectId: string, instanceId: string, linkerId: string) { + return this.http.post(`/api/projects/${projectId}/instances/${instanceId}/context/link`, { + linkerIds: [linkerId], + }); } } diff --git a/ScriptBeeClient/src/app/services/loaders/loader.service.ts b/ScriptBeeClient/src/app/services/loaders/loader.service.ts index 0cb5628d..3e16bbed 100644 --- a/ScriptBeeClient/src/app/services/loaders/loader.service.ts +++ b/ScriptBeeClient/src/app/services/loaders/loader.service.ts @@ -1,41 +1,24 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { TreeNode } from '../../types/tree-node'; -import { Loader, LoadModel } from '../../types/load-model'; +import { TreeNodeWithParent } from '../../types/tree-node'; +import { Loader } from '../../types/load-model'; import { ReturnedContextSlice } from '../../types/returned-context-slice'; @Injectable({ providedIn: 'root', }) export class LoaderService { - private loadersAPIUrl = '/api/loaders'; - private loadersClearContextAPIUrl = '/api/loaders/clear'; - constructor(private http: HttpClient) {} - getAllLoaders() { - return this.http.get(this.loadersAPIUrl); + getAllLoaders(projectId: string, instanceId: string) { + return this.http.get(`/api/projects/${projectId}/instances/${instanceId}/loaders`); } - loadModels(projectId: string, checkedFiles: TreeNode[]) { - const loadModels: LoadModel = { - projectId: projectId, - nodes: checkedFiles - .filter((treeNode) => treeNode.children && treeNode.children.length > 0) - .map((treeNode) => ({ - loaderName: treeNode.name, - models: (treeNode.children ?? []).map((child: any) => child.name), - })), - }; - - return this.http.post(this.loadersAPIUrl, loadModels); - } - - reloadProjectContext(projectId: string) { - return this.http.post(`${this.loadersAPIUrl}/${projectId}`, null); - } + loadModels(projectId: string, instanceId: string, checkedFiles: TreeNodeWithParent[]) { + const loaderIds = checkedFiles.map((node) => node.parent?.name); - clearProjectContext(projectId: string) { - return this.http.post(`${this.loadersClearContextAPIUrl}/${projectId}`, null); + return this.http.post(`/api/projects/${projectId}/instances/${instanceId}/context/load`, { + loaderIds, + }); } } diff --git a/ScriptBeeClient/src/app/services/upload/upload.service.ts b/ScriptBeeClient/src/app/services/upload/upload.service.ts index a869bddb..e8c285bb 100644 --- a/ScriptBeeClient/src/app/services/upload/upload.service.ts +++ b/ScriptBeeClient/src/app/services/upload/upload.service.ts @@ -6,16 +6,12 @@ import { UploadModelsResult } from '../../types/upload-models-result'; providedIn: 'root', }) export class UploadService { - private uploadFilesUrl = '/api/uploadmodel/fromfile'; - constructor(private http: HttpClient) {} - uploadModels(loaderId: string, projectId: string, files: File[]) { + uploadModels(projectId: string, loaderId: string, files: File[]) { const formData = new FormData(); - formData.append('loaderId', loaderId); files.forEach((file) => formData.append('files', file)); - formData.append('projectId', projectId); - return this.http.post(this.uploadFilesUrl, formData); + return this.http.put(`/api/projects/${projectId}/loaders/${loaderId}/files`, formData); } } diff --git a/ScriptBeeClient/src/app/types/instance.ts b/ScriptBeeClient/src/app/types/instance.ts index 7c881bfa..382cb47e 100644 --- a/ScriptBeeClient/src/app/types/instance.ts +++ b/ScriptBeeClient/src/app/types/instance.ts @@ -1,7 +1,4 @@ export interface InstanceInfo { id: string; - loaders: string[]; - linkers: string[]; - loadedModels: Record; creationDate: string; } diff --git a/ScriptBeeClient/src/app/types/upload-models-result.ts b/ScriptBeeClient/src/app/types/upload-models-result.ts index ec483367..b27b5be6 100644 --- a/ScriptBeeClient/src/app/types/upload-models-result.ts +++ b/ScriptBeeClient/src/app/types/upload-models-result.ts @@ -1,4 +1,4 @@ export interface UploadModelsResult { - loaderName: string; - files: string[]; + loaderId: string; + fileNames: string[]; } diff --git a/ScriptBeeClient/src/app/utils/api.ts b/ScriptBeeClient/src/app/utils/api.ts index a9c67681..07fff000 100644 --- a/ScriptBeeClient/src/app/utils/api.ts +++ b/ScriptBeeClient/src/app/utils/api.ts @@ -1,7 +1,14 @@ import { ErrorResponse } from '../types/api'; +import { HttpErrorResponse } from '@angular/common/http'; export const DEFAULT_ERROR_RESPONSE: ErrorResponse = { title: 'Unexpected Error Occurred', detail: 'Please try again or contact support.', status: 500, }; + +export function isNoInstanceAllocatedForProjectError(errorResponse: HttpErrorResponse) { + const error: ErrorResponse = errorResponse.error; + + return error.title === 'No Instance Allocated For Project'; +} diff --git a/calculation.Dockerfile b/analysis.Dockerfile similarity index 72% rename from calculation.Dockerfile rename to analysis.Dockerfile index a2e05771..6b71cd6c 100644 --- a/calculation.Dockerfile +++ b/analysis.Dockerfile @@ -11,10 +11,14 @@ COPY src/Common src/Common COPY src/Application/Domain/Model src/Application/Domain/Model COPY src/Application/Domain/Service.Analysis src/Application/Domain/Service.Analysis +COPY src/Application/Domain/Service.Plugin src/Application/Domain/Service.Plugin COPY src/Application/Ports/Driving/UseCases.Analysis src/Application/Ports/Driving/UseCases.Analysis +COPY src/Application/Ports/Driving/UseCases.Plugin src/Application/Ports/Driving/UseCases.Plugin + COPY src/Application/Ports/Driven/Ports.Analysis src/Application/Ports/Driven/Ports.Analysis COPY src/Application/Ports/Driven/Ports.Files src/Application/Ports/Driven/Ports.Files +COPY src/Application/Ports/Driven/Ports.Instance src/Application/Ports/Driven/Ports.Instance COPY src/Application/Ports/Driven/Ports.Plugins src/Application/Ports/Driven/Ports.Plugins COPY src/Application/Ports/Driven/Ports.Project src/Application/Ports/Driven/Ports.Project @@ -23,11 +27,11 @@ COPY src/Adapters/Driven/Persistence.InMemory src/Adapters/Driven/Persistence.In COPY src/Adapters/Driven/Persistence.Mongodb src/Adapters/Driven/Persistence.Mongodb COPY src/Adapters/Driving/Common.Web src/Adapters/Driving/Common.Web -COPY src/Adapters/Driving/Calculation.Web src/Adapters/Driving/Calculation.Web +COPY src/Adapters/Driving/Analysis.Web src/Adapters/Driving/Analysis.Web -RUN dotnet restore src/Adapters/Driving/Calculation.Web +RUN dotnet restore src/Adapters/Driving/Analysis.Web -RUN dotnet publish src/Adapters/Driving/Calculation.Web -c Release -o publish --no-restore +RUN dotnet publish src/Adapters/Driving/Analysis.Web -c Release -o publish --no-restore # Build the final image FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final @@ -39,4 +43,4 @@ WORKDIR /app COPY --from=build_webapp /app/publish . -ENTRYPOINT ["dotnet", "Calculation.Web.dll"] +ENTRYPOINT ["dotnet", "Analysis.Web.dll"] diff --git a/src/Adapters/Driven/Analysis.Instance.Docker/CalculationInstanceDockerAdapter.cs b/src/Adapters/Driven/Analysis.Instance.Docker/CalculationInstanceDockerAdapter.cs index 2431e66f..3616ed55 100644 --- a/src/Adapters/Driven/Analysis.Instance.Docker/CalculationInstanceDockerAdapter.cs +++ b/src/Adapters/Driven/Analysis.Instance.Docker/CalculationInstanceDockerAdapter.cs @@ -12,7 +12,8 @@ namespace ScriptBee.Analysis.Instance.Docker; public class CalculationInstanceDockerAdapter( IOptions config, - ILogger logger + ILogger logger, + IFreePortProvider freePortProvider ) : IAllocateInstance, IDeallocateInstance { public async Task Allocate( @@ -26,12 +27,30 @@ public async Task Allocate( await PullImageIfNeeded(client, image.ImageName, cancellationToken); + var hostPort = freePortProvider.GetFreeTcpPort(); + + var portBindings = new Dictionary> + { + { + $"{calculationDockerConfig.Port}/tcp", + new List { new() { HostPort = hostPort.ToString() } } + }, + }; + var response = await client.Containers.CreateContainerAsync( new CreateContainerParameters { Name = $"scriptbee-calculation-{instanceId}", Image = image.ImageName, - HostConfig = new HostConfig { NetworkMode = calculationDockerConfig.Network }, + HostConfig = new HostConfig + { + NetworkMode = calculationDockerConfig.Network, + PortBindings = portBindings, + }, + ExposedPorts = new Dictionary + { + { $"{calculationDockerConfig.Port}/tcp", new EmptyStruct() }, + }, }, cancellationToken ); @@ -49,7 +68,7 @@ await client.Containers.StartContainerAsync( client, response.ID, calculationDockerConfig.Network, - calculationDockerConfig.Port, + hostPort, cancellationToken ); } @@ -127,7 +146,7 @@ await client.Containers.RemoveContainerAsync( private static async Task GetContainerUrl( DockerClient client, string containerId, - string networkName, + string? networkName, int port, CancellationToken cancellationToken ) @@ -137,6 +156,11 @@ CancellationToken cancellationToken cancellationToken ); + if (networkName == null) + { + return $"http://localhost:{port}"; + } + return containerInfo.NetworkSettings.Networks.TryGetValue(networkName, out var network) ? $"http://{network.IPAddress}:{port}" : $"http://localhost:{port}"; diff --git a/src/Adapters/Driven/Analysis.Instance.Docker/Config/CalculationDockerConfig.cs b/src/Adapters/Driven/Analysis.Instance.Docker/Config/CalculationDockerConfig.cs index e8fff2a0..850d9234 100644 --- a/src/Adapters/Driven/Analysis.Instance.Docker/Config/CalculationDockerConfig.cs +++ b/src/Adapters/Driven/Analysis.Instance.Docker/Config/CalculationDockerConfig.cs @@ -4,7 +4,7 @@ public class CalculationDockerConfig { public required string DockerSocket { get; init; } - public required int Port { get; init; } + public int Port { get; init; } = 8080; - public required string Network { get; init; } + public string? Network { get; init; } } diff --git a/src/Adapters/Driven/Analysis.Instance.Docker/Extensions/AnalysisDockerInstanceExtensions.cs b/src/Adapters/Driven/Analysis.Instance.Docker/Extensions/AnalysisDockerInstanceExtensions.cs new file mode 100644 index 00000000..10664fb8 --- /dev/null +++ b/src/Adapters/Driven/Analysis.Instance.Docker/Extensions/AnalysisDockerInstanceExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using ScriptBee.Analysis.Instance.Docker.Config; +using ScriptBee.Ports.Instance; + +namespace ScriptBee.Analysis.Instance.Docker.Extensions; + +public static class AnalysisDockerInstanceExtensions +{ + public static IServiceCollection AddDockerInstanceAdapter( + this IServiceCollection services, + string dockerConfigSection + ) + { + services + .AddOptions() + .BindConfiguration("ScriptBee:Calculation:Docker"); + + return services + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } +} diff --git a/src/Adapters/Driven/Analysis.Instance.Docker/FreePortProvider.cs b/src/Adapters/Driven/Analysis.Instance.Docker/FreePortProvider.cs new file mode 100644 index 00000000..f9f6abc4 --- /dev/null +++ b/src/Adapters/Driven/Analysis.Instance.Docker/FreePortProvider.cs @@ -0,0 +1,16 @@ +using System.Net; +using System.Net.Sockets; + +namespace ScriptBee.Analysis.Instance.Docker; + +public class FreePortProvider : IFreePortProvider +{ + public int GetFreeTcpPort() + { + var l = new TcpListener(IPAddress.Loopback, 0); + l.Start(); + var port = ((IPEndPoint)l.LocalEndpoint).Port; + l.Stop(); + return port; + } +} diff --git a/src/Adapters/Driven/Analysis.Instance.Docker/IFreePortProvider.cs b/src/Adapters/Driven/Analysis.Instance.Docker/IFreePortProvider.cs new file mode 100644 index 00000000..0787bf0e --- /dev/null +++ b/src/Adapters/Driven/Analysis.Instance.Docker/IFreePortProvider.cs @@ -0,0 +1,6 @@ +namespace ScriptBee.Analysis.Instance.Docker; + +public interface IFreePortProvider +{ + int GetFreeTcpPort(); +} diff --git a/src/Adapters/Driven/Persistence.Mongodb/Entity/Analysis/MongodbAnalysisInfo.cs b/src/Adapters/Driven/Persistence.Mongodb/Entity/Analysis/MongodbAnalysisInfo.cs index e77e1853..bb149242 100644 --- a/src/Adapters/Driven/Persistence.Mongodb/Entity/Analysis/MongodbAnalysisInfo.cs +++ b/src/Adapters/Driven/Persistence.Mongodb/Entity/Analysis/MongodbAnalysisInfo.cs @@ -6,6 +6,7 @@ namespace ScriptBee.Persistence.Mongodb.Entity.Analysis; +[BsonIgnoreExtraElements] public class MongodbAnalysisInfo : IDocument { [BsonId] diff --git a/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectInstance.cs b/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectInstance.cs index 0dc787d5..71171ee1 100644 --- a/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectInstance.cs +++ b/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectInstance.cs @@ -4,6 +4,7 @@ namespace ScriptBee.Persistence.Mongodb.Entity; +[BsonIgnoreExtraElements] public class MongodbProjectInstance : IDocument { [BsonId] diff --git a/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectModel.cs b/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectModel.cs index 322978be..3ad161ca 100644 --- a/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectModel.cs +++ b/src/Adapters/Driven/Persistence.Mongodb/Entity/MongodbProjectModel.cs @@ -4,6 +4,7 @@ namespace ScriptBee.Persistence.Mongodb.Entity; +[BsonIgnoreExtraElements] public class MongodbProjectModel : IDocument { [BsonId] diff --git a/src/Adapters/Driven/Persistence.Mongodb/Entity/Script/MongodbScript.cs b/src/Adapters/Driven/Persistence.Mongodb/Entity/Script/MongodbScript.cs index 032c54cf..eb9fe24b 100644 --- a/src/Adapters/Driven/Persistence.Mongodb/Entity/Script/MongodbScript.cs +++ b/src/Adapters/Driven/Persistence.Mongodb/Entity/Script/MongodbScript.cs @@ -4,6 +4,7 @@ namespace ScriptBee.Persistence.Mongodb.Entity.Script; +[BsonIgnoreExtraElements] public class MongodbScript : IDocument { [BsonId] diff --git a/src/Adapters/Driving/Web/Extensions/ScriptBeeCalculationConfigExtensions.cs b/src/Adapters/Driving/Web/Extensions/ScriptBeeCalculationConfigExtensions.cs index f1473100..6e993155 100644 --- a/src/Adapters/Driving/Web/Extensions/ScriptBeeCalculationConfigExtensions.cs +++ b/src/Adapters/Driving/Web/Extensions/ScriptBeeCalculationConfigExtensions.cs @@ -1,5 +1,4 @@ -using ScriptBee.Analysis.Instance.Docker; -using ScriptBee.Analysis.Instance.Docker.Config; +using ScriptBee.Analysis.Instance.Docker.Extensions; using ScriptBee.Domain.Model.Analysis; using ScriptBee.Ports.Instance; using ScriptBee.Web.Config; @@ -30,12 +29,6 @@ ConfigurationManager configurationManager ) ); - services - .AddOptions() - .BindConfiguration("ScriptBee:Calculation:Docker"); - - return services - .AddSingleton() - .AddSingleton(); + return services.AddDockerInstanceAdapter("ScriptBee:Calculation:Docker"); } } diff --git a/src/Adapters/Driving/Web/appsettings.json b/src/Adapters/Driving/Web/appsettings.json index 5749e67d..2b4e0f13 100644 --- a/src/Adapters/Driving/Web/appsettings.json +++ b/src/Adapters/Driving/Web/appsettings.json @@ -31,7 +31,7 @@ }, "ScriptBee": { "Calculation": { - "Image": "scriptbee/calculation:latest", + "Image": "dxworks/scriptbee/calculation:latest", "Driver": "Docker", "Docker": { "DockerSocket": "unix:///var/run/docker.sock" diff --git a/test/Adapters/Driven/Analysis.Instance.Docker.Tests/CalculationInstanceDockerAdapterTest.cs b/test/Adapters/Driven/Analysis.Instance.Docker.Tests/CalculationInstanceDockerAdapterTest.cs index fa41c147..93608a4c 100644 --- a/test/Adapters/Driven/Analysis.Instance.Docker.Tests/CalculationInstanceDockerAdapterTest.cs +++ b/test/Adapters/Driven/Analysis.Instance.Docker.Tests/CalculationInstanceDockerAdapterTest.cs @@ -1,6 +1,7 @@ using Docker.DotNet.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using NSubstitute; using ScriptBee.Analysis.Instance.Docker.Config; using ScriptBee.Domain.Model.Analysis; using ScriptBee.Domain.Model.Instance; @@ -12,10 +13,11 @@ namespace ScriptBee.Analysis.Instance.Docker.Tests; public class CalculationInstanceDockerAdapterTest : IClassFixture { - private readonly IOptions _config; + private readonly IFreePortProvider _freePortProvider = Substitute.For(); private readonly DockerFixture _dockerFixture; private readonly CalculationInstanceDockerAdapter _calculationInstanceDockerAdapter; + private readonly int _testPort; public CalculationInstanceDockerAdapterTest( DockerFixture dockerFixture, @@ -23,21 +25,26 @@ ITestOutputHelper outputHelper ) { _dockerFixture = dockerFixture; - _config = Options.Create( + var config = Options.Create( new CalculationDockerConfig { DockerSocket = dockerFixture.DockerClient.Configuration.EndpointBaseUri.ToString(), Network = DockerFixture.TestNetworkName, - Port = 8080, } ); + _testPort = new FreePortProvider().GetFreeTcpPort(); + _freePortProvider.GetFreeTcpPort().Returns(_testPort); var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new XUnitLoggerProvider(outputHelper)) ); var logger = loggerFactory.CreateLogger(); - _calculationInstanceDockerAdapter = new CalculationInstanceDockerAdapter(_config, logger); + _calculationInstanceDockerAdapter = new CalculationInstanceDockerAdapter( + config, + logger, + _freePortProvider + ); } [Fact] @@ -49,7 +56,7 @@ public async Task Allocate_ShouldCreateAndStartContainerAndReturnUrlWithNetworkI var containerUrl = await _calculationInstanceDockerAdapter.Allocate(instanceId, image); containerUrl.ShouldStartWith("http://"); - containerUrl.ShouldContain($":{_config.Value.Port}"); + containerUrl.ShouldContain($":{_testPort}"); var containers = await _dockerFixture.DockerClient.Containers.ListContainersAsync( new ContainersListParameters { All = true } ); @@ -63,7 +70,7 @@ public async Task Allocate_ShouldCreateAndStartContainerAndReturnUrlWithNetworkI .NetworkSettings.Networks[DockerFixture.TestNetworkName] .IPAddress.ShouldNotBeNullOrEmpty(); containerUrl.ShouldBe( - $"http://{ourContainer.NetworkSettings.Networks[DockerFixture.TestNetworkName].IPAddress}:{_config.Value.Port}" + $"http://{ourContainer.NetworkSettings.Networks[DockerFixture.TestNetworkName].IPAddress}:{_testPort}" ); } diff --git a/test/Adapters/Driven/Analysis.Instance.Docker.Tests/FreePortProviderTest.cs b/test/Adapters/Driven/Analysis.Instance.Docker.Tests/FreePortProviderTest.cs new file mode 100644 index 00000000..84935fd6 --- /dev/null +++ b/test/Adapters/Driven/Analysis.Instance.Docker.Tests/FreePortProviderTest.cs @@ -0,0 +1,14 @@ +namespace ScriptBee.Analysis.Instance.Docker.Tests; + +public class FreePortProviderTest +{ + private readonly FreePortProvider _freePortProvider = new(); + + [Fact] + public void PortIsNotZero() + { + var port = _freePortProvider.GetFreeTcpPort(); + + port.ShouldBeGreaterThan(0); + } +}