Merge pull request #1549 from github/robertbrignull/skipped-repos
Implement skipped repositories tabs
This commit is contained in:
@@ -73,10 +73,13 @@ export interface VariantAnalysisSkippedRepositories {
|
||||
|
||||
export interface VariantAnalysisSkippedRepositoryGroup {
|
||||
repositoryCount: number,
|
||||
repositories: Array<{
|
||||
id?: number,
|
||||
fullName: string
|
||||
}>
|
||||
repositories: VariantAnalysisSkippedRepository[],
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepository {
|
||||
id?: number,
|
||||
fullName: string,
|
||||
private?: boolean,
|
||||
}
|
||||
|
||||
export interface VariantAnalysisScannedRepositoryResult {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from '../../view/variant-analysis/VariantAnalysisSkippedRepositoriesTab';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Skipped Repositories Tab',
|
||||
component: VariantAnalysisSkippedRepositoriesTab,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
} as ComponentMeta<typeof VariantAnalysisSkippedRepositoriesTab>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisSkippedRepositoriesTab> = (args) => (
|
||||
<VariantAnalysisSkippedRepositoriesTab {...args} />
|
||||
);
|
||||
|
||||
export const NoAccessNoOmissions = Template.bind({});
|
||||
NoAccessNoOmissions.args = {
|
||||
alertTitle: 'No access',
|
||||
alertMessage: 'The following repositories could not be scanned because you do not have read access.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 2,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-globe',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-planet',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAccessWithOmissions = Template.bind({});
|
||||
NoAccessWithOmissions.args = {
|
||||
...NoAccessNoOmissions.args,
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 12345,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-globe',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-planet',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoDatabaseNoOmissions = Template.bind({});
|
||||
NoDatabaseNoOmissions.args = {
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 2,
|
||||
repositories: [
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-globe',
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-planet',
|
||||
private: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoDatabaseWithOmissions = Template.bind({});
|
||||
NoDatabaseWithOmissions.args = {
|
||||
...NoDatabaseNoOmissions.args,
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 12345,
|
||||
repositories: [
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-globe',
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-planet',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-universe',
|
||||
private: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisSkippedRepositoryRow } from '../../view/variant-analysis/VariantAnalysisSkippedRepositoryRow';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Skipped Repository',
|
||||
component: VariantAnalysisSkippedRepositoryRow,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<VariantAnalysisContainer>
|
||||
<Story />
|
||||
</VariantAnalysisContainer>
|
||||
)
|
||||
],
|
||||
} as ComponentMeta<typeof VariantAnalysisSkippedRepositoryRow>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisSkippedRepositoryRow> = (args) => (
|
||||
<VariantAnalysisSkippedRepositoryRow {...args} />
|
||||
);
|
||||
|
||||
export const OnlyFullName = Template.bind({});
|
||||
OnlyFullName.args = {
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-globe',
|
||||
}
|
||||
};
|
||||
|
||||
export const Public = Template.bind({});
|
||||
Public.args = {
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-globe',
|
||||
private: false,
|
||||
}
|
||||
};
|
||||
|
||||
export const Private = Template.bind({});
|
||||
Private.args = {
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-globe',
|
||||
private: true,
|
||||
}
|
||||
};
|
||||
@@ -106,7 +106,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
],
|
||||
skippedRepos: {
|
||||
notFoundRepos: {
|
||||
repositoryCount: 2,
|
||||
repositoryCount: 9999,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-globe'
|
||||
@@ -121,19 +121,23 @@ const variantAnalysis: VariantAnalysisDomainModel = {
|
||||
repositories: [
|
||||
{
|
||||
id: 100,
|
||||
fullName: 'octodemo/no-db-1'
|
||||
fullName: 'octodemo/no-db-1',
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: 101,
|
||||
fullName: 'octodemo/no-db-2'
|
||||
fullName: 'octodemo/no-db-2',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
fullName: 'octodemo/no-db-3'
|
||||
fullName: 'octodemo/no-db-3',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
fullName: 'octodemo/no-db-4'
|
||||
fullName: 'octodemo/no-db-4',
|
||||
private: false,
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const VariantAnalysisNoCodeqlDbRepos = () => {
|
||||
return <div>This is the no database found view</div>;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const VariantAnalysisNotFoundRepos = () => {
|
||||
return <div>This is the no access view</div>;
|
||||
};
|
||||
@@ -4,9 +4,8 @@ import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vsc
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from '../../remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { VariantAnalysisNotFoundRepos } from './VariantAnalysisNotFoundRepos';
|
||||
import { VariantAnalysisNoCodeqlDbRepos } from './VariantAnalysisNoCodeqlDbRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
|
||||
export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
@@ -37,8 +36,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
variantAnalysis,
|
||||
repositoryResults,
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const noCodeqlDbRepositoryCount = variantAnalysis.skippedRepos?.noCodeqlDbRepos?.repositoryCount ?? 0;
|
||||
const notFoundRepositoryCount = variantAnalysis.skippedRepos?.notFoundRepos?.repositoryCount ?? 0;
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
|
||||
const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0;
|
||||
const accessMismatchRepositoryCount = variantAnalysis.skippedRepos?.accessMismatchRepos?.repositoryCount ?? 0;
|
||||
|
||||
@@ -61,7 +60,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
</WarningsContainer>
|
||||
);
|
||||
|
||||
if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) {
|
||||
if (!noCodeqlDbRepos?.repositoryCount && !notFoundRepos?.repositoryCount) {
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
@@ -78,21 +77,33 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
Analyzed
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(variantAnalysis.scannedRepos?.length ?? 0)}</VSCodeBadge>
|
||||
</Tab>
|
||||
{notFoundRepositoryCount > 0 && (
|
||||
{notFoundRepos?.repositoryCount && (
|
||||
<Tab>
|
||||
No access
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(notFoundRepositoryCount)}</VSCodeBadge>
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(notFoundRepos.repositoryCount)}</VSCodeBadge>
|
||||
</Tab>
|
||||
)}
|
||||
{noCodeqlDbRepositoryCount > 0 && (
|
||||
{noCodeqlDbRepos?.repositoryCount && (
|
||||
<Tab>
|
||||
No database
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(noCodeqlDbRepositoryCount)}</VSCodeBadge>
|
||||
<VSCodeBadge appearance="secondary">{formatDecimal(noCodeqlDbRepos.repositoryCount)}</VSCodeBadge>
|
||||
</Tab>
|
||||
)}
|
||||
<VSCodePanelView><VariantAnalysisAnalyzedRepos variantAnalysis={variantAnalysis} repositoryResults={repositoryResults} /></VSCodePanelView>
|
||||
{notFoundRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNotFoundRepos /></VSCodePanelView>}
|
||||
{noCodeqlDbRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNoCodeqlDbRepos /></VSCodePanelView>}
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No access'
|
||||
alertMessage='The following repositories could not be scanned because you do not have read access.'
|
||||
skippedRepositoryGroup={notFoundRepos} />
|
||||
</VSCodePanelView>}
|
||||
{noCodeqlDbRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No database'
|
||||
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
|
||||
skippedRepositoryGroup={noCodeqlDbRepos} />
|
||||
</VSCodePanelView>}
|
||||
</VSCodePanels>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoryRow } from './VariantAnalysisSkippedRepositoryRow';
|
||||
|
||||
export type VariantAnalysisSkippedRepositoriesTabProps = {
|
||||
alertTitle: string,
|
||||
alertMessage: string,
|
||||
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
|
||||
};
|
||||
|
||||
function getSkipReasonAlert(
|
||||
title: string,
|
||||
message: string,
|
||||
repos: VariantAnalysisSkippedRepositoryGroup
|
||||
) {
|
||||
const repositoriesOmittedText = repos.repositoryCount > repos.repositories.length
|
||||
? ` (Only the first ${repos.repositories.length > 1 ? `${repos.repositories.length} repositories are` : 'repository is'} shown.)`
|
||||
: '';
|
||||
return (
|
||||
<Alert
|
||||
key='alert'
|
||||
type='warning'
|
||||
title={title}
|
||||
message={message + repositoriesOmittedText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const VariantAnalysisSkippedRepositoriesTab = ({
|
||||
alertTitle,
|
||||
alertMessage,
|
||||
skippedRepositoryGroup,
|
||||
}: VariantAnalysisSkippedRepositoriesTabProps) => {
|
||||
return (
|
||||
<Container>
|
||||
{getSkipReasonAlert(alertTitle, alertMessage, skippedRepositoryGroup)}
|
||||
{skippedRepositoryGroup.repositories.map((repo) =>
|
||||
<VariantAnalysisSkippedRepositoryRow key={`repo/${repo.fullName}`} repository={repo} />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { VSCodeBadge, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Codicon, WarningIcon } from '../common';
|
||||
import { VariantAnalysisSkippedRepository as SkippedRepo } from '../../remote-queries/shared/variant-analysis';
|
||||
|
||||
export type VariantAnalysisSkippedRepositoryRowProps = {
|
||||
repository: SkippedRepo,
|
||||
};
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ChevronIcon = styled(Codicon)`
|
||||
color: var(--vscode-disabledForeground);
|
||||
`;
|
||||
|
||||
const PrivacyText = styled.span`
|
||||
font-size: small;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
`;
|
||||
|
||||
function getPrivacyElement(isPrivate: boolean | undefined) {
|
||||
if (isPrivate === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const text = isPrivate ? 'private' : 'public';
|
||||
return <PrivacyText>{text}</PrivacyText>;
|
||||
}
|
||||
|
||||
export const VariantAnalysisSkippedRepositoryRow = ({
|
||||
repository,
|
||||
}: VariantAnalysisSkippedRepositoryRowProps) => {
|
||||
return (
|
||||
<Row>
|
||||
<VSCodeCheckbox />
|
||||
<ChevronIcon name='chevron-right' label='Expand' />
|
||||
<VSCodeBadge>-</VSCodeBadge>
|
||||
<span>{repository.fullName}</span>
|
||||
{getPrivacyElement(repository.private)}
|
||||
<WarningIcon />
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisSkippedRepositoriesTab, VariantAnalysisSkippedRepositoriesTabProps } from '../VariantAnalysisSkippedRepositoriesTab';
|
||||
|
||||
describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
const render = (props: VariantAnalysisSkippedRepositoriesTabProps) =>
|
||||
reactRender(<VariantAnalysisSkippedRepositoriesTab {...props} />);
|
||||
|
||||
it('renders warning title', async () => {
|
||||
render({
|
||||
alertTitle: 'No access',
|
||||
alertMessage: 'The following repositories could not be scanned because you do not have read access.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [],
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('Warning: No access')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning message when no repositories are omitted', async () => {
|
||||
render({
|
||||
alertTitle: 'No access',
|
||||
alertMessage: 'The following repositories could not be scanned because you do not have read access.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('The following repositories could not be scanned because you do not have read access.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning message when there are repositories omitted and only one shown', async () => {
|
||||
render({
|
||||
alertTitle: 'No access',
|
||||
alertMessage: 'The following repositories could not be scanned because you do not have read access.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 44,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('The following repositories could not be scanned because you do not have read access. (Only the first repository is shown.)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders warning message when there are repositories omitted and multiple shown', async () => {
|
||||
render({
|
||||
alertTitle: 'No access',
|
||||
alertMessage: 'The following repositories could not be scanned because you do not have read access.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 44,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('The following repositories could not be scanned because you do not have read access. (Only the first 2 repositories are shown.)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple skipped repository rows', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
|
||||
expect(screen.getByText('octodemo/hello-galaxy')).toBeInTheDocument();
|
||||
expect(screen.getByText('octodemo/hello-universe')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisSkippedRepositoryRow, VariantAnalysisSkippedRepositoryRowProps } from '../VariantAnalysisSkippedRepositoryRow';
|
||||
|
||||
describe(VariantAnalysisSkippedRepositoryRow.name, () => {
|
||||
const render = (props: VariantAnalysisSkippedRepositoryRowProps) =>
|
||||
reactRender(<VariantAnalysisSkippedRepositoryRow {...props} />);
|
||||
|
||||
it('shows repository name', async () => {
|
||||
render({
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-world',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows visibility when public', async () => {
|
||||
render({
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-world',
|
||||
private: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('public')).toBeInTheDocument();
|
||||
expect(screen.queryByText('private')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows visibility when private', async () => {
|
||||
render({
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-world',
|
||||
private: true,
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.queryByText('public')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('private')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show visibility when unknown', async () => {
|
||||
render({
|
||||
repository: {
|
||||
fullName: 'octodemo/hello-world',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.queryByText('public')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('private')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user