Merge branch 'koesie10/selected-copy' into koesie10/export-results-sorting-filtering
This commit is contained in:
@@ -2,4 +2,7 @@ import { AppEventEmitter } from './events';
|
||||
|
||||
export interface App {
|
||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||
extensionPath: string;
|
||||
globalStoragePath: string;
|
||||
workspaceStoragePath?: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { App } from '../app';
|
||||
import { AppEventEmitter } from '../events';
|
||||
import { VSCodeAppEventEmitter } from './events';
|
||||
|
||||
export class ExtensionApp implements App {
|
||||
public constructor(
|
||||
public readonly extensionContext: vscode.ExtensionContext
|
||||
) {
|
||||
}
|
||||
|
||||
public get extensionPath(): string {
|
||||
return this.extensionContext.extensionPath;
|
||||
}
|
||||
|
||||
public get globalStoragePath(): string {
|
||||
return this.extensionContext.globalStorageUri.fsPath;
|
||||
}
|
||||
|
||||
public get workspaceStoragePath(): string | undefined {
|
||||
return this.extensionContext.storageUri?.fsPath;
|
||||
}
|
||||
|
||||
public createEventEmitter<T>(): AppEventEmitter<T> {
|
||||
return new VSCodeAppEventEmitter<T>();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as chokidar from 'chokidar';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { DbConfigValidator } from './db-config-validator';
|
||||
import { ValueResult } from '../common/value-result';
|
||||
import { App } from '../common/app';
|
||||
|
||||
export class DbConfigStore extends DisposableObject {
|
||||
private readonly configPath: string;
|
||||
@@ -14,17 +15,16 @@ export class DbConfigStore extends DisposableObject {
|
||||
private configErrors: string[];
|
||||
private configWatcher: chokidar.FSWatcher | undefined;
|
||||
|
||||
public constructor(
|
||||
workspaceStoragePath: string,
|
||||
extensionPath: string) {
|
||||
public constructor(app: App) {
|
||||
super();
|
||||
|
||||
this.configPath = path.join(workspaceStoragePath, 'workspace-databases.json');
|
||||
const storagePath = app.workspaceStoragePath || app.globalStoragePath;
|
||||
this.configPath = path.join(storagePath, 'workspace-databases.json');
|
||||
|
||||
this.config = this.createEmptyConfig();
|
||||
this.configErrors = [];
|
||||
this.configWatcher = undefined;
|
||||
this.configValidator = new DbConfigValidator(extensionPath);
|
||||
this.configValidator = new DbConfigValidator(app.extensionPath);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
@@ -99,7 +99,11 @@ export class DbConfigStore extends DisposableObject {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: [],
|
||||
}
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
export interface DbConfig {
|
||||
remote: RemoteDbConfig;
|
||||
local: LocalDbConfig;
|
||||
}
|
||||
|
||||
export interface RemoteDbConfig {
|
||||
@@ -15,6 +16,23 @@ export interface RemoteRepositoryList {
|
||||
repositories: string[];
|
||||
}
|
||||
|
||||
export interface LocalDbConfig {
|
||||
lists: LocalList[];
|
||||
databases: LocalDatabase[];
|
||||
}
|
||||
|
||||
export interface LocalList {
|
||||
name: string;
|
||||
databases: LocalDatabase[];
|
||||
}
|
||||
|
||||
export interface LocalDatabase {
|
||||
name: string;
|
||||
dateAdded: number;
|
||||
language: string;
|
||||
storagePath: string;
|
||||
}
|
||||
|
||||
export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
return {
|
||||
remote: {
|
||||
@@ -24,6 +42,13 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||
})),
|
||||
owners: [...config.remote.owners],
|
||||
repositories: [...config.remote.repositories],
|
||||
}
|
||||
},
|
||||
local: {
|
||||
lists: config.local.lists.map((list) => ({
|
||||
name: list.name,
|
||||
databases: list.databases.map((db) => ({ ...db })),
|
||||
})),
|
||||
databases: config.local.databases.map((db) => ({ ...db })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ExtensionApp } from '../common/vscode/vscode-app';
|
||||
import { isCanary, isNewQueryRunExperienceEnabled } from '../config';
|
||||
import { logger } from '../logging';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
@@ -21,9 +22,9 @@ export class DbModule extends DisposableObject {
|
||||
|
||||
void logger.log('Initializing database module');
|
||||
|
||||
const storagePath = extensionContext.storageUri?.fsPath || extensionContext.globalStorageUri.fsPath;
|
||||
const extensionPath = extensionContext.extensionPath;
|
||||
const dbConfigStore = new DbConfigStore(storagePath, extensionPath);
|
||||
const app = new ExtensionApp(extensionContext);
|
||||
|
||||
const dbConfigStore = new DbConfigStore(app);
|
||||
await dbConfigStore.initialize();
|
||||
|
||||
const dbManager = new DbManager(dbConfigStore);
|
||||
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
*/
|
||||
export class DbTreeViewItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
// iconPath and tooltip must have those names because
|
||||
// they are part of the vscode.TreeItem interface
|
||||
|
||||
public readonly dbItem: DbItem | undefined,
|
||||
public readonly icon: vscode.ThemeIcon | undefined,
|
||||
public readonly iconPath: vscode.ThemeIcon | undefined,
|
||||
public readonly label: string,
|
||||
public readonly tooltip: string | undefined,
|
||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
||||
|
||||
@@ -120,6 +120,7 @@ import { createVariantAnalysisContentProvider } from './remote-queries/variant-a
|
||||
import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server';
|
||||
import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager';
|
||||
import { initializeDbModule } from './databases/db-module';
|
||||
import { RepositoriesFilterSortStateWithIds } from './pure/variant-analysis-filter-sort';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -949,8 +950,8 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId);
|
||||
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number, filterSort?: RepositoriesFilterSortStateWithIds) => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId, filterSort);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from '../remote-queries/shared/variant-analysis';
|
||||
import { RepositoriesFilterSortStateWithIds } from './variant-analysis-filter-sort';
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -474,6 +475,7 @@ export interface OpenQueryTextMessage {
|
||||
|
||||
export interface CopyRepositoryListMessage {
|
||||
t: 'copyRepositoryList';
|
||||
filterSort?: RepositoriesFilterSortStateWithIds;
|
||||
}
|
||||
|
||||
export interface ExportResultsMessage {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Repository, RepositoryWithMetadata } from '../../remote-queries/shared/repository';
|
||||
import { parseDate } from '../../pure/date';
|
||||
import { Repository, RepositoryWithMetadata } from '../remote-queries/shared/repository';
|
||||
import { parseDate } from './date';
|
||||
|
||||
export enum SortKey {
|
||||
Name = 'name',
|
||||
@@ -13,6 +13,10 @@ export type RepositoriesFilterSortState = {
|
||||
sortKey: SortKey;
|
||||
}
|
||||
|
||||
export type RepositoriesFilterSortStateWithIds = RepositoriesFilterSortState & {
|
||||
repositoryIds?: number[];
|
||||
}
|
||||
|
||||
export const defaultFilterSortState: RepositoriesFilterSortState = {
|
||||
searchValue: '',
|
||||
sortKey: SortKey.Name,
|
||||
@@ -52,7 +56,7 @@ export function compareRepository(filterSortState: RepositoriesFilterSortState |
|
||||
}
|
||||
|
||||
type SortableResult = {
|
||||
repository: SortableRepository;
|
||||
repository: SortableRepository & Pick<Repository, 'id'>;
|
||||
resultCount?: number;
|
||||
}
|
||||
|
||||
@@ -71,3 +75,31 @@ export function compareWithResults(filterSortState: RepositoriesFilterSortState
|
||||
return fallbackSort(left.repository, right.repository);
|
||||
};
|
||||
}
|
||||
|
||||
function hasRepositoryIds(filterSortState: RepositoriesFilterSortState | RepositoriesFilterSortStateWithIds | undefined): filterSortState is RepositoriesFilterSortStateWithIds {
|
||||
if (!filterSortState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'repositoryIds' in filterSortState;
|
||||
}
|
||||
|
||||
function isFilterOnRepositoryIds(filterSortState: RepositoriesFilterSortState | RepositoriesFilterSortStateWithIds | undefined): filterSortState is RepositoriesFilterSortStateWithIds & Required<Pick<RepositoriesFilterSortStateWithIds, 'repositoryIds'>> {
|
||||
return hasRepositoryIds(filterSortState) && filterSortState.repositoryIds !== undefined && filterSortState.repositoryIds.length > 0;
|
||||
}
|
||||
|
||||
// These define the behavior for undefined input values
|
||||
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[], filterSortState: RepositoriesFilterSortState | RepositoriesFilterSortStateWithIds | undefined): T[];
|
||||
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[] | undefined, filterSortState: RepositoriesFilterSortState | RepositoriesFilterSortStateWithIds | undefined): T[] | undefined;
|
||||
|
||||
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(repositories: T[] | undefined, filterSortState: RepositoriesFilterSortState | RepositoriesFilterSortStateWithIds | undefined): T[] | undefined {
|
||||
if (!repositories) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filteredRepositories = isFilterOnRepositoryIds(filterSortState) ?
|
||||
repositories.filter(repo => filterSortState.repositoryIds.includes(repo.repository.id)) :
|
||||
repositories.filter(repo => matchesFilter(repo.repository, filterSortState));
|
||||
|
||||
return filteredRepositories.sort(compareWithResults(filterSortState));
|
||||
}
|
||||
@@ -32,6 +32,11 @@ import * as os from 'os';
|
||||
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
|
||||
import { ProgressCallback, UserCancellationException } from '../commandRunner';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
filterAndSortRepositoriesWithResults,
|
||||
RepositoriesFilterSortStateWithIds,
|
||||
} from '../pure/variant-analysis-filter-sort';
|
||||
|
||||
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
|
||||
private static readonly REPO_STATES_FILENAME = 'repo_states.json';
|
||||
@@ -389,13 +394,15 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
await cancelVariantAnalysis(credentials, variantAnalysis);
|
||||
}
|
||||
|
||||
public async copyRepoListToClipboard(variantAnalysisId: number) {
|
||||
public async copyRepoListToClipboard(variantAnalysisId: number, filterSort: RepositoriesFilterSortStateWithIds = defaultFilterSortState) {
|
||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||
}
|
||||
|
||||
const fullNames = variantAnalysis.scannedRepos?.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
|
||||
const filteredRepositories = filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos ?? [], filterSort);
|
||||
|
||||
const fullNames = filteredRepositories.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
|
||||
if (!fullNames || fullNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
||||
await this.openQueryText();
|
||||
break;
|
||||
case 'copyRepositoryList':
|
||||
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId);
|
||||
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId, msg.filterSort);
|
||||
break;
|
||||
case 'exportResults':
|
||||
void commands.executeCommand('codeQL.exportVariantAnalysisResults', this.variantAnalysisId);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSearchSortRow as RepositoriesSearchSortRowComponent } from '../../view/variant-analysis/RepositoriesSearchSortRow';
|
||||
import { defaultFilterSortState } from '../../view/variant-analysis/filterSort';
|
||||
import { defaultFilterSortState } from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Search and Sort Row',
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSort as RepositoriesSortComponent } from '../../view/variant-analysis/RepositoriesSort';
|
||||
import { SortKey } from '../../view/variant-analysis/filterSort';
|
||||
import { SortKey } from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Sort',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { VariantAnalysisRepoStatus, VariantAnalysisStatus } from '../../remote-q
|
||||
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Variant Analysis Outcome Panels',
|
||||
@@ -21,9 +22,13 @@ export default {
|
||||
],
|
||||
} as ComponentMeta<typeof VariantAnalysisOutcomePanels>;
|
||||
|
||||
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => (
|
||||
<VariantAnalysisOutcomePanels {...args} />
|
||||
);
|
||||
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (args) => {
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
return (
|
||||
<VariantAnalysisOutcomePanels {...args} filterSortState={filterSortState} setFilterSortState={setFilterSortState} />
|
||||
);
|
||||
};
|
||||
|
||||
export const WithoutSkippedRepos = Template.bind({});
|
||||
WithoutSkippedRepos.args = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeBadge, VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react';
|
||||
import {
|
||||
@@ -80,6 +80,9 @@ export type RepoRowProps = {
|
||||
|
||||
interpretedResults?: AnalysisAlert[];
|
||||
rawResults?: AnalysisRawResults;
|
||||
|
||||
selected?: boolean;
|
||||
onSelectedChange?: (repositoryId: number, selected: boolean) => void;
|
||||
}
|
||||
|
||||
const canExpand = (
|
||||
@@ -101,6 +104,11 @@ const canExpand = (
|
||||
return downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded || downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
};
|
||||
|
||||
const canSelect = (
|
||||
status: VariantAnalysisRepoStatus | undefined,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus | undefined,
|
||||
) => status == VariantAnalysisRepoStatus.Succeeded && downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
||||
|
||||
const isExpandableContentLoaded = (
|
||||
status: VariantAnalysisRepoStatus | undefined,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus | undefined,
|
||||
@@ -133,6 +141,8 @@ export const RepoRow = ({
|
||||
resultCount,
|
||||
interpretedResults,
|
||||
rawResults,
|
||||
selected,
|
||||
onSelectedChange,
|
||||
}: RepoRowProps) => {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
const resultsLoaded = !!interpretedResults || !!rawResults;
|
||||
@@ -163,13 +173,35 @@ export const RepoRow = ({
|
||||
}
|
||||
}, [resultsLoaded, resultsLoading]);
|
||||
|
||||
const onClickCheckbox = useCallback((e: React.MouseEvent) => {
|
||||
// Prevent calling the onClick event of the container, which would toggle the expanded state
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
const onChangeCheckbox = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
// This is called on first render, but we don't really care about this value
|
||||
if (e.target.checked === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repository.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectedChange?.(repository.id, e.target.checked);
|
||||
}, [onSelectedChange, repository]);
|
||||
|
||||
const disabled = !canExpand(status, downloadStatus);
|
||||
const expandableContentLoaded = isExpandableContentLoaded(status, downloadStatus, resultsLoaded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TitleContainer onClick={toggleExpanded} disabled={disabled} aria-expanded={isExpanded}>
|
||||
<VSCodeCheckbox disabled />
|
||||
<VSCodeCheckbox
|
||||
onChange={onChangeCheckbox}
|
||||
onClick={onClickCheckbox}
|
||||
checked={selected}
|
||||
disabled={!repository.id || !canSelect(status, downloadStatus)}
|
||||
/>
|
||||
{isExpanded ? <ExpandCollapseCodicon name="chevron-down" label="Collapse" /> :
|
||||
<ExpandCollapseCodicon name="chevron-right" label="Expand" />}
|
||||
<VSCodeBadge>{resultCount === undefined ? '-' : formatDecimal(resultCount)}</VSCodeBadge>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RepositoriesFilterSortState, SortKey } from './filterSort';
|
||||
import { RepositoriesFilterSortState, SortKey } from '../../pure/variant-analysis-filter-sort';
|
||||
import { RepositoriesSearch } from './RepositoriesSearch';
|
||||
import { RepositoriesSort } from './RepositoriesSort';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
|
||||
import { SortKey } from './filterSort';
|
||||
import { SortKey } from '../../pure/variant-analysis-filter-sort';
|
||||
import { Codicon } from '../common';
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisDomainModel,
|
||||
@@ -11,6 +11,7 @@ import { VariantAnalysisOutcomePanels } from './VariantAnalysisOutcomePanels';
|
||||
import { VariantAnalysisLoading } from './VariantAnalysisLoading';
|
||||
import { ToVariantAnalysisMessage } from '../../pure/interface-types';
|
||||
import { vscode } from '../vscode-api';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
type Props = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
@@ -36,12 +37,6 @@ const stopQuery = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const copyRepositoryList = () => {
|
||||
vscode.postMessage({
|
||||
t: 'copyRepositoryList',
|
||||
});
|
||||
};
|
||||
|
||||
const exportResults = () => {
|
||||
vscode.postMessage({
|
||||
t: 'exportResults',
|
||||
@@ -63,6 +58,9 @@ export function VariantAnalysis({
|
||||
const [repoStates, setRepoStates] = useState<VariantAnalysisScannedRepositoryState[]>(initialRepoStates);
|
||||
const [repoResults, setRepoResults] = useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults);
|
||||
|
||||
const [selectedRepositoryIds, setSelectedRepositoryIds] = useState<number[]>([]);
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
@@ -96,6 +94,16 @@ export function VariantAnalysis({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const copyRepositoryList = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: 'copyRepositoryList',
|
||||
filterSort: {
|
||||
...filterSortState,
|
||||
repositoryIds: selectedRepositoryIds,
|
||||
},
|
||||
});
|
||||
}, [filterSortState, selectedRepositoryIds]);
|
||||
|
||||
if (variantAnalysis?.actionsWorkflowRunId === undefined) {
|
||||
return <VariantAnalysisLoading />;
|
||||
}
|
||||
@@ -115,6 +123,10 @@ export function VariantAnalysis({
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repoStates}
|
||||
repositoryResults={repoResults}
|
||||
selectedRepositoryIds={selectedRepositoryIds}
|
||||
setSelectedRepositoryIds={setSelectedRepositoryIds}
|
||||
filterSortState={filterSortState}
|
||||
setFilterSortState={setFilterSortState}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import {
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { compareWithResults, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
import {
|
||||
filterAndSortRepositoriesWithResults,
|
||||
RepositoriesFilterSortState,
|
||||
} from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
@@ -22,6 +25,9 @@ export type VariantAnalysisAnalyzedReposProps = {
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
|
||||
filterSortState?: RepositoriesFilterSortState;
|
||||
|
||||
selectedRepositoryIds?: number[];
|
||||
setSelectedRepositoryIds?: Dispatch<SetStateAction<number[]>>;
|
||||
}
|
||||
|
||||
export const VariantAnalysisAnalyzedRepos = ({
|
||||
@@ -29,6 +35,8 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
filterSortState,
|
||||
selectedRepositoryIds,
|
||||
setSelectedRepositoryIds,
|
||||
}: VariantAnalysisAnalyzedReposProps) => {
|
||||
const repositoryStateById = useMemo(() => {
|
||||
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
|
||||
@@ -47,11 +55,23 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
}, [repositoryResults]);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
return variantAnalysis.scannedRepos?.filter((repoTask) => {
|
||||
return matchesFilter(repoTask.repository, filterSortState);
|
||||
})?.sort(compareWithResults(filterSortState));
|
||||
return filterAndSortRepositoriesWithResults(variantAnalysis.scannedRepos, filterSortState);
|
||||
}, [filterSortState, variantAnalysis.scannedRepos]);
|
||||
|
||||
const onSelectedChange = useCallback((repositoryId: number, selected: boolean) => {
|
||||
setSelectedRepositoryIds?.((prevSelectedRepositoryIds) => {
|
||||
if (selected) {
|
||||
if (prevSelectedRepositoryIds.includes(repositoryId)) {
|
||||
return prevSelectedRepositoryIds;
|
||||
}
|
||||
|
||||
return [...prevSelectedRepositoryIds, repositoryId];
|
||||
} else {
|
||||
return prevSelectedRepositoryIds.filter((id) => id !== repositoryId);
|
||||
}
|
||||
});
|
||||
}, [setSelectedRepositoryIds]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{repositories?.map(repository => {
|
||||
@@ -67,6 +87,8 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
resultCount={repository.resultCount}
|
||||
interpretedResults={results?.interpretedResults}
|
||||
rawResults={results?.rawResults}
|
||||
selected={selectedRepositoryIds?.includes(repository.repository.id)}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from './filterSort';
|
||||
import { RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
|
||||
import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow';
|
||||
import { FailureReasonAlert } from './FailureReasonAlert';
|
||||
|
||||
@@ -20,6 +20,12 @@ export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
repositoryStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
|
||||
selectedRepositoryIds?: number[];
|
||||
setSelectedRepositoryIds?: Dispatch<SetStateAction<number[]>>;
|
||||
|
||||
filterSortState: RepositoriesFilterSortState;
|
||||
setFilterSortState: Dispatch<SetStateAction<RepositoriesFilterSortState>>;
|
||||
};
|
||||
|
||||
const Tab = styled(VSCodePanelTab)`
|
||||
@@ -46,9 +52,11 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
variantAnalysis,
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
selectedRepositoryIds,
|
||||
setSelectedRepositoryIds,
|
||||
filterSortState,
|
||||
setFilterSortState,
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
const scannedReposCount = variantAnalysis.scannedRepos?.length ?? 0;
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
|
||||
@@ -94,6 +102,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
filterSortState={filterSortState}
|
||||
selectedRepositoryIds={selectedRepositoryIds}
|
||||
setSelectedRepositoryIds={setSelectedRepositoryIds}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -126,6 +136,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
filterSortState={filterSortState}
|
||||
selectedRepositoryIds={selectedRepositoryIds}
|
||||
setSelectedRepositoryIds={setSelectedRepositoryIds}
|
||||
/>
|
||||
</VSCodePanelView>
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from 'styled-components';
|
||||
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
|
||||
import { Alert } from '../common';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import { compareRepository, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
import { compareRepository, matchesFilter, RepositoriesFilterSortState } from '../../pure/variant-analysis-filter-sort';
|
||||
|
||||
export type VariantAnalysisSkippedRepositoriesTabProps = {
|
||||
alertTitle: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { render as reactRender, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus
|
||||
@@ -330,4 +330,42 @@ describe(RepoRow.name, () => {
|
||||
expanded: false
|
||||
})).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not allow selecting the item if the item has not succeeded', async () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.InProgress,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not allow selecting the item if the item has not been downloaded', async () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not allow selecting the item if the item has not been downloaded successfully', async () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
});
|
||||
|
||||
// It seems like sometimes the first render doesn't have the checkbox disabled
|
||||
// Might be related to https://github.com/microsoft/vscode-webview-ui-toolkit/issues/404
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows selecting the item if the item has been downloaded', async () => {
|
||||
render({
|
||||
status: VariantAnalysisRepoStatus.Succeeded,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('checkbox')).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { VariantAnalysisAnalyzedRepos, VariantAnalysisAnalyzedReposProps } from
|
||||
import { createMockVariantAnalysis } from '../../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { createMockScannedRepo } from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
import { defaultFilterSortState, SortKey } from '../../../pure/variant-analysis-filter-sort';
|
||||
|
||||
describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
const defaultVariantAnalysis = createMockVariantAnalysis({
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createMockScannedRepo,
|
||||
createMockScannedRepos
|
||||
} from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import { defaultFilterSortState } from '../../../pure/variant-analysis-filter-sort';
|
||||
|
||||
describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
const defaultVariantAnalysis = {
|
||||
@@ -81,6 +82,8 @@ describe(VariantAnalysisOutcomePanels.name, () => {
|
||||
...defaultVariantAnalysis,
|
||||
...variantAnalysis,
|
||||
}}
|
||||
filterSortState={defaultFilterSortState}
|
||||
setFilterSortState={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisSkippedRepositoriesTab, VariantAnalysisSkippedRepositoriesTabProps } from '../VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
import { defaultFilterSortState, SortKey } from '../../../pure/variant-analysis-filter-sort';
|
||||
|
||||
describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
const render = (props: VariantAnalysisSkippedRepositoriesTabProps) =>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { compareRepository, compareWithResults, defaultFilterSortState, matchesFilter, SortKey } from '../filterSort';
|
||||
import {
|
||||
compareRepository,
|
||||
compareWithResults,
|
||||
defaultFilterSortState,
|
||||
filterAndSortRepositoriesWithResults,
|
||||
matchesFilter,
|
||||
SortKey,
|
||||
} from '../../../pure/variant-analysis-filter-sort';
|
||||
|
||||
// TODO: Move this file to the "pure" tests once it has been switched to Jest
|
||||
describe(matchesFilter.name, () => {
|
||||
const repository = {
|
||||
fullName: 'github/codeql'
|
||||
@@ -169,11 +177,13 @@ describe(compareWithResults.name, () => {
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
id: 10,
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
id: 12,
|
||||
fullName: 'github/world',
|
||||
},
|
||||
};
|
||||
@@ -191,12 +201,14 @@ describe(compareWithResults.name, () => {
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
id: 11,
|
||||
fullName: 'github/galaxy',
|
||||
stargazersCount: 1,
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
id: 12,
|
||||
fullName: 'github/world',
|
||||
stargazersCount: 10,
|
||||
},
|
||||
@@ -215,12 +227,14 @@ describe(compareWithResults.name, () => {
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
id: 11,
|
||||
fullName: 'github/galaxy',
|
||||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
id: 12,
|
||||
fullName: 'github/world',
|
||||
updatedAt: '2021-01-01T00:00:00Z',
|
||||
},
|
||||
@@ -239,12 +253,14 @@ describe(compareWithResults.name, () => {
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
id: 11,
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
resultCount: 10,
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
id: 12,
|
||||
fullName: 'github/world',
|
||||
},
|
||||
resultCount: 100,
|
||||
@@ -277,3 +293,84 @@ describe(compareWithResults.name, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(filterAndSortRepositoriesWithResults.name, () => {
|
||||
const repositories = [
|
||||
{
|
||||
repository: {
|
||||
id: 10,
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
resultCount: 10,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 11,
|
||||
fullName: 'github/world',
|
||||
},
|
||||
resultCount: undefined,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 13,
|
||||
fullName: 'github/planet',
|
||||
},
|
||||
resultCount: 500,
|
||||
},
|
||||
{
|
||||
repository: {
|
||||
id: 783532,
|
||||
fullName: 'github/stars',
|
||||
},
|
||||
resultCount: 8000,
|
||||
}
|
||||
];
|
||||
|
||||
describe('when sort key is given without filter', () => {
|
||||
it('returns the correct results', () => {
|
||||
expect(filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
})).toEqual([repositories[3], repositories[2], repositories[0], repositories[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key and search filter are given', () => {
|
||||
it('returns the correct results', () => {
|
||||
expect(filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
searchValue: 'la',
|
||||
})).toEqual([repositories[2], repositories[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key, search filter, and repository ids are given', () => {
|
||||
it('returns the correct results', () => {
|
||||
expect(filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
searchValue: 'la',
|
||||
repositoryIds: [repositories[1].repository.id, repositories[3].repository.id],
|
||||
})).toEqual([repositories[3], repositories[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when repository ids are given', () => {
|
||||
it('returns the correct results', () => {
|
||||
expect(filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
repositoryIds: [repositories[0].repository.id, repositories[3].repository.id],
|
||||
})).toEqual([repositories[0], repositories[3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when empty repository ids are given', () => {
|
||||
it('returns the correct results', () => {
|
||||
expect(filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
repositoryIds: [],
|
||||
})).toEqual([repositories[0], repositories[2], repositories[3], repositories[1]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri,
|
||||
import { CodeQLExtensionInterface } from '../../../extension';
|
||||
import { logger } from '../../../logging';
|
||||
import * as config from '../../../config';
|
||||
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import * as ghActionsApiClient from '../../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import { Credentials } from '../../../authentication';
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { UserCancellationException } from '../../../commandRunner';
|
||||
import { Repository } from '../../../remote-queries/gh-api/repository';
|
||||
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../../config';
|
||||
import { defaultFilterSortState, SortKey } from '../../../pure/variant-analysis-filter-sort';
|
||||
|
||||
describe('Variant Analysis Manager', async function() {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -766,23 +767,23 @@ describe('Variant Analysis Manager', async function() {
|
||||
describe('when the variant analysis has repositories with results', () => {
|
||||
const scannedRepos = [
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
...createMockScannedRepo('pear'),
|
||||
resultCount: 100,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
...createMockScannedRepo('apple'),
|
||||
resultCount: 0,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
...createMockScannedRepo('citrus'),
|
||||
resultCount: 200,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
...createMockScannedRepo('sky'),
|
||||
resultCount: undefined,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
...createMockScannedRepo('banana'),
|
||||
resultCount: 5,
|
||||
},
|
||||
];
|
||||
@@ -809,8 +810,44 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
expect(parsed).to.deep.eq({
|
||||
'new-repo-list': [
|
||||
scannedRepos[0].repository.fullName,
|
||||
scannedRepos[4].repository.fullName,
|
||||
scannedRepos[2].repository.fullName,
|
||||
scannedRepos[0].repository.fullName,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the sort key', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
});
|
||||
|
||||
const text = writeTextStub.getCalls()[0].lastArg;
|
||||
|
||||
const parsed = JSON.parse('{' + text + '}');
|
||||
|
||||
expect(parsed).to.deep.eq({
|
||||
'new-repo-list': [
|
||||
scannedRepos[2].repository.fullName,
|
||||
scannedRepos[0].repository.fullName,
|
||||
scannedRepos[4].repository.fullName,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the search value', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id, {
|
||||
...defaultFilterSortState,
|
||||
searchValue: 'ban',
|
||||
});
|
||||
|
||||
const text = writeTextStub.getCalls()[0].lastArg;
|
||||
|
||||
const parsed = JSON.parse('{' + text + '}');
|
||||
|
||||
expect(parsed).to.deep.eq({
|
||||
'new-repo-list': [
|
||||
scannedRepos[4].repository.fullName,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* Creates a partially implemented mock of vscode.ExtensionContext.
|
||||
*/
|
||||
export function createMockExtensionContext({
|
||||
extensionPath = '/mock/extension/path',
|
||||
workspaceStoragePath = '/mock/workspace/storage/path',
|
||||
globalStoragePath = '/mock/global/storage/path',
|
||||
}: {
|
||||
extensionPath?: string,
|
||||
workspaceStoragePath?: string,
|
||||
globalStoragePath?: string,
|
||||
}): vscode.ExtensionContext {
|
||||
|
||||
return {
|
||||
extensionPath: extensionPath,
|
||||
globalStorageUri: vscode.Uri.file(globalStoragePath),
|
||||
storageUri: vscode.Uri.file(workspaceStoragePath),
|
||||
} as any as vscode.ExtensionContext;
|
||||
}
|
||||
@@ -10,11 +10,14 @@ import { DbTreeDataProvider } from '../../../databases/ui/db-tree-data-provider'
|
||||
import { DbPanel } from '../../../databases/ui/db-panel';
|
||||
import { DbItemKind } from '../../../databases/db-item';
|
||||
import { DbTreeViewItem } from '../../../databases/ui/db-tree-view-item';
|
||||
import { ExtensionApp } from '../../../common/vscode/vscode-app';
|
||||
import { createMockExtensionContext } from '../../factories/extension-context';
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
|
||||
describe('db panel', async () => {
|
||||
const workspaceStoragePath = path.join(__dirname, 'test-workspace');
|
||||
const workspaceStoragePath = path.join(__dirname, 'test-workspace-storage');
|
||||
const globalStoragePath = path.join(__dirname, 'test-global-storage');
|
||||
const extensionPath = path.join(__dirname, '../../../../');
|
||||
const dbConfigFilePath = path.join(workspaceStoragePath, 'workspace-databases.json');
|
||||
let dbTreeDataProvider: DbTreeDataProvider;
|
||||
@@ -23,7 +26,14 @@ describe('db panel', async () => {
|
||||
let dbPanel: DbPanel;
|
||||
|
||||
before(async () => {
|
||||
dbConfigStore = new DbConfigStore(workspaceStoragePath, extensionPath);
|
||||
const extensionContext = createMockExtensionContext({
|
||||
extensionPath,
|
||||
globalStoragePath,
|
||||
workspaceStoragePath
|
||||
});
|
||||
const app = new ExtensionApp(extensionContext);
|
||||
|
||||
dbConfigStore = new DbConfigStore(app);
|
||||
dbManager = new DbManager(dbConfigStore);
|
||||
|
||||
// Create a modified version of the DbPanel module that allows
|
||||
@@ -58,6 +68,10 @@ describe('db panel', async () => {
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -116,6 +130,10 @@ describe('db panel', async () => {
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -148,6 +166,10 @@ describe('db panel', async () => {
|
||||
owners: ['owner1', 'owner2'],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -177,6 +199,10 @@ describe('db panel', async () => {
|
||||
owners: [],
|
||||
repositories: ['owner1/repo1', 'owner1/repo2']
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
},
|
||||
};
|
||||
|
||||
await saveDbConfig(dbConfig);
|
||||
@@ -214,7 +240,7 @@ describe('db panel', async () => {
|
||||
): void {
|
||||
expect(item.label).to.equal(`Top ${n} repositories`);
|
||||
expect(item.tooltip).to.equal(`Top ${n} repositories of a language`);
|
||||
expect(item.icon).to.deep.equal(new vscode.ThemeIcon('github'));
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('github'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
}
|
||||
|
||||
@@ -225,7 +251,7 @@ describe('db panel', async () => {
|
||||
): void {
|
||||
expect(item.label).to.equal(listName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.icon).to.be.undefined;
|
||||
expect(item.iconPath).to.be.undefined;
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.Collapsed);
|
||||
expect(item.children).to.be.ok;
|
||||
expect(item.children.length).to.equal(repos.length);
|
||||
@@ -241,7 +267,7 @@ describe('db panel', async () => {
|
||||
): void {
|
||||
expect(item.label).to.equal(ownerName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.icon).to.deep.equal(new vscode.ThemeIcon('organization'));
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('organization'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
expect(item.children).to.be.ok;
|
||||
expect(item.children.length).to.equal(0);
|
||||
@@ -253,7 +279,7 @@ describe('db panel', async () => {
|
||||
): void {
|
||||
expect(item.label).to.equal(repoName);
|
||||
expect(item.tooltip).to.be.undefined;
|
||||
expect(item.icon).to.deep.equal(new vscode.ThemeIcon('database'));
|
||||
expect(item.iconPath).to.deep.equal(new vscode.ThemeIcon('database'));
|
||||
expect(item.collapsibleState).to.equal(vscode.TreeItemCollapsibleState.None);
|
||||
}
|
||||
});
|
||||
|
||||
36
extensions/ql-vscode/test/__mocks__/appMock.ts
Normal file
36
extensions/ql-vscode/test/__mocks__/appMock.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { App } from '../../src/common/app';
|
||||
import { AppEvent, AppEventEmitter } from '../../src/common/events';
|
||||
import { Disposable } from '../../src/pure/disposable-object';
|
||||
|
||||
export function createMockApp({
|
||||
extensionPath = '/mock/extension/path',
|
||||
workspaceStoragePath = '/mock/workspace/storage/path',
|
||||
globalStoragePath = '/mock/global/storage/path',
|
||||
createEventEmitter = <T>() => new MockAppEventEmitter<T>()
|
||||
}: {
|
||||
extensionPath?: string,
|
||||
workspaceStoragePath?: string,
|
||||
globalStoragePath?: string,
|
||||
createEventEmitter?: <T>() => AppEventEmitter<T>
|
||||
}): App {
|
||||
return {
|
||||
createEventEmitter,
|
||||
extensionPath,
|
||||
workspaceStoragePath,
|
||||
globalStoragePath
|
||||
};
|
||||
}
|
||||
|
||||
export class MockAppEventEmitter<T> implements AppEventEmitter<T> {
|
||||
public event: AppEvent<T>;
|
||||
|
||||
constructor() {
|
||||
this.event = () => {
|
||||
return {} as Disposable;
|
||||
};
|
||||
}
|
||||
|
||||
public fire(): void {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,39 @@
|
||||
],
|
||||
"owners": [],
|
||||
"repositories": ["owner/repo1", "owner/repo2", "owner/repo3"]
|
||||
},
|
||||
"local": {
|
||||
"lists": [
|
||||
{
|
||||
"name": "localList1",
|
||||
"databases": [
|
||||
{
|
||||
"name": "foo/bar",
|
||||
"dateAdded": 1668096745193,
|
||||
"language": "go",
|
||||
"storagePath": "/path/to/database/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "localList2",
|
||||
"databases": [
|
||||
{
|
||||
"name": "foo/baz",
|
||||
"dateAdded": 1668096760848,
|
||||
"language": "javascript",
|
||||
"storagePath": "/path/to/database/"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"databases": [
|
||||
{
|
||||
"name": "example-db",
|
||||
"dateAdded": 1668096927267,
|
||||
"language": "ruby",
|
||||
"storagePath": "/path/to/database/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { DbConfigStore } from '../../../src/databases/db-config-store';
|
||||
import { expect } from 'chai';
|
||||
import { createMockApp } from '../../__mocks__/appMock';
|
||||
|
||||
describe('db config store', async () => {
|
||||
const extensionPath = path.join(__dirname, '../../..');
|
||||
@@ -17,9 +18,14 @@ describe('db config store', async () => {
|
||||
});
|
||||
|
||||
it('should create a new config if one does not exist', async () => {
|
||||
const app = createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath: tempWorkspaceStoragePath
|
||||
});
|
||||
|
||||
const configPath = path.join(tempWorkspaceStoragePath, 'workspace-databases.json');
|
||||
|
||||
const configStore = new DbConfigStore(tempWorkspaceStoragePath, extensionPath);
|
||||
const configStore = new DbConfigStore(app);
|
||||
await configStore.initialize();
|
||||
|
||||
expect(await fs.pathExists(configPath)).to.be.true;
|
||||
@@ -27,25 +33,58 @@ describe('db config store', async () => {
|
||||
expect(config.remote.repositoryLists).to.be.empty;
|
||||
expect(config.remote.owners).to.be.empty;
|
||||
expect(config.remote.repositories).to.be.empty;
|
||||
expect(config.local.lists).to.be.empty;
|
||||
expect(config.local.databases).to.be.empty;
|
||||
});
|
||||
|
||||
it('should load an existing config', async () => {
|
||||
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
|
||||
const app = createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath: testDataStoragePath
|
||||
});
|
||||
const configStore = new DbConfigStore(app);
|
||||
await configStore.initialize();
|
||||
|
||||
const config = configStore.getConfig().value;
|
||||
expect(config.remote.repositoryLists).to.have.length(1);
|
||||
expect(config.remote.repositoryLists[0]).to.deep.equal({
|
||||
'name': 'repoList1',
|
||||
'repositories': ['foo/bar', 'foo/baz']
|
||||
name: 'repoList1',
|
||||
repositories: ['foo/bar', 'foo/baz']
|
||||
});
|
||||
expect(config.remote.owners).to.be.empty;
|
||||
expect(config.remote.repositories).to.have.length(3);
|
||||
expect(config.remote.repositories).to.deep.equal(['owner/repo1', 'owner/repo2', 'owner/repo3']);
|
||||
expect(config.remote.repositories).to.deep.equal([
|
||||
'owner/repo1',
|
||||
'owner/repo2',
|
||||
'owner/repo3',
|
||||
]);
|
||||
expect(config.local.lists).to.have.length(2);
|
||||
expect(config.local.lists[0]).to.deep.equal({
|
||||
name: 'localList1',
|
||||
databases: [
|
||||
{
|
||||
name: 'foo/bar',
|
||||
dateAdded: 1668096745193,
|
||||
language: 'go',
|
||||
storagePath: '/path/to/database/',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(config.local.databases).to.have.length(1);
|
||||
expect(config.local.databases[0]).to.deep.equal({
|
||||
name: 'example-db',
|
||||
dateAdded: 1668096927267,
|
||||
language: 'ruby',
|
||||
storagePath: '/path/to/database/',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow modification of the config', async () => {
|
||||
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
|
||||
const app = createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath: testDataStoragePath
|
||||
});
|
||||
const configStore = new DbConfigStore(app);
|
||||
await configStore.initialize();
|
||||
|
||||
const config = configStore.getConfig().value;
|
||||
|
||||
@@ -25,9 +25,10 @@ describe('db config validation', async () => {
|
||||
|
||||
const validationOutput = configValidator.validate(dbConfig);
|
||||
|
||||
expect(validationOutput).to.have.length(2);
|
||||
expect(validationOutput).to.have.length(3);
|
||||
|
||||
expect(validationOutput[0]).to.deep.equal('/remote must have required property \'owners\'');
|
||||
expect(validationOutput[1]).to.deep.equal('/remote must NOT have additional properties');
|
||||
expect(validationOutput[0]).to.deep.equal(' must have required property \'local\'');
|
||||
expect(validationOutput[1]).to.deep.equal('/remote must have required property \'owners\'');
|
||||
expect(validationOutput[2]).to.deep.equal('/remote must NOT have additional properties');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ describe('db tree creator', () => {
|
||||
repositoryLists: [],
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,6 +67,10 @@ describe('db tree creator', () => {
|
||||
],
|
||||
owners: [],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,6 +110,10 @@ describe('db tree creator', () => {
|
||||
'owner2'
|
||||
],
|
||||
repositories: []
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,6 +146,10 @@ describe('db tree creator', () => {
|
||||
'owner1/repo2',
|
||||
'owner2/repo1'
|
||||
]
|
||||
},
|
||||
local: {
|
||||
lists: [],
|
||||
databases: []
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"rootDirs": [".", "../src"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,72 @@
|
||||
},
|
||||
"required": ["repositoryLists", "owners", "repositories"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "number"
|
||||
},
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"storagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "dateAdded", "language", "storagePath"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "databases"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "number"
|
||||
},
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"storagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "dateAdded", "language", "storagePath"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["lists", "databases"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["remote", "local"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user