Merge pull request #1543 from github/koesie10/outcome-panel
Add outcome panels
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}),
|
||||
};
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisContainer } from './VariantAnalysisContainer';
|
||||
import { VariantAnalysisHeader } from './VariantAnalysisHeader';
|
||||
import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels';
|
||||
import { VariantAnalysisLoading } from './VariantAnalysisLoading';
|
||||
|
||||
const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
id: 1,
|
||||
controllerRepoId: 1,
|
||||
actionsWorkflowRunId: 789263,
|
||||
query: {
|
||||
name: 'Example query',
|
||||
filePath: 'example.ql',
|
||||
@@ -20,7 +22,6 @@ const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
},
|
||||
databases: {},
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
actionsWorkflowRunId: 123,
|
||||
scannedRepos: [
|
||||
{
|
||||
repository: {
|
||||
@@ -102,7 +103,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'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
|
||||
@@ -111,15 +164,18 @@ function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
|
||||
}
|
||||
|
||||
return (
|
||||
<VariantAnalysisHeader
|
||||
variantAnalysis={variantAnalysis}
|
||||
onOpenQueryFileClick={() => console.log('Open query')}
|
||||
onViewQueryTextClick={() => console.log('View query')}
|
||||
onStopQueryClick={() => console.log('Stop query')}
|
||||
onCopyRepositoryListClick={() => console.log('Copy repository list')}
|
||||
onExportResultsClick={() => console.log('Export results')}
|
||||
onViewLogsClick={() => console.log('View logs')}
|
||||
/>
|
||||
<>
|
||||
<VariantAnalysisHeader
|
||||
variantAnalysis={variantAnalysis}
|
||||
onOpenQueryFileClick={() => console.log('Open query')}
|
||||
onViewQueryTextClick={() => console.log('View query')}
|
||||
onStopQueryClick={() => console.log('Stop query')}
|
||||
onCopyRepositoryListClick={() => console.log('Copy repository list')}
|
||||
onExportResultsClick={() => console.log('Export results')}
|
||||
onViewLogsClick={() => console.log('View logs')}
|
||||
/>
|
||||
<VariantAnalysisOutcomePanels variantAnalysis={variantAnalysis} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const VariantAnalysisAnalyzedRepos = () => {
|
||||
return <div>This is the analyzed view</div>;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const VariantAnalysisNoCodeqlDbRepos = () => {
|
||||
return <div>This is the no database found view</div>;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const VariantAnalysisNotFoundRepos = () => {
|
||||
return <div>This is the no access view</div>;
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user