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')}
+ />
+
+ >
);
}
diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx
new file mode 100644
index 00000000000..b068bcb831e
--- /dev/null
+++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisAnalyzedRepos.tsx
@@ -0,0 +1,5 @@
+import * as React from 'react';
+
+export const VariantAnalysisAnalyzedRepos = () => {
+ return This is the analyzed view
;
+};
diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx
new file mode 100644
index 00000000000..b21ee09588b
--- /dev/null
+++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNoCodeqlDbRepos.tsx
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 00000000000..a35d7a09e6a
--- /dev/null
+++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisNotFoundRepos.tsx
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 00000000000..296586a1fd2
--- /dev/null
+++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisOutcomePanels.tsx
@@ -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 = (
+
+ {overLimitRepositoryCount > 0 && (
+
+ )}
+ {accessMismatchRepositoryCount > 0 && (
+
+ )}
+
+ );
+
+ if (noCodeqlDbRepositoryCount === 0 && notFoundRepositoryCount === 0) {
+ return (
+ <>
+ {warnings}
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {warnings}
+
+
+ Analyzed
+ {formatDecimal(variantAnalysis.scannedRepos?.length ?? 0)}
+
+ {notFoundRepositoryCount > 0 && (
+
+ No access
+ {formatDecimal(notFoundRepositoryCount)}
+
+ )}
+ {noCodeqlDbRepositoryCount > 0 && (
+
+ No database
+ {formatDecimal(noCodeqlDbRepositoryCount)}
+
+ )}
+
+ {notFoundRepositoryCount > 0 && }
+ {noCodeqlDbRepositoryCount > 0 && }
+
+ >
+ );
+};
diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx
new file mode 100644
index 00000000000..51b0bd3d43b
--- /dev/null
+++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisOutcomePanels.spec.tsx
@@ -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 = {}, props: Partial = {}) => {
+ return reactRender(
+
+ );
+ };
+
+ 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();
+ });
+});