diff --git a/extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts b/extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts index 583ecc78ae3..4103a10042c 100644 --- a/extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts +++ b/extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts @@ -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 { diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoriesTab.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoriesTab.stories.tsx new file mode 100644 index 00000000000..7cbf93f4c3b --- /dev/null +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoriesTab.stories.tsx @@ -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) => ( + + + + ) + ], +} as ComponentMeta; + +const Template: ComponentStory = (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, + }, + ], + }, +}; diff --git a/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoryRow.stories.tsx b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoryRow.stories.tsx new file mode 100644 index 00000000000..7f33d53cdad --- /dev/null +++ b/extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisSkippedRepositoryRow.stories.tsx @@ -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) => ( + + + + ) + ], +} as ComponentMeta; + +const Template: ComponentStory = (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, + } +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx index e1f4856230a..711adb3260f 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx @@ -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, } ] }, diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx deleted file mode 100644 index b21ee09588b..00000000000 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index a35d7a09e6a..00000000000 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx +++ /dev/null @@ -1,5 +0,0 @@ -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 index 2a780929d9c..2ccd440e74c 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx @@ -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 = ({ ); - if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) { + if (!noCodeqlDbRepos?.repositoryCount && !notFoundRepos?.repositoryCount) { return ( <> {warnings} @@ -78,21 +77,33 @@ export const VariantAnalysisOutcomePanels = ({ Analyzed {formatDecimal(variantAnalysis.scannedRepos?.length ?? 0)} - {notFoundRepositoryCount > 0 && ( + {notFoundRepos?.repositoryCount && ( No access - {formatDecimal(notFoundRepositoryCount)} + {formatDecimal(notFoundRepos.repositoryCount)} )} - {noCodeqlDbRepositoryCount > 0 && ( + {noCodeqlDbRepos?.repositoryCount && ( No database - {formatDecimal(noCodeqlDbRepositoryCount)} + {formatDecimal(noCodeqlDbRepos.repositoryCount)} )} - {notFoundRepositoryCount > 0 && } - {noCodeqlDbRepositoryCount > 0 && } + {notFoundRepos?.repositoryCount && + + + } + {noCodeqlDbRepos?.repositoryCount && + + + } ); diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoriesTab.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoriesTab.tsx new file mode 100644 index 00000000000..381cdbaef93 --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoriesTab.tsx @@ -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 ( + + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 0.5em; + width: 100%; +`; + +export const VariantAnalysisSkippedRepositoriesTab = ({ + alertTitle, + alertMessage, + skippedRepositoryGroup, +}: VariantAnalysisSkippedRepositoriesTabProps) => { + return ( + + {getSkipReasonAlert(alertTitle, alertMessage, skippedRepositoryGroup)} + {skippedRepositoryGroup.repositories.map((repo) => + + )} + + ); +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoryRow.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoryRow.tsx new file mode 100644 index 00000000000..deb8d9d06df --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisSkippedRepositoryRow.tsx @@ -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 {text}; +} + +export const VariantAnalysisSkippedRepositoryRow = ({ + repository, +}: VariantAnalysisSkippedRepositoryRowProps) => { + return ( + + + + - + {repository.fullName} + {getPrivacyElement(repository.private)} + + + ); +}; diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoriesTab.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoriesTab.spec.tsx new file mode 100644 index 00000000000..38558db51ef --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoriesTab.spec.tsx @@ -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(); + + 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(); + }); +}); diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoryRow.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoryRow.spec.tsx new file mode 100644 index 00000000000..4e108fac884 --- /dev/null +++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisSkippedRepositoryRow.spec.tsx @@ -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(); + + 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(); + }); +});