diff --git a/shared/models/ISbomBuildArtifact.ts b/shared/models/ISbomBuildArtifact.ts index 1aa3857..527424c 100644 --- a/shared/models/ISbomBuildArtifact.ts +++ b/shared/models/ISbomBuildArtifact.ts @@ -1,6 +1,7 @@ import { IDocument } from './spdx/2.3/IDocument'; export interface ISbomBuildArtifact { + id: string; spdxDocument: IDocument; xlsxDocument?: ArrayBuffer; svgDocument?: ArrayBuffer; diff --git a/ui/components/sbom/SpdxSummaryCard.tsx b/ui/components/sbom/SpdxSummaryCard.tsx index e97853e..2fb348f 100644 --- a/ui/components/sbom/SpdxSummaryCard.tsx +++ b/ui/components/sbom/SpdxSummaryCard.tsx @@ -299,7 +299,7 @@ export class SpdxSummaryCard extends React.Component { diff --git a/ui/sbom-report-tab.tsx b/ui/sbom-report-tab.tsx index ce56800..35b9c48 100644 --- a/ui/sbom-report-tab.tsx +++ b/ui/sbom-report-tab.tsx @@ -4,8 +4,11 @@ import * as ReactDOM from 'react-dom'; import { CommonServiceIds, getClient, IProjectPageService } from 'azure-devops-extension-api'; import { BuildServiceIds, IBuildPageDataService } from 'azure-devops-extension-api/Build'; +import { ObservableValue } from 'azure-devops-ui/Core/Observable'; import { MessageCard, MessageCardSeverity } from 'azure-devops-ui/MessageCard'; +import { Observer } from 'azure-devops-ui/Observer'; import { Spinner } from 'azure-devops-ui/Spinner'; +import { Tab, TabBar, TabContent, TabSize } from 'azure-devops-ui/Tabs'; import { ZeroData } from 'azure-devops-ui/ZeroData'; import '../shared/extensions/ArrayExtensions'; @@ -28,9 +31,12 @@ interface State { } export class Root extends React.Component<{}, State> { + private selectedArtifactId: ObservableValue; + constructor(props: {}) { super(props); this.state = { artifacts: undefined, loadError: undefined }; + this.selectedArtifactId = new ObservableValue(''); } public componentDidMount() { @@ -133,6 +139,7 @@ export class Root extends React.Component<{}, State> { } sbomArtifacts.push({ + id: spdxDocument.documentNamespace, spdxDocument: spdxDocument, svgDocument: spdxSvgDocumentStream, }); @@ -142,6 +149,7 @@ export class Root extends React.Component<{}, State> { } console.info(`Loaded ${Object.keys(sbomArtifacts).length} SBOM artifact(s) for build ${buildId}`); + this.selectedArtifactId.value = sbomArtifacts[0]?.spdxDocument?.documentNamespace || ''; this.setState({ artifacts: sbomArtifacts, loadError: undefined }); } catch (error) { console.error(error); @@ -162,13 +170,22 @@ export class Root extends React.Component<{}, State> { } console.info(`Loaded SBOM artifact from '${file.name}'`); - this.setState({ artifacts: [{ spdxDocument: spdxDocument }], loadError: undefined }); + const newArtifact: ISbomBuildArtifact = { id: spdxDocument.documentNamespace, spdxDocument: spdxDocument }; + this.selectedArtifactId.value = newArtifact.id; + this.setState({ + artifacts: [...(this.state.artifacts || []), newArtifact], + loadError: undefined, + }); } catch (error) { console.error(error); - this.setState({ artifacts: undefined, loadError: error }); + this.setState({ artifacts: this.state.artifacts, loadError: error }); } } + private onSelectedArtifactTabChanged = (newSpdxId: string) => { + this.selectedArtifactId.value = newSpdxId; + }; + public render(): JSX.Element { return (
@@ -178,19 +195,51 @@ export class Root extends React.Component<{}, State> { ) : !this.state.artifacts ? ( - ) : !this.state.artifacts[0] ? ( + ) : this.state.artifacts.length == 0 ? ( - ) : ( - // TODO: Add support for viewing multiple artifacts in a single build? + ) : this.state.artifacts.length == 1 ? ( this.loadSbomArtifactFromFileUpload(file)} /> + ) : ( +
+ + {this.state.artifacts.map((artifact, index) => ( + + ))} + + + + {(props: { selectedArtifactId: string }) => + props.selectedArtifactId && + this.state.artifacts?.find((artifact) => artifact.id === props.selectedArtifactId) ? ( + artifact.id === props.selectedArtifactId)!} + onLoadArtifact={(file) => this.loadSbomArtifactFromFileUpload(file)} + /> + ) : ( + + ) + } + + +
)}
);