From 4bcc5a53114d75936dedc5acc023e194357eeac9 Mon Sep 17 00:00:00 2001 From: Ibrahim Matar Date: Sat, 16 Aug 2025 08:51:38 -1000 Subject: [PATCH 1/3] feat(ui,toast,upload): green info toasts; search by id/name; hover full name; auto-enable filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/app/shared/services/toast.service.ts: add showInfoToast(title, message). - src/app/shared/components/toast/toast.component.html: bind [ngClass]="type" on header/body. - src/app/shared/components/toast/toast.component.scss: info=green (#4CAF50), warning=amber (#FFC107), error=red (#F44336); lighter bodies. - src/app/shared/services/sbom.service.ts: when adding by ID, ensure SetSBOMFormat(...) and SetSBOMSchema(...) are set to true; minor typing/index-signature fixes. - src/app/features/upload/upload.component.html: search matches alias OR id; add title="{{ getAlias(item) }}" to show full filename on hover. - src/app/features/upload/upload.component.ts: ClearSearch() re-enables all discovered formats/schemas so rows aren’t hidden after clearing search. Result: clear success/error visuals, immediate visibility of newly added SBOMs, and more effective search by name or id. --- src/app/features/upload/upload.component.html | 4 +-- src/app/features/upload/upload.component.ts | 5 ++- .../components/toast/toast.component.html | 4 +-- .../components/toast/toast.component.scss | 26 +++++++++++++-- src/app/shared/services/sbom.service.ts | 32 +++++++++++-------- src/app/shared/services/toast.service.ts | 8 +++++ 6 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/app/features/upload/upload.component.html b/src/app/features/upload/upload.component.html index 5348c195..9987aeee 100644 --- a/src/app/features/upload/upload.component.html +++ b/src/app/features/upload/upload.component.html @@ -25,11 +25,11 @@

Format {{sortingOptions['FORMAT'] ? '▴' : ' (dragover)="onDragOver($event)" (dragleave)="onDragLeave($event)">
-
+
- +
{{GetSBOMInfo(item).format}} diff --git a/src/app/features/upload/upload.component.ts b/src/app/features/upload/upload.component.ts index 77b2f3b2..d8fe063f 100644 --- a/src/app/features/upload/upload.component.ts +++ b/src/app/features/upload/upload.component.ts @@ -313,7 +313,7 @@ export class UploadComponent implements OnInit { const files = event.dataTransfer?.files; if (files && files.length > 0) { - const filePaths = Array.from(files).map((file) => file.path); + const filePaths = Array.from(files).map((file: any) => (file.path as string)); this.sbomService.AddFiles(filePaths); } } @@ -331,6 +331,9 @@ export class UploadComponent implements OnInit { if (searchInput) { searchInput.value = ''; } + // Re-enable all formats and schemas so nothing stays hidden + Object.keys(this.GetSBOMFormat()).forEach((key: string) => this.sbomService.SetSBOMFormat(key, true)); + Object.keys(this.GetSBOMSchemas()).forEach((key: string) => this.sbomService.SetSBOMSchema(key, true)); } UpdateSearch(event: any) { diff --git a/src/app/shared/components/toast/toast.component.html b/src/app/shared/components/toast/toast.component.html index 6aa635b1..d8e1e0a7 100644 --- a/src/app/shared/components/toast/toast.component.html +++ b/src/app/shared/components/toast/toast.component.html @@ -6,9 +6,9 @@ aria-live="assertive" aria-atomic="true" > -
+
{{ title }}
-
{{ message }}
+
{{ message }}
\ No newline at end of file diff --git a/src/app/shared/components/toast/toast.component.scss b/src/app/shared/components/toast/toast.component.scss index 9fe50d9c..f52b311b 100644 --- a/src/app/shared/components/toast/toast.component.scss +++ b/src/app/shared/components/toast/toast.component.scss @@ -1,16 +1,36 @@ .warning { - background-color: #F44336; + background-color: #FFC107; + } + + .info { + background-color: #4CAF50; } .error { background-color: #F44336; } - .toast-header { + .toast-header.info { + background-color: #4CAF50; + } + + .toast-header.warning { + background-color: #FFC107; + } + + .toast-header.error { background-color: #F44336; } - .toast-body { + .toast-body.info { + background-color: #E8F5E9; + } + + .toast-body.warning { + background-color: #FFF8E1; + } + + .toast-body.error { background-color: #FDE0DE; } \ No newline at end of file diff --git a/src/app/shared/services/sbom.service.ts b/src/app/shared/services/sbom.service.ts index fd42f23c..b96d387c 100644 --- a/src/app/shared/services/sbom.service.ts +++ b/src/app/shared/services/sbom.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { SVIPService } from './SVIP.service'; import { PAGES, RoutingService } from './routing.service'; import File, { FileStatus } from '../models/file'; +import { SBOM } from '../models/sbom'; @Injectable({ providedIn: 'root', @@ -24,14 +25,17 @@ export class SbomService { * @param getSBOM by ID */ addSBOMbyID(id: number) { - this.SVIPService.getSBOM(id as number).subscribe((sbom) => { + this.SVIPService.getSBOM(id as number).subscribe((sbom: SBOM) => { this.SVIPService.getSBOMContents(id as number).subscribe((data: any) => { - let path = data.fileName; - let contents = data.contents; + let path = (data as any).fileName; + let contents = (data as any).contents as string; const file = new File(path).setValid(id, contents, sbom); this.files[id] = file; this.SetSBOMFormat(sbom.format, true); + if ((sbom as any)['schema']) { + this.SetSBOMSchema((sbom as any)['schema'], true); + } }); }); } @@ -40,9 +44,9 @@ export class SbomService { * Gets all SBOMS in database and sets up SBOM service */ getAllSBOMs() { - this.SVIPService.getSBOMS().subscribe((ids) => { + this.SVIPService.getSBOMS().subscribe((ids: number[]) => { if (ids) { - ids.forEach((id) => this.addSBOMbyID(id)); + ids.forEach((id: number) => this.addSBOMbyID(id)); } }); } @@ -56,13 +60,13 @@ export class SbomService { let randomID = -Math.random().toString() + "-loading"; // File is loading this.files[randomID] = new File(path); - this.SVIPService.getFileData(path).then((contents) => { + this.SVIPService.getFileData(path).then((contents: string) => { if (contents) { this.SVIPService.uploadSBOM(path, contents).subscribe( - (id) => { + (id: number) => { if (id) { // Successful upload - this.SVIPService.getSBOM(id).subscribe((sbom) => { + this.SVIPService.getSBOM(id).subscribe((sbom: SBOM) => { delete this.files[randomID]; let file = new File(path).setValid(id, contents, sbom); this.files[id] = file; @@ -87,7 +91,7 @@ export class SbomService { downloadSBOM(id: string): Blob { const file = this.files[id]?.contents; if (file !== null) { - return new Blob([file]); + return new Blob([file as any]); } throw new Error('File does not exist!'); } @@ -107,7 +111,7 @@ export class SbomService { idList.unshift(Number(targetID)); - this.SVIPService.compareSBOMs(idList).subscribe((result) => { + this.SVIPService.compareSBOMs(idList).subscribe((result: any) => { this.comparison = result; }); } @@ -119,7 +123,7 @@ export class SbomService { deleteFile(id: string) { if (id && !isNaN(Number(id))) { // TODO: Add error handling for when file cannot be deleted - this.SVIPService.deleteSBOM(Number(id)).subscribe((deleted) => { + this.SVIPService.deleteSBOM(Number(id)).subscribe((deleted: any) => { if (deleted) { const data = this.routingService.data; if (data === id) { @@ -148,7 +152,7 @@ export class SbomService { overwrite: boolean ) { this.SVIPService.convertSBOM(Number(id), schema, format, overwrite).subscribe( - (result) => { + (result: string) => { if (result) { this.addSBOMbyID(Number(result)); @@ -182,7 +186,7 @@ export class SbomService { * @param id sbom to check for */ GetSBOMSchema(id: string) { - return this.files[id].schema; + return (this.files[id] as any).schema; } //#region SBOM format @@ -207,7 +211,7 @@ export class SbomService { * @param id sbom to check for */ GetSBOMFormat(id: string) { - return this.files[id].format; + return (this.files[id] as any).format; } //#endregion diff --git a/src/app/shared/services/toast.service.ts b/src/app/shared/services/toast.service.ts index 0fa7e815..4d4cead8 100644 --- a/src/app/shared/services/toast.service.ts +++ b/src/app/shared/services/toast.service.ts @@ -14,6 +14,14 @@ export class ToastService { this.toastEvents = this._toastEvents.asObservable(); } + showInfoToast(title: string, message: string) { + this._toastEvents.next({ + message, + title, + type: EventTypes.Info, + }); + } + showWarningToast(title: string, message: string) { this._toastEvents.next({ message, From fbb3c57a8aec5a0d6554317f50feae24e0d4c9b4 Mon Sep 17 00:00:00 2001 From: Ibrahim Matar Date: Thu, 21 Aug 2025 23:13:25 -1000 Subject: [PATCH 2/3] feat(ui,scripts): support pre-zipped project uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ui: allow selecting an existing .zip to skip client-side zipping SVIP.service.ts: uploadProject now passes through File if already a File generate-modal.component.ts: add SELECTING_SOURCE state, SelectFolderAndZip(), OnZipFileSelected(); update flow to upload selected .zip directly and fetch OSI tools generate-modal.component.html: add “Select Source” step with “Select folder and zip” and “Use existing .zip” no backend changes required (POST /svip/generators/osi/project already accepts zipped project) --- .../generate-modal.component.html | 12 ++++ .../generate-modal.component.ts | 59 +++++++++++-------- src/app/shared/services/SVIP.service.ts | 3 +- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html index a6985a02..1c2c9c26 100644 --- a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html +++ b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html @@ -6,6 +6,18 @@
Waiting for Project Selection...
+
+
Select Source
+
+
+ Select folder and zip +
+ +
+
Zipping Project Contents...
diff --git a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts index ab912e3e..dbbbbeda 100644 --- a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts +++ b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts @@ -37,6 +37,7 @@ export class GenerateModalComponent implements OnInit { public status: GenerationStatus = GenerationStatus.NULL; public zippedFileData: any; + public useExistingZip: boolean = false; constructor(private service: SVIPService, private sbomService: SbomService, private toast: ToastService) {} @@ -46,32 +47,7 @@ export class GenerateModalComponent implements OnInit { return; this.zippedFileData = undefined; - this.status = GenerationStatus.GENERATING; - - this.service.getProjectDirectory().then((result) => { - this.status = GenerationStatus.ZIPPING; - - this.service.zipFileDirectory(result).then((data) => { - - //TODO: Prompt user beforehand on OSI or Parsers so don't need to upload project if don't have to OR the backend should be reworked for parsers - this.service.uploadProject(data, 'osi').then((tools: any) => { - - tools.forEach((tool: any) => { - this.osiTools[tool] = true; - }) - - this.zippedFileData = data; - this.status = GenerationStatus.PROJECT_INFO; - - }).catch((error) => { - this.Close(); - }) - }).catch((error) => { - this.Close(); - }) - }).catch((error) => { - this.Close(); - }) + this.status = GenerationStatus.SELECTING_SOURCE; }); } @@ -112,6 +88,36 @@ export class GenerateModalComponent implements OnInit { this.osiTools[event.name] = event.value; } + SelectFolderAndZip() { + this.status = GenerationStatus.ZIPPING; + this.service.getProjectDirectory().then((result) => { + this.service.zipFileDirectory(result).then((data) => { + this.service.uploadProject(data, 'osi').then((tools: any) => { + tools.forEach((tool: any) => { + this.osiTools[tool] = true; + }) + this.zippedFileData = data; + this.status = GenerationStatus.PROJECT_INFO; + }).catch(() => { this.Close(); }) + }).catch(() => { this.Close(); }) + }).catch(() => { this.Close(); }) + } + + OnZipFileSelected(event: any) { + const files: FileList = event.target.files; + if (!files || files.length === 0) + return; + const zip = files[0]; + this.status = GenerationStatus.GENERATING; + this.service.uploadProject(zip, 'osi').then((tools: any) => { + tools.forEach((tool: any) => { + this.osiTools[tool] = true; + }) + this.zippedFileData = zip; + this.status = GenerationStatus.PROJECT_INFO; + }).catch(() => { this.Close(); }) + } + Close() { this.status = GenerationStatus.NULL; @@ -126,4 +132,5 @@ enum GenerationStatus { ZIPPING, PROJECT_INFO, GENERATING, + SELECTING_SOURCE, } diff --git a/src/app/shared/services/SVIP.service.ts b/src/app/shared/services/SVIP.service.ts index 2ffce0b0..31438813 100644 --- a/src/app/shared/services/SVIP.service.ts +++ b/src/app/shared/services/SVIP.service.ts @@ -222,7 +222,8 @@ export class SVIPService { async uploadProject(file: any, type: string) { return new Promise(async(resolve, reject) => { let formData = new FormData(); - formData.append('project', new File([file], 'temp.zip')); + const fileToSend = (file instanceof File) ? file : new File([file], 'temp.zip'); + formData.append('project', fileToSend); let params = new HttpParams(); From a00a23b762230901ba6d29f1b3e463af70135cfa Mon Sep 17 00:00:00 2001 From: Ibrahim Matar Date: Thu, 21 Aug 2025 23:33:34 -1000 Subject: [PATCH 3/3] chore(ui): comment out pre-zipped upload feature; keep original zip-before-upload service: comment passthrough, restore wrapper File('temp.zip') modal.ts: comment pre-zipped state/handlers; keep original zip+upload flow modal.html: retain placeholder; optional full UI preserved as commented block --- .../generate-modal.component.html | 2 + .../generate-modal.component.ts | 93 ++++++++++++------- src/app/shared/services/SVIP.service.ts | 7 +- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html index 1c2c9c26..628f46b4 100644 --- a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html +++ b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.html @@ -6,6 +6,7 @@
Waiting for Project Selection...
+
Zipping Project Contents...
diff --git a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts index dbbbbeda..74d9b847 100644 --- a/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts +++ b/src/app/shared/components/toolbar/modals/generate-modal/generate-modal.component.ts @@ -37,7 +37,9 @@ export class GenerateModalComponent implements OnInit { public status: GenerationStatus = GenerationStatus.NULL; public zippedFileData: any; - public useExistingZip: boolean = false; + // Pre-zipped upload temporarily disabled; keep flag for future use + // public useExistingZip: boolean = false; + // public selectingSource: boolean = true; // when re-enabled, use to gate source step constructor(private service: SVIPService, private sbomService: SbomService, private toast: ToastService) {} @@ -47,7 +49,32 @@ export class GenerateModalComponent implements OnInit { return; this.zippedFileData = undefined; - this.status = GenerationStatus.SELECTING_SOURCE; + // Reverted to original flow; previous pre-zip source selection kept below as comments + this.status = GenerationStatus.GENERATING; + + this.service.getProjectDirectory().then((result) => { + this.status = GenerationStatus.ZIPPING; + + this.service.zipFileDirectory(result).then((data) => { + + this.service.uploadProject(data, 'osi').then((tools: any) => { + + tools.forEach((tool: any) => { + this.osiTools[tool] = true; + }) + + this.zippedFileData = data; + this.status = GenerationStatus.PROJECT_INFO; + + }).catch((error) => { + this.Close(); + }) + }).catch((error) => { + this.Close(); + }) + }).catch((error) => { + this.Close(); + }) }); } @@ -88,35 +115,37 @@ export class GenerateModalComponent implements OnInit { this.osiTools[event.name] = event.value; } - SelectFolderAndZip() { - this.status = GenerationStatus.ZIPPING; - this.service.getProjectDirectory().then((result) => { - this.service.zipFileDirectory(result).then((data) => { - this.service.uploadProject(data, 'osi').then((tools: any) => { - tools.forEach((tool: any) => { - this.osiTools[tool] = true; - }) - this.zippedFileData = data; - this.status = GenerationStatus.PROJECT_INFO; - }).catch(() => { this.Close(); }) - }).catch(() => { this.Close(); }) - }).catch(() => { this.Close(); }) - } - - OnZipFileSelected(event: any) { - const files: FileList = event.target.files; - if (!files || files.length === 0) - return; - const zip = files[0]; - this.status = GenerationStatus.GENERATING; - this.service.uploadProject(zip, 'osi').then((tools: any) => { - tools.forEach((tool: any) => { - this.osiTools[tool] = true; - }) - this.zippedFileData = zip; - this.status = GenerationStatus.PROJECT_INFO; - }).catch(() => { this.Close(); }) - } + // --- Begin: Pre-zipped flow (commented out) --- + // SelectFolderAndZip() { + // this.status = GenerationStatus.ZIPPING; + // this.service.getProjectDirectory().then((result) => { + // this.service.zipFileDirectory(result).then((data) => { + // this.service.uploadProject(data, 'osi').then((tools: any) => { + // tools.forEach((tool: any) => { + // this.osiTools[tool] = true; + // }) + // this.zippedFileData = data; + // this.status = GenerationStatus.PROJECT_INFO; + // }).catch(() => { this.Close(); }) + // }).catch(() => { this.Close(); }) + // }).catch(() => { this.Close(); }) + // } + // + // OnZipFileSelected(event: any) { + // const files: FileList = event.target.files; + // if (!files || files.length === 0) + // return; + // const zip = files[0]; + // this.status = GenerationStatus.GENERATING; + // this.service.uploadProject(zip, 'osi').then((tools: any) => { + // tools.forEach((tool: any) => { + // this.osiTools[tool] = true; + // }) + // this.zippedFileData = zip; + // this.status = GenerationStatus.PROJECT_INFO; + // }).catch(() => { this.Close(); }) + // } + // --- End: Pre-zipped flow (commented out) --- Close() { @@ -132,5 +161,5 @@ enum GenerationStatus { ZIPPING, PROJECT_INFO, GENERATING, - SELECTING_SOURCE, + // SELECTING_SOURCE, // disabled } diff --git a/src/app/shared/services/SVIP.service.ts b/src/app/shared/services/SVIP.service.ts index 31438813..0db80c11 100644 --- a/src/app/shared/services/SVIP.service.ts +++ b/src/app/shared/services/SVIP.service.ts @@ -222,8 +222,11 @@ export class SVIPService { async uploadProject(file: any, type: string) { return new Promise(async(resolve, reject) => { let formData = new FormData(); - const fileToSend = (file instanceof File) ? file : new File([file], 'temp.zip'); - formData.append('project', fileToSend); + // Disabled pre-zipped passthrough (kept for future use): + // const fileToSend = (file instanceof File) ? file : new File([file], 'temp.zip'); + // formData.append('project', fileToSend); + // Reverted to original behavior: always wrap provided bytes into a new File + formData.append('project', new File([file], 'temp.zip')); let params = new HttpParams();