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.
This commit is contained in:
Koen Vlaswinkel
2022-09-26 14:02:57 +02:00
parent 43bcd69e39
commit d3701944bf
7 changed files with 526 additions and 1 deletions

View File

@@ -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) => (
<VariantAnalysisContainer>
<Story />
</VariantAnalysisContainer>
)
],
} as ComponentMeta<typeof VariantAnalysisOutcomePanels>;
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => (
<VariantAnalysisOutcomePanels {...args} />
);
const buildVariantAnalysis = (data: Partial<VariantAnalysis>) => ({
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>): 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,
}
}),
};

View File

@@ -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')}
/>
<VariantAnalysisOutcomePanels variantAnalysis={variantAnalysis} />
</VariantAnalysisContainer>
);
}

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
export const VariantAnalysisAnalyzedRepos = () => {
return <div>This is the analyzed view</div>;
};

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
export const VariantAnalysisNoCodeqlDbRepos = () => {
return <div>This is the no database found view</div>;
};

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
export const VariantAnalysisNotFoundRepos = () => {
return <div>This is the no access view</div>;
};

View File

@@ -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 = (
<WarningsContainer>
{overLimitRepositoryCount > 0 && (
<Alert
type="warning"
title="Repository limit exceeded"
message={`The number of requested repositories exceeds the maximum number of repositories supported by multi-repository variant analysis. ${overLimitRepositoryCount} ${overLimitRepositoryCount === 1 ? 'repository was' : 'repositories were'} skipped.`}
/>
)}
{accessMismatchRepositoryCount > 0 && (
<Alert
type="warning"
title="Access mismatch"
message={`${accessMismatchRepositoryCount} ${accessMismatchRepositoryCount === 1 ? 'repository is' : 'repositories are'} private, while the controller repository is public. ${accessMismatchRepositoryCount === 1 ? 'This repository was' : 'These repositories were'} skipped.`}
/>
)}
</WarningsContainer>
);
if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) {
return (
<>
{warnings}
<VariantAnalysisAnalyzedRepos />
</>
);
}
return (
<>
{warnings}
<VSCodePanels>
<Tab>
Analyzed
<VSCodeBadge appearance="secondary">{formatDecimal(variantAnalysis.scannedRepos?.length ?? 0)}</VSCodeBadge>
</Tab>
{notFoundRepositoryCount > 0 && (
<Tab>
No access
<VSCodeBadge appearance="secondary">{formatDecimal(notFoundRepositoryCount)}</VSCodeBadge>
</Tab>
)}
{noCodeqlDbRepositoryCount > 0 && (
<Tab>
No database
<VSCodeBadge appearance="secondary">{formatDecimal(noCodeqlDbRepositoryCount)}</VSCodeBadge>
</Tab>
)}
<VSCodePanelView><VariantAnalysisAnalyzedRepos /></VSCodePanelView>
{notFoundRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNotFoundRepos /></VSCodePanelView>}
{noCodeqlDbRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNoCodeqlDbRepos /></VSCodePanelView>}
</VSCodePanels>
</>
);
};

View File

@@ -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<VariantAnalysis> = {}, props: Partial<VariantAnalysisOutcomePanelProps> = {}) => {
return reactRender(
<VariantAnalysisOutcomePanels
variantAnalysis={{
...defaultVariantAnalysis,
...variantAnalysis,
}}
{...props}
/>
);
};
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();
});
});