From d3701944bfae0162ab91620f0305550432f76d95 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Mon, 26 Sep 2022 14:02:57 +0200 Subject: [PATCH] Add outcome panels This creates the component for showing the outcome panels. It does not implement the content of each individual panel; it only implements the tabs, panel views, and the general warnings. --- .../VariantAnalysisOutcomePanels.stories.tsx | 151 +++++++++++++ .../view/variant-analysis/VariantAnalysis.tsx | 57 ++++- .../VariantAnalysisAnalyzedRepos.tsx | 5 + .../VariantAnalysisNoCodeqlDbRepos.tsx | 5 + .../VariantAnalysisNotFoundRepos.tsx | 5 + .../VariantAnalysisOutcomePanels.tsx | 97 ++++++++ .../VariantAnalysisOutcomePanels.spec.tsx | 207 ++++++++++++++++++ 7 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisOutcomePanels.stories.tsx create mode 100644 extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx create mode 100644 extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx create mode 100644 extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx create mode 100644 extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx create mode 100644 extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisOutcomePanels.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisOutcomePanels.stories.tsx new file mode 100644 index 00000000000..55b7ecd6173 --- /dev/null +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisOutcomePanels.stories.tsx @@ -0,0 +1,151 @@ +import React from 'react'; + +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer'; +import { VariantAnalysisOutcomePanels } from '../../view/variant-analysis/VariantAnalysisOutcomePanels'; +import { + VariantAnalysis, + VariantAnalysisQueryLanguage, + VariantAnalysisRepoStatus, + VariantAnalysisScannedRepository, + VariantAnalysisStatus +} from '../../remote-queries/shared/variant-analysis'; + +export default { + title: 'Variant Analysis/Variant Analysis Outcome Panels', + component: VariantAnalysisOutcomePanels, + decorators: [ + (Story) => ( + + + + ) + ], +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +const buildVariantAnalysis = (data: Partial) => ({ + id: 1, + controllerRepoId: 1, + query: { + name: 'Query name', + filePath: 'example.ql', + language: VariantAnalysisQueryLanguage.Javascript, + }, + databases: {}, + status: VariantAnalysisStatus.InProgress, + ...data, +}); + +const buildScannedRepo = (id: number, data?: Partial): VariantAnalysisScannedRepository => ({ + repository: { + id: id, + fullName: `octodemo/hello-world-${id}`, + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + ...data, +}); + +export const WithoutSkippedRepos = Template.bind({}); +WithoutSkippedRepos.args = { + variantAnalysis: buildVariantAnalysis({ + scannedRepos: [ + buildScannedRepo(1, { + analysisStatus: VariantAnalysisRepoStatus.Succeeded, + resultCount: 99_999, + }), + buildScannedRepo(2, { + analysisStatus: VariantAnalysisRepoStatus.Failed, + }), + buildScannedRepo(3, { + analysisStatus: VariantAnalysisRepoStatus.Succeeded, + resultCount: 0, + }), + buildScannedRepo(4), + buildScannedRepo(5), + buildScannedRepo(6), + buildScannedRepo(7), + buildScannedRepo(8), + buildScannedRepo(9), + buildScannedRepo(10), + ] + }), +}; + +export const WithSkippedRepos = Template.bind({}); +WithSkippedRepos.args = { + ...WithoutSkippedRepos.args, + variantAnalysis: buildVariantAnalysis({ + ...WithoutSkippedRepos.args.variantAnalysis, + skippedRepos: { + notFoundRepos: { + repositoryCount: 2, + repositories: [ + { + fullName: 'octodemo/hello-globe' + }, + { + fullName: 'octodemo/hello-planet' + } + ] + }, + noCodeqlDbRepos: { + repositoryCount: 4, + repositories: [ + { + id: 100, + fullName: 'octodemo/no-db-1' + }, + { + id: 101, + fullName: 'octodemo/no-db-2' + }, + { + id: 102, + fullName: 'octodemo/no-db-3' + }, + { + id: 103, + fullName: 'octodemo/no-db-4' + } + ] + }, + overLimitRepos: { + repositoryCount: 1, + repositories: [ + { + id: 201, + fullName: 'octodemo/over-limit-1' + } + ] + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: [ + { + id: 205, + fullName: 'octodemo/private' + } + ] + } + }, + }), +}; + +export const WithOnlyWarningsSkippedRepos = Template.bind({}); +WithOnlyWarningsSkippedRepos.args = { + ...WithoutSkippedRepos.args, + variantAnalysis: buildVariantAnalysis({ + ...WithSkippedRepos.args.variantAnalysis, + skippedRepos: { + ...WithSkippedRepos.args.variantAnalysis?.skippedRepos, + notFoundRepos: undefined, + noCodeqlDbRepos: undefined, + } + }), +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx index 80c5e973c05..6293a2bbf22 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx @@ -8,10 +8,12 @@ import { } from '../../remote-queries/shared/variant-analysis'; import { VariantAnalysisContainer } from './VariantAnalysisContainer'; import { VariantAnalysisHeader } from './VariantAnalysisHeader'; +import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels'; const variantAnalysis: VariantAnalysisDomainModel = { id: 1, controllerRepoId: 1, + actionsWorkflowRunId: 789263, query: { name: 'Example query', filePath: 'example.ql', @@ -100,7 +102,59 @@ const variantAnalysis: VariantAnalysisDomainModel = { }, analysisStatus: VariantAnalysisRepoStatus.Pending, }, - ] + ], + skippedRepos: { + notFoundRepos: { + repositoryCount: 2, + repositories: [ + { + fullName: 'octodemo/hello-globe' + }, + { + fullName: 'octodemo/hello-planet' + } + ] + }, + noCodeqlDbRepos: { + repositoryCount: 4, + repositories: [ + { + id: 100, + fullName: 'octodemo/no-db-1' + }, + { + id: 101, + fullName: 'octodemo/no-db-2' + }, + { + id: 102, + fullName: 'octodemo/no-db-3' + }, + { + id: 103, + fullName: 'octodemo/no-db-4' + } + ] + }, + overLimitRepos: { + repositoryCount: 1, + repositories: [ + { + id: 201, + fullName: 'octodemo/over-limit-1' + } + ] + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: [ + { + id: 205, + fullName: 'octodemo/private' + } + ] + } + }, }; export function VariantAnalysis(): JSX.Element { @@ -115,6 +169,7 @@ export function VariantAnalysis(): JSX.Element { onExportResultsClick={() => console.log('Export results')} onViewLogsClick={() => console.log('View logs')} /> + ); } diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx new file mode 100644 index 00000000000..b068bcb831e --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const VariantAnalysisAnalyzedRepos = () => { + return
This is the analyzed view
; +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx new file mode 100644 index 00000000000..b21ee09588b --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const VariantAnalysisNoCodeqlDbRepos = () => { + return
This is the no database found view
; +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx new file mode 100644 index 00000000000..a35d7a09e6a --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const VariantAnalysisNotFoundRepos = () => { + return
This is the no access view
; +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx new file mode 100644 index 00000000000..296586a1fd2 --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react'; +import { formatDecimal } from '../../pure/number'; +import { VariantAnalysis } from '../../remote-queries/shared/variant-analysis'; +import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos'; +import { VariantAnalysisNotFoundRepos } from './VariantAnalysisNotFoundRepos'; +import { VariantAnalysisNoCodeqlDbRepos } from './VariantAnalysisNoCodeqlDbRepos'; +import { Alert } from '../common'; + +export type VariantAnalysisOutcomePanelProps = { + variantAnalysis: VariantAnalysis; +}; + +const Tab = styled(VSCodePanelTab)` + text-transform: uppercase; +`; + +const WarningsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1em; + + margin-top: 1em; + + > * { + // Add a margin to the last alert, independent of the number of alerts. This will not add a margin when + // there is no warning to ensure we do not have a margin-top AND a margin-bottom. + &:last-child { + margin-bottom: 1em; + } + } +`; + +export const VariantAnalysisOutcomePanels = ({ + variantAnalysis +}: VariantAnalysisOutcomePanelProps) => { + const noCodeqlDbRepositoryCount = variantAnalysis.skippedRepos?.noCodeqlDbRepos?.repositoryCount ?? 0; + const notFoundRepositoryCount = variantAnalysis.skippedRepos?.notFoundRepos?.repositoryCount ?? 0; + const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0; + const accessMismatchRepositoryCount = variantAnalysis.skippedRepos?.accessMismatchRepos?.repositoryCount ?? 0; + + const warnings = ( + + {overLimitRepositoryCount > 0 && ( + + )} + {accessMismatchRepositoryCount > 0 && ( + + )} + + ); + + if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) { + return ( + <> + {warnings} + + + ); + } + + return ( + <> + {warnings} + + + Analyzed + {formatDecimal(variantAnalysis.scannedRepos?.length ?? 0)} + + {notFoundRepositoryCount > 0 && ( + + No access + {formatDecimal(notFoundRepositoryCount)} + + )} + {noCodeqlDbRepositoryCount > 0 && ( + + No database + {formatDecimal(noCodeqlDbRepositoryCount)} + + )} + + {notFoundRepositoryCount > 0 && } + {noCodeqlDbRepositoryCount > 0 && } + + + ); +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx new file mode 100644 index 00000000000..51b0bd3d43b --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx @@ -0,0 +1,207 @@ +import * as React from 'react'; +import { render as reactRender, screen } from '@testing-library/react'; +import { + VariantAnalysis, + VariantAnalysisQueryLanguage, VariantAnalysisRepoStatus, + VariantAnalysisStatus +} from '../../../remote-queries/shared/variant-analysis'; +import { VariantAnalysisOutcomePanelProps, VariantAnalysisOutcomePanels } from '../VariantAnalysisOutcomePanels'; + +describe(VariantAnalysisOutcomePanels.name, () => { + const defaultVariantAnalysis = { + id: 1, + controllerRepoId: 1, + actionsWorkflowRunId: 789263, + query: { + name: 'Example query', + filePath: 'example.ql', + language: VariantAnalysisQueryLanguage.Javascript, + }, + databases: {}, + status: VariantAnalysisStatus.InProgress, + scannedRepos: [ + { + repository: { + id: 1, + fullName: 'octodemo/hello-world-1', + private: false, + }, + analysisStatus: VariantAnalysisRepoStatus.Pending, + }, + ], + skippedRepos: { + notFoundRepos: { + repositoryCount: 2, + repositories: [ + { + fullName: 'octodemo/hello-globe' + }, + { + fullName: 'octodemo/hello-planet' + } + ] + }, + noCodeqlDbRepos: { + repositoryCount: 4, + repositories: [ + { + id: 100, + fullName: 'octodemo/no-db-1' + }, + { + id: 101, + fullName: 'octodemo/no-db-2' + }, + { + id: 102, + fullName: 'octodemo/no-db-3' + }, + { + id: 103, + fullName: 'octodemo/no-db-4' + } + ] + }, + overLimitRepos: { + repositoryCount: 1, + repositories: [ + { + id: 201, + fullName: 'octodemo/over-limit-1' + } + ] + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: [ + { + id: 205, + fullName: 'octodemo/private' + } + ] + } + }, + }; + + const render = (variantAnalysis: Partial = {}, props: Partial = {}) => { + return reactRender( + + ); + }; + + it('renders correctly', () => { + render(); + + expect(screen.getByText('Analyzed')).toBeInTheDocument(); + }); + + it('does not render panels without skipped repos', () => { + render({ + skippedRepos: undefined, + }); + + expect(screen.queryByText('Analyzed')).not.toBeInTheDocument(); + expect(screen.queryByText('No access')).not.toBeInTheDocument(); + expect(screen.queryByText('No database')).not.toBeInTheDocument(); + }); + + it('renders panels with not found repos', () => { + render({ + skippedRepos: { + notFoundRepos: defaultVariantAnalysis.skippedRepos.notFoundRepos, + }, + }); + + expect(screen.getByText('Analyzed')).toBeInTheDocument(); + expect(screen.getByText('No access')).toBeInTheDocument(); + expect(screen.queryByText('No database')).not.toBeInTheDocument(); + }); + + it('renders panels with no database repos', () => { + render({ + skippedRepos: { + noCodeqlDbRepos: defaultVariantAnalysis.skippedRepos.noCodeqlDbRepos, + }, + }); + + expect(screen.getByText('Analyzed')).toBeInTheDocument(); + expect(screen.queryByText('No access')).not.toBeInTheDocument(); + expect(screen.getByText('No database')).toBeInTheDocument(); + }); + + it('renders panels with not found and no database repos', () => { + render({ + skippedRepos: { + notFoundRepos: defaultVariantAnalysis.skippedRepos.notFoundRepos, + noCodeqlDbRepos: defaultVariantAnalysis.skippedRepos.noCodeqlDbRepos, + }, + }); + + expect(screen.getByText('Analyzed')).toBeInTheDocument(); + expect(screen.getByText('No access')).toBeInTheDocument(); + expect(screen.getByText('No database')).toBeInTheDocument(); + }); + + it('renders warning with access mismatch repos', () => { + render({ + skippedRepos: { + notFoundRepos: defaultVariantAnalysis.skippedRepos.notFoundRepos, + accessMismatchRepos: defaultVariantAnalysis.skippedRepos.accessMismatchRepos, + }, + }); + + expect(screen.getByText('Warning: Access mismatch')).toBeInTheDocument(); + }); + + it('renders warning with over limit repos', () => { + render({ + skippedRepos: { + overLimitRepos: defaultVariantAnalysis.skippedRepos.overLimitRepos, + }, + }); + + expect(screen.getByText('Warning: Repository limit exceeded')).toBeInTheDocument(); + }); + + it('renders singulars in warnings', () => { + render({ + skippedRepos: { + overLimitRepos: { + repositoryCount: 1, + repositories: defaultVariantAnalysis.skippedRepos.overLimitRepos.repositories, + }, + accessMismatchRepos: { + repositoryCount: 1, + repositories: defaultVariantAnalysis.skippedRepos.overLimitRepos.repositories, + } + }, + }); + + expect(screen.getByText('The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. 1 repository was skipped.')).toBeInTheDocument(); + expect(screen.getByText('1 repository is private, while the controller repository is public. This repository was skipped.')).toBeInTheDocument(); + }); + + it('renders plurals in warnings', () => { + render({ + skippedRepos: { + overLimitRepos: { + repositoryCount: 2, + repositories: defaultVariantAnalysis.skippedRepos.overLimitRepos.repositories, + }, + accessMismatchRepos: { + repositoryCount: 2, + repositories: defaultVariantAnalysis.skippedRepos.overLimitRepos.repositories, + } + }, + }); + + expect(screen.getByText('The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. 2 repositories were skipped.')).toBeInTheDocument(); + expect(screen.getByText('2 repositories are private, while the controller repository is public. These repositories were skipped.')).toBeInTheDocument(); + }); +});