Implement skipped repositories tabs

This commit is contained in:
Robert
2022-09-27 12:26:33 +01:00
parent 110d930b68
commit c442ff5599
11 changed files with 462 additions and 30 deletions

View File

@@ -72,10 +72,13 @@ export interface VariantAnalysisSkippedRepositories {
export interface VariantAnalysisSkippedRepositoryGroup {
repositoryCount: number,
repositories: Array<{
id?: number,
fullName: string
}>
repositories: Array<VariantAnalysisSkippedRepository>,
}
export interface VariantAnalysisSkippedRepository {
id?: number,
fullName: string,
private?: boolean,
}
/**

View File

@@ -0,0 +1,102 @@
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 = {
reason: 'no_access',
skippedRepositoryGroup: {
repositoryCount: 2,
repositories: [
{
fullName: 'octodemo/hello-globe',
},
{
fullName: 'octodemo/hello-planet',
},
],
},
};
export const NoAccessWithOmissions = Template.bind({});
NoAccessWithOmissions.args = {
reason: 'no_access',
skippedRepositoryGroup: {
repositoryCount: 12345,
repositories: [
{
fullName: 'octodemo/hello-globe',
},
{
fullName: 'octodemo/hello-planet',
},
{
fullName: 'octodemo/hello-universe',
},
],
},
};
export const NoDatabaseNoOmissions = Template.bind({});
NoDatabaseNoOmissions.args = {
reason: 'no_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 = {
reason: 'no_database',
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,
},
],
},
};

View File

@@ -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,
}
};

View File

@@ -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,
}
]
},

View File

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

View File

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

View File

@@ -4,9 +4,8 @@ import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vsc
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';
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
export type VariantAnalysisOutcomePanelProps = {
variantAnalysis: VariantAnalysis;
@@ -35,8 +34,8 @@ const WarningsContainer = styled.div`
export const VariantAnalysisOutcomePanels = ({
variantAnalysis
}: 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;
@@ -59,7 +58,7 @@ export const VariantAnalysisOutcomePanels = ({
</WarningsContainer>
);
if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) {
if (!noCodeqlDbRepos?.repositoryCount && !notFoundRepos?.repositoryCount) {
return (
<>
{warnings}
@@ -76,21 +75,31 @@ 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 /></VSCodePanelView>
{notFoundRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNotFoundRepos /></VSCodePanelView>}
{noCodeqlDbRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNoCodeqlDbRepos /></VSCodePanelView>}
{notFoundRepos?.repositoryCount &&
<VSCodePanelView>
<VariantAnalysisSkippedRepositoriesTab
reason='no_access'
skippedRepositoryGroup={notFoundRepos} />
</VSCodePanelView>}
{noCodeqlDbRepos?.repositoryCount &&
<VSCodePanelView>
<VariantAnalysisSkippedRepositoriesTab
reason='no_database'
skippedRepositoryGroup={noCodeqlDbRepos} />
</VSCodePanelView>}
</VSCodePanels>
</>
);

View File

@@ -0,0 +1,71 @@
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 SkippedRepositoriesReason = 'no_access' | 'no_database';
export type VariantAnalysisSkippedRepositoriesTabProps = {
reason: SkippedRepositoriesReason,
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
};
function getSkipReasonAlertTitle(reason: SkippedRepositoriesReason): string {
switch (reason) {
case 'no_access':
return 'No access';
case 'no_database':
return 'No database';
}
}
function getSkipReasonAlertMessage(
reason: SkippedRepositoriesReason,
repos: VariantAnalysisSkippedRepositoryGroup
): string {
const repositoriesOmittedText = repos.repositoryCount > repos.repositories.length
? ` (Only the first ${repos.repositories.length} ${repos.repositories.length > 1 ? 'repositories are' : 'repository is'} shown.)`
: '';
switch (reason) {
case 'no_access':
return `The following repositories could not be scanned because you do not have read access.${repositoriesOmittedText}`;
case 'no_database':
return `The following repositories could not be scanned because they do not have an available CodeQL database.${repositoriesOmittedText}`;
}
}
function getSkipReasonAlert(
reason: SkippedRepositoriesReason,
repos: VariantAnalysisSkippedRepositoryGroup
) {
return (
<Alert
key='alert'
type='warning'
title={getSkipReasonAlertTitle(reason)}
message={getSkipReasonAlertMessage(reason, repos)}
/>
);
}
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 0.5em;
width: 100%;
`;
export const VariantAnalysisSkippedRepositoriesTab = ({
reason,
skippedRepositoryGroup,
}: VariantAnalysisSkippedRepositoriesTabProps) => {
return (
<Container>
{getSkipReasonAlert(reason, skippedRepositoryGroup)}
{skippedRepositoryGroup.repositories.map((repo) =>
<VariantAnalysisSkippedRepositoryRow key={`repo/${repo.fullName}`} repository={repo} />
)}
</Container>
);
};

View File

@@ -0,0 +1,48 @@
import { VSCodeBadge, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
import * as React from 'react';
import styled from 'styled-components';
import { 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.span`
color: var(--vscode-descriptionForeground);
`;
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 className="codicon codicon-chevron-right" />
<VSCodeBadge>-</VSCodeBadge>
<span>{repository.fullName}</span>
{getPrivacyElement(repository.private)}
<WarningIcon />
</Row>
);
};

View File

@@ -0,0 +1,107 @@
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 when reason is no_access', async () => {
render({
reason: 'no_access',
skippedRepositoryGroup: {
repositoryCount: 1,
repositories: [],
}
});
expect(screen.getByText('Warning: No access')).toBeInTheDocument();
});
it('renders warning title when reason is no_database', async () => {
render({
reason: 'no_database',
skippedRepositoryGroup: {
repositoryCount: 1,
repositories: [],
}
});
expect(screen.getByText('Warning: No database')).toBeInTheDocument();
});
it('renders warning message when no repositories are omitted', async () => {
render({
reason: 'no_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({
reason: 'no_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 1 repository is shown.)')).toBeInTheDocument();
});
it('renders warning message when there are repositories omitted and multiple shown', async () => {
render({
reason: 'no_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({
reason: 'no_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();
});
});

View File

@@ -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();
});
});