diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisStats.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisStats.stories.tsx index 90ff4c5e872..49366e5158d 100644 --- a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisStats.stories.tsx +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisStats.stories.tsx @@ -34,6 +34,9 @@ export const Starting = Template.bind({}); Starting.args = { variantAnalysisStatus: VariantAnalysisStatus.InProgress, totalRepositoryCount: 10, + completedRepositoryCount: 0, + successfulRepositoryCount: 0, + skippedRepositoryCount: 0, }; export const Started = Template.bind({}); @@ -41,12 +44,20 @@ Started.args = { ...Starting.args, resultCount: 99_999, completedRepositoryCount: 2, + successfulRepositoryCount: 2, }; -export const StartedWithWarnings = Template.bind({}); -StartedWithWarnings.args = { +export const StartedWithSkippedRepositories = Template.bind({}); +StartedWithSkippedRepositories.args = { ...Starting.args, - hasWarnings: true, + skippedRepositoryCount: 3, +}; + +export const StartedWithFailedAnalyses = Template.bind({}); +StartedWithFailedAnalyses.args = { + ...Starting.args, + completedRepositoryCount: 5, + successfulRepositoryCount: 3, }; export const Succeeded = Template.bind({}); @@ -54,17 +65,35 @@ Succeeded.args = { ...Started.args, totalRepositoryCount: 1000, completedRepositoryCount: 1000, + successfulRepositoryCount: 1000, variantAnalysisStatus: VariantAnalysisStatus.Succeeded, createdAt: new Date(1661262726000), completedAt: new Date(1661263446000), }; -export const SucceededWithWarnings = Template.bind({}); -SucceededWithWarnings.args = { +export const SucceededWithSkippedRepositories = Template.bind({}); +SucceededWithSkippedRepositories.args = { ...Succeeded.args, totalRepositoryCount: 10, - completedRepositoryCount: 2, - hasWarnings: true, + completedRepositoryCount: 10, + successfulRepositoryCount: 10, + skippedRepositoryCount: 6, +}; + +export const SucceededWithFailedAnalyses = Template.bind({}); +SucceededWithFailedAnalyses.args = { + ...Succeeded.args, + totalRepositoryCount: 10, + completedRepositoryCount: 10, + successfulRepositoryCount: 7, +}; + +export const SucceededWithFailedAnalysesAndSkippedRepositories = Template.bind( + {}, +); +SucceededWithFailedAnalysesAndSkippedRepositories.args = { + ...SucceededWithFailedAnalyses.args, + skippedRepositoryCount: 6, }; export const Failed = Template.bind({}); @@ -77,6 +106,7 @@ Failed.args = { export const Stopped = Template.bind({}); Stopped.args = { - ...SucceededWithWarnings.args, + ...Started.args, variantAnalysisStatus: VariantAnalysisStatus.Canceled, + completedRepositoryCount: 10, }; diff --git a/extensions/ql-vscode/src/variant-analysis/shared/variant-analysis.ts b/extensions/ql-vscode/src/variant-analysis/shared/variant-analysis.ts index a370ddc5940..7674193d762 100644 --- a/extensions/ql-vscode/src/variant-analysis/shared/variant-analysis.ts +++ b/extensions/ql-vscode/src/variant-analysis/shared/variant-analysis.ts @@ -214,6 +214,16 @@ export function hasRepoScanCompleted( return isCompletedAnalysisRepoStatus(repo.analysisStatus); } +/** + * @param repo + * @returns whether the repo scan completed successfully + */ +export function isRepoScanSuccessful( + repo: VariantAnalysisScannedRepository, +): boolean { + return repo.analysisStatus === VariantAnalysisRepoStatus.Succeeded; +} + /** * @param repo * @returns whether the repo scan has an artifact that can be downloaded diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx index 9661acc5630..a579a426895 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx @@ -5,6 +5,7 @@ import { getSkippedRepoCount, getTotalResultCount, hasRepoScanCompleted, + isRepoScanSuccessful, VariantAnalysis, VariantAnalysisScannedRepositoryDownloadStatus, VariantAnalysisScannedRepositoryState, @@ -69,11 +70,17 @@ export const VariantAnalysisHeader = ({ ?.length ?? 0 ); }, [variantAnalysis.scannedRepos]); + const successfulRepositoryCount = useMemo(() => { + return ( + variantAnalysis.scannedRepos?.filter((repo) => isRepoScanSuccessful(repo)) + ?.length ?? 0 + ); + }, [variantAnalysis.scannedRepos]); const resultCount = useMemo(() => { return getTotalResultCount(variantAnalysis.scannedRepos); }, [variantAnalysis.scannedRepos]); - const hasSkippedRepos = useMemo(() => { - return getSkippedRepoCount(variantAnalysis.skippedRepos) > 0; + const skippedRepositoryCount = useMemo(() => { + return getSkippedRepoCount(variantAnalysis.skippedRepos); }, [variantAnalysis.skippedRepos]); const filteredRepositories = useMemo(() => { return filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos, { @@ -130,8 +137,9 @@ export const VariantAnalysisHeader = ({ variantAnalysisStatus={variantAnalysis.status} totalRepositoryCount={totalScannedRepositoryCount} completedRepositoryCount={completedRepositoryCount} + successfulRepositoryCount={successfulRepositoryCount} + skippedRepositoryCount={skippedRepositoryCount} resultCount={resultCount} - hasWarnings={hasSkippedRepos} createdAt={parseDate(variantAnalysis.createdAt)} completedAt={parseDate(variantAnalysis.completedAt)} onViewLogsClick={onViewLogsClick} diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisRepositoriesStats.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisRepositoriesStats.tsx index bd6cbf48857..39e99fe390e 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisRepositoriesStats.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisRepositoriesStats.tsx @@ -12,43 +12,78 @@ type Props = { variantAnalysisStatus: VariantAnalysisStatus; totalRepositoryCount: number; - completedRepositoryCount?: number | undefined; - - showWarning?: boolean; + completedRepositoryCount: number; + successfulRepositoryCount: number; + skippedRepositoryCount: number; }; +function getIcon( + variantAnalysisStatus: VariantAnalysisStatus, + completedRepositoryCount: number, + successfulRepositoryCount: number, + skippedRepositoryCount: number, +) { + if (successfulRepositoryCount < completedRepositoryCount) { + if (variantAnalysisStatus === VariantAnalysisStatus.Canceled) { + return ( + <> + + + + ); + } else { + return ( + <> + + + + ); + } + } else if (skippedRepositoryCount > 0) { + return ( + <> + + + + ); + } else if (variantAnalysisStatus === VariantAnalysisStatus.Succeeded) { + return ( + <> + + + + ); + } else { + return undefined; + } +} + export const VariantAnalysisRepositoriesStats = ({ variantAnalysisStatus, totalRepositoryCount, - completedRepositoryCount = 0, - showWarning, + completedRepositoryCount, + successfulRepositoryCount, + skippedRepositoryCount, }: Props) => { if (variantAnalysisStatus === VariantAnalysisStatus.Failed) { return ( <> 0 - + ); } return ( <> - {formatDecimal(completedRepositoryCount)}/ + {formatDecimal(successfulRepositoryCount)}/ {formatDecimal(totalRepositoryCount)} - {showWarning && ( - <> - - - + {getIcon( + variantAnalysisStatus, + completedRepositoryCount, + successfulRepositoryCount, + skippedRepositoryCount, )} - {!showWarning && - variantAnalysisStatus === VariantAnalysisStatus.Succeeded && ( - <> - - - - )} ); }; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisStats.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisStats.tsx index 25802d20e1a..69ae82be304 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisStats.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisStats.tsx @@ -12,9 +12,9 @@ export type VariantAnalysisStatsProps = { variantAnalysisStatus: VariantAnalysisStatus; totalRepositoryCount: number; - completedRepositoryCount?: number | undefined; - - hasWarnings?: boolean; + completedRepositoryCount: number; + successfulRepositoryCount: number; + skippedRepositoryCount: number; resultCount?: number | undefined; createdAt: Date; @@ -32,8 +32,9 @@ const Row = styled.div` export const VariantAnalysisStats = ({ variantAnalysisStatus, totalRepositoryCount, - completedRepositoryCount = 0, - hasWarnings, + completedRepositoryCount, + successfulRepositoryCount, + skippedRepositoryCount, resultCount, createdAt, completedAt, @@ -54,13 +55,17 @@ export const VariantAnalysisStats = ({ if ( variantAnalysisStatus === VariantAnalysisStatus.Succeeded && - hasWarnings + successfulRepositoryCount < completedRepositoryCount ) { - return "Succeeded warnings"; + return "Some analyses failed"; } return "Succeeded"; - }, [variantAnalysisStatus, hasWarnings]); + }, [ + variantAnalysisStatus, + successfulRepositoryCount, + completedRepositoryCount, + ]); const duration = useMemo(() => { if (!completedAt) { @@ -80,7 +85,8 @@ export const VariantAnalysisStats = ({ variantAnalysisStatus={variantAnalysisStatus} totalRepositoryCount={totalRepositoryCount} completedRepositoryCount={completedRepositoryCount} - showWarning={hasWarnings} + successfulRepositoryCount={successfulRepositoryCount} + skippedRepositoryCount={skippedRepositoryCount} /> diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx index 30cf29b9837..d139461483d 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisStats.spec.tsx @@ -19,6 +19,9 @@ describe(VariantAnalysisStats.name, () => { { expect(screen.queryAllByText("-").length).toBe(1); }); - it("renders the number of repositories as a formatted number", () => { - render({ totalRepositoryCount: 123456, completedRepositoryCount: 654321 }); + it("renders the number of successful repositories as a formatted number", () => { + render({ + totalRepositoryCount: 123456, + successfulRepositoryCount: 654321, + }); expect(screen.getByText("654,321/123,456")).toBeInTheDocument(); }); - it("renders a warning icon when has warnings is set", () => { - render({ hasWarnings: true }); + it("renders a warning icon when skippedRepositoryCount is greater than zero", () => { + render({ skippedRepositoryCount: 4 }); expect( screen.getByRole("img", { - name: "Warning", + name: "Some repositories were skipped", }), ).toBeInTheDocument(); }); @@ -59,7 +65,7 @@ describe(VariantAnalysisStats.name, () => { expect( screen.getByRole("img", { - name: "Error", + name: "Variant analysis failed", }), ).toBeInTheDocument(); }); @@ -74,7 +80,68 @@ describe(VariantAnalysisStats.name, () => { ).toBeInTheDocument(); }); - it("renders a view logs link when the variant analysis status is succeeded", () => { + it("renders an error icon when the overall variant analysis status is in progress but some analyses failed", () => { + render({ + variantAnalysisStatus: VariantAnalysisStatus.InProgress, + completedRepositoryCount: 10, + successfulRepositoryCount: 5, + }); + + expect( + screen.getByRole("img", { + name: "Some analyses failed", + }), + ).toBeInTheDocument(); + }); + + it("renders an error icon when the overall variant analysis status is succeeded but some analyses failed", () => { + render({ + variantAnalysisStatus: VariantAnalysisStatus.Succeeded, + completedRepositoryCount: 10, + successfulRepositoryCount: 5, + }); + + expect( + screen.getByRole("img", { + name: "Some analyses failed", + }), + ).toBeInTheDocument(); + }); + + it("renders an error icon when the overall variant analysis status is canceled and some analyses failed", () => { + render({ + variantAnalysisStatus: VariantAnalysisStatus.Canceled, + completedRepositoryCount: 10, + successfulRepositoryCount: 5, + }); + + expect( + screen.getByRole("img", { + name: "Some analyses were stopped", + }), + ).toBeInTheDocument(); + }); + + it("renders an error icon when some analyses failed but also some repositories were skipped", () => { + render({ + completedRepositoryCount: 10, + successfulRepositoryCount: 5, + skippedRepositoryCount: 2, + }); + + expect( + screen.getByRole("img", { + name: "Some analyses failed", + }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("img", { + name: "Some repositories were skipped", + }), + ).not.toBeInTheDocument(); + }); + + it("renders 'View logs' link when the variant analysis status is succeeded", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded, completedAt: new Date(), @@ -84,38 +151,48 @@ describe(VariantAnalysisStats.name, () => { expect(onViewLogsClick).toHaveBeenCalledTimes(1); }); - it("renders a running text when the variant analysis status is in progress", () => { + it("renders 'Running' text when the variant analysis status is in progress", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.InProgress }); expect(screen.getByText("Running")).toBeInTheDocument(); }); - it("renders a failed text when the variant analysis status is failed", () => { + it("renders 'Failed' text when the variant analysis status is failed", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.Failed }); expect(screen.getByText("Failed")).toBeInTheDocument(); }); - it("renders a stopped text when the variant analysis status is canceled", () => { + it("renders 'Stopped' text when the variant analysis status is canceled", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.Canceled }); expect(screen.getByText("Stopped")).toBeInTheDocument(); }); - it("renders a succeeded warnings text when the variant analysis status is succeeded and has warnings", () => { + it("renders 'Some analyses failed' text when the overall variant analysis status is succeeded but not all analyses successful", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded, - hasWarnings: true, + completedRepositoryCount: 10, + successfulRepositoryCount: 5, }); - expect(screen.getByText("Succeeded warnings")).toBeInTheDocument(); + expect(screen.getByText("Some analyses failed")).toBeInTheDocument(); }); - it("renders a succeeded text when the variant analysis status is succeeded", () => { + it("renders 'Succeeded' text when the variant analysis status is succeeded and successful repository count omitted", () => { render({ variantAnalysisStatus: VariantAnalysisStatus.Succeeded }); expect(screen.getByText("Succeeded")).toBeInTheDocument(); - expect(screen.queryByText("Succeeded warnings")).not.toBeInTheDocument(); + }); + + it("renders 'Succeeded' text when the variant analysis status is succeeded and successful repository count equals total repository count", () => { + render({ + variantAnalysisStatus: VariantAnalysisStatus.Succeeded, + completedRepositoryCount: 10, + successfulRepositoryCount: 10, + }); + + expect(screen.getByText("Succeeded")).toBeInTheDocument(); }); it("does not render the duration when the completedAt is not set", () => {