Merge branch 'koesie10/filter-export-copy' into koesie10/selected-copy
This commit is contained in:
@@ -325,7 +325,7 @@
|
|||||||
"title": "CodeQL: Run Variant Analysis"
|
"title": "CodeQL: Run Variant Analysis"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.exportVariantAnalysisResults",
|
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||||
"title": "CodeQL: Export Variant Analysis Results"
|
"title": "CodeQL: Export Variant Analysis Results"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -954,7 +954,7 @@
|
|||||||
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "codeQL.exportVariantAnalysisResults",
|
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||||
"when": "config.codeQL.canary"
|
"when": "config.codeQL.canary"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
import { Disposable } from '../pure/disposable-object';
|
||||||
import { AppEventEmitter } from './events';
|
import { AppEventEmitter } from './events';
|
||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||||
|
mode: AppMode;
|
||||||
|
subscriptions: Disposable[];
|
||||||
extensionPath: string;
|
extensionPath: string;
|
||||||
globalStoragePath: string;
|
globalStoragePath: string;
|
||||||
workspaceStoragePath?: string;
|
workspaceStoragePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AppMode {
|
||||||
|
Production = 1,
|
||||||
|
Development = 2,
|
||||||
|
Test = 3,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { App } from '../app';
|
import { Disposable } from '../../pure/disposable-object';
|
||||||
|
import { App, AppMode } from '../app';
|
||||||
import { AppEventEmitter } from '../events';
|
import { AppEventEmitter } from '../events';
|
||||||
import { VSCodeAppEventEmitter } from './events';
|
import { VSCodeAppEventEmitter } from './events';
|
||||||
|
|
||||||
@@ -21,6 +22,21 @@ export class ExtensionApp implements App {
|
|||||||
return this.extensionContext.storageUri?.fsPath;
|
return this.extensionContext.storageUri?.fsPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get subscriptions(): Disposable[] {
|
||||||
|
return this.extensionContext.subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mode(): AppMode {
|
||||||
|
switch (this.extensionContext.extensionMode) {
|
||||||
|
case vscode.ExtensionMode.Development:
|
||||||
|
return AppMode.Development;
|
||||||
|
case vscode.ExtensionMode.Test:
|
||||||
|
return AppMode.Test;
|
||||||
|
default:
|
||||||
|
return AppMode.Production;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public createEventEmitter<T>(): AppEventEmitter<T> {
|
public createEventEmitter<T>(): AppEventEmitter<T> {
|
||||||
return new VSCodeAppEventEmitter<T>();
|
return new VSCodeAppEventEmitter<T>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import { DisposableObject } from '../pure/disposable-object';
|
|||||||
import { DbConfigValidator } from './db-config-validator';
|
import { DbConfigValidator } from './db-config-validator';
|
||||||
import { ValueResult } from '../common/value-result';
|
import { ValueResult } from '../common/value-result';
|
||||||
import { App } from '../common/app';
|
import { App } from '../common/app';
|
||||||
|
import { AppEvent, AppEventEmitter } from '../common/events';
|
||||||
|
|
||||||
export class DbConfigStore extends DisposableObject {
|
export class DbConfigStore extends DisposableObject {
|
||||||
|
public readonly onDidChangeConfig: AppEvent<void>;
|
||||||
|
private readonly onDidChangeConfigEventEmitter: AppEventEmitter<void>;
|
||||||
|
|
||||||
private readonly configPath: string;
|
private readonly configPath: string;
|
||||||
private readonly configValidator: DbConfigValidator;
|
private readonly configValidator: DbConfigValidator;
|
||||||
|
|
||||||
@@ -25,6 +29,8 @@ export class DbConfigStore extends DisposableObject {
|
|||||||
this.configErrors = [];
|
this.configErrors = [];
|
||||||
this.configWatcher = undefined;
|
this.configWatcher = undefined;
|
||||||
this.configValidator = new DbConfigValidator(app.extensionPath);
|
this.configValidator = new DbConfigValidator(app.extensionPath);
|
||||||
|
this.onDidChangeConfigEventEmitter = app.createEventEmitter<void>();
|
||||||
|
this.onDidChangeConfig = this.onDidChangeConfigEventEmitter.event;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<void> {
|
||||||
@@ -85,6 +91,8 @@ export class DbConfigStore extends DisposableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.config = this.configErrors.length === 0 ? newConfig : undefined;
|
this.config = this.configErrors.length === 0 ? newConfig : undefined;
|
||||||
|
|
||||||
|
this.onDidChangeConfigEventEmitter.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
private watchConfig(): void {
|
private watchConfig(): void {
|
||||||
@@ -95,15 +103,17 @@ export class DbConfigStore extends DisposableObject {
|
|||||||
|
|
||||||
private createEmptyConfig(): DbConfig {
|
private createEmptyConfig(): DbConfig {
|
||||||
return {
|
return {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [],
|
repositoryLists: [],
|
||||||
repositories: [],
|
owners: [],
|
||||||
},
|
repositories: [],
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: [],
|
lists: [],
|
||||||
},
|
databases: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
// Contains models for the data we want to store in the database config
|
// Contains models for the data we want to store in the database config
|
||||||
|
|
||||||
export interface DbConfig {
|
export interface DbConfig {
|
||||||
|
databases: DbConfigDatabases;
|
||||||
|
selected?: SelectedDbItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DbConfigDatabases {
|
||||||
remote: RemoteDbConfig;
|
remote: RemoteDbConfig;
|
||||||
local: LocalDbConfig;
|
local: LocalDbConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SelectedDbItem {
|
||||||
|
kind: SelectedDbItemKind;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SelectedDbItemKind {
|
||||||
|
ConfigDefined = 'configDefined',
|
||||||
|
RemoteSystemDefinedList = 'remoteSystemDefinedList',
|
||||||
|
}
|
||||||
|
|
||||||
export interface RemoteDbConfig {
|
export interface RemoteDbConfig {
|
||||||
repositoryLists: RemoteRepositoryList[];
|
repositoryLists: RemoteRepositoryList[];
|
||||||
owners: string[];
|
owners: string[];
|
||||||
@@ -35,20 +50,26 @@ export interface LocalDatabase {
|
|||||||
|
|
||||||
export function cloneDbConfig(config: DbConfig): DbConfig {
|
export function cloneDbConfig(config: DbConfig): DbConfig {
|
||||||
return {
|
return {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: config.remote.repositoryLists.map((list) => ({
|
remote: {
|
||||||
name: list.name,
|
repositoryLists: config.databases.remote.repositoryLists.map((list) => ({
|
||||||
repositories: [...list.repositories],
|
name: list.name,
|
||||||
})),
|
repositories: [...list.repositories],
|
||||||
owners: [...config.remote.owners],
|
})),
|
||||||
repositories: [...config.remote.repositories],
|
owners: [...config.databases.remote.owners],
|
||||||
},
|
repositories: [...config.databases.remote.repositories],
|
||||||
local: {
|
},
|
||||||
lists: config.local.lists.map((list) => ({
|
local: {
|
||||||
name: list.name,
|
lists: config.databases.local.lists.map((list) => ({
|
||||||
databases: list.databases.map((db) => ({ ...db })),
|
name: list.name,
|
||||||
})),
|
databases: list.databases.map((db) => ({ ...db })),
|
||||||
databases: config.local.databases.map((db) => ({ ...db })),
|
})),
|
||||||
|
databases: config.databases.local.databases.map((db) => ({ ...db })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
selected: config.selected ? {
|
||||||
|
kind: config.selected.kind,
|
||||||
|
value: config.selected.value,
|
||||||
|
} : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
|
import { App } from '../common/app';
|
||||||
|
import { AppEvent, AppEventEmitter } from '../common/events';
|
||||||
import { ValueResult } from '../common/value-result';
|
import { ValueResult } from '../common/value-result';
|
||||||
import { DbConfigStore } from './db-config-store';
|
import { DbConfigStore } from './db-config-store';
|
||||||
import { DbItem } from './db-item';
|
import { DbItem } from './db-item';
|
||||||
import { createLocalTree, createRemoteTree } from './db-tree-creator';
|
import { createLocalTree, createRemoteTree } from './db-tree-creator';
|
||||||
|
|
||||||
export class DbManager {
|
export class DbManager {
|
||||||
|
public readonly onDbItemsChanged: AppEvent<void>;
|
||||||
|
private readonly onDbItemsChangesEventEmitter: AppEventEmitter<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
app: App,
|
||||||
private readonly dbConfigStore: DbConfigStore
|
private readonly dbConfigStore: DbConfigStore
|
||||||
) {
|
) {
|
||||||
|
this.onDbItemsChangesEventEmitter = app.createEventEmitter<void>();
|
||||||
|
this.onDbItemsChanged = this.onDbItemsChangesEventEmitter.event;
|
||||||
|
|
||||||
|
this.dbConfigStore.onDidChangeConfig(() => {
|
||||||
|
this.onDbItemsChangesEventEmitter.fire();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDbItems(): ValueResult<DbItem[]> {
|
public getDbItems(): ValueResult<DbItem[]> {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as vscode from 'vscode';
|
import { App, AppMode } from '../common/app';
|
||||||
import { ExtensionApp } from '../common/vscode/vscode-app';
|
|
||||||
import { isCanary, isNewQueryRunExperienceEnabled } from '../config';
|
import { isCanary, isNewQueryRunExperienceEnabled } from '../config';
|
||||||
import { logger } from '../logging';
|
import { logger } from '../logging';
|
||||||
import { DisposableObject } from '../pure/disposable-object';
|
import { DisposableObject } from '../pure/disposable-object';
|
||||||
@@ -8,10 +7,8 @@ import { DbManager } from './db-manager';
|
|||||||
import { DbPanel } from './ui/db-panel';
|
import { DbPanel } from './ui/db-panel';
|
||||||
|
|
||||||
export class DbModule extends DisposableObject {
|
export class DbModule extends DisposableObject {
|
||||||
public async initialize(
|
public async initialize(app: App): Promise<void> {
|
||||||
extensionContext: vscode.ExtensionContext
|
if (app.mode !== AppMode.Development ||
|
||||||
): Promise<void> {
|
|
||||||
if (extensionContext.extensionMode !== vscode.ExtensionMode.Development ||
|
|
||||||
!isCanary() ||
|
!isCanary() ||
|
||||||
!isNewQueryRunExperienceEnabled()) {
|
!isNewQueryRunExperienceEnabled()) {
|
||||||
// Currently, we only want to expose the new database panel when we
|
// Currently, we only want to expose the new database panel when we
|
||||||
@@ -22,25 +19,20 @@ export class DbModule extends DisposableObject {
|
|||||||
|
|
||||||
void logger.log('Initializing database module');
|
void logger.log('Initializing database module');
|
||||||
|
|
||||||
const app = new ExtensionApp(extensionContext);
|
|
||||||
|
|
||||||
const dbConfigStore = new DbConfigStore(app);
|
const dbConfigStore = new DbConfigStore(app);
|
||||||
await dbConfigStore.initialize();
|
await dbConfigStore.initialize();
|
||||||
|
|
||||||
const dbManager = new DbManager(dbConfigStore);
|
const dbManager = new DbManager(app, dbConfigStore);
|
||||||
const dbPanel = new DbPanel(dbManager);
|
const dbPanel = new DbPanel(dbManager);
|
||||||
await dbPanel.initialize();
|
await dbPanel.initialize();
|
||||||
extensionContext.subscriptions.push(dbPanel);
|
|
||||||
|
|
||||||
this.push(dbPanel);
|
this.push(dbPanel);
|
||||||
this.push(dbConfigStore);
|
this.push(dbConfigStore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeDbModule(
|
export async function initializeDbModule(app: App): Promise<DbModule> {
|
||||||
extensionContext: vscode.ExtensionContext
|
|
||||||
): Promise<DbModule> {
|
|
||||||
const dbModule = new DbModule();
|
const dbModule = new DbModule();
|
||||||
await dbModule.initialize(extensionContext);
|
await dbModule.initialize(app);
|
||||||
return dbModule;
|
return dbModule;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
|
|||||||
createSystemDefinedList(1000)
|
createSystemDefinedList(1000)
|
||||||
];
|
];
|
||||||
|
|
||||||
const userDefinedRepoLists = dbConfig.remote.repositoryLists.map(createUserDefinedList);
|
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map(createUserDefinedList);
|
||||||
const owners = dbConfig.remote.owners.map(createOwnerItem);
|
const owners = dbConfig.databases.remote.owners.map(createOwnerItem);
|
||||||
const repos = dbConfig.remote.repositories.map(createRepoItem);
|
const repos = dbConfig.databases.remote.repositories.map(createRepoItem);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: DbItemKind.RootRemote,
|
kind: DbItemKind.RootRemote,
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import { ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
|
import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
|
||||||
import { createDbTreeViewItemError, DbTreeViewItem } from './db-tree-view-item';
|
import { createDbTreeViewItemError, DbTreeViewItem } from './db-tree-view-item';
|
||||||
import { DbManager } from '../db-manager';
|
import { DbManager } from '../db-manager';
|
||||||
import { mapDbItemToTreeViewItem } from './db-item-mapper';
|
import { mapDbItemToTreeViewItem } from './db-item-mapper';
|
||||||
|
import { DisposableObject } from '../../pure/disposable-object';
|
||||||
|
|
||||||
export class DbTreeDataProvider implements TreeDataProvider<DbTreeViewItem> {
|
export class DbTreeDataProvider extends DisposableObject implements TreeDataProvider<DbTreeViewItem> {
|
||||||
|
|
||||||
|
// This is an event to signal that there's been a change in the tree which
|
||||||
|
// will case the view to refresh. It is part of the TreeDataProvider interface.
|
||||||
|
public readonly onDidChangeTreeData: Event<DbTreeViewItem | undefined>;
|
||||||
|
|
||||||
|
private _onDidChangeTreeData = this.push(new EventEmitter<DbTreeViewItem | undefined>());
|
||||||
private dbTreeItems: DbTreeViewItem[];
|
private dbTreeItems: DbTreeViewItem[];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dbManager: DbManager
|
private readonly dbManager: DbManager
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this.dbTreeItems = this.createTree();
|
this.dbTreeItems = this.createTree();
|
||||||
|
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||||
|
|
||||||
|
dbManager.onDbItemsChanged(() => {
|
||||||
|
this.dbTreeItems = this.createTree();
|
||||||
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
|||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||||
import { exportRemoteQueryResults } from './remote-queries/export-results';
|
import {
|
||||||
|
exportRemoteQueryResults,
|
||||||
|
exportSelectedRemoteQueryResults,
|
||||||
|
exportVariantAnalysisResults
|
||||||
|
} from './remote-queries/export-results';
|
||||||
import { RemoteQuery } from './remote-queries/remote-query';
|
import { RemoteQuery } from './remote-queries/remote-query';
|
||||||
import { EvalLogViewer } from './eval-log-viewer';
|
import { EvalLogViewer } from './eval-log-viewer';
|
||||||
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
|
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
|
||||||
@@ -116,6 +120,7 @@ import { createVariantAnalysisContentProvider } from './remote-queries/variant-a
|
|||||||
import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server';
|
import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server';
|
||||||
import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager';
|
import { VariantAnalysisResultsManager } from './remote-queries/variant-analysis-results-manager';
|
||||||
import { initializeDbModule } from './databases/db-module';
|
import { initializeDbModule } from './databases/db-module';
|
||||||
|
import { ExtensionApp } from './common/vscode/vscode-app';
|
||||||
import { RepositoriesFilterSortStateWithIds } from './pure/variant-analysis-filter-sort';
|
import { RepositoriesFilterSortStateWithIds } from './pure/variant-analysis-filter-sort';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -992,11 +997,23 @@ async function activateWithInstalledDistribution(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
ctx.subscriptions.push(
|
ctx.subscriptions.push(
|
||||||
commandRunner('codeQL.exportVariantAnalysisResults', async (queryId?: string) => {
|
commandRunner('codeQL.exportSelectedVariantAnalysisResults', async () => {
|
||||||
|
await exportSelectedRemoteQueryResults(qhm);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.subscriptions.push(
|
||||||
|
commandRunner('codeQL.exportRemoteQueryResults', async (queryId: string) => {
|
||||||
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
|
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ctx.subscriptions.push(
|
||||||
|
commandRunner('codeQL.exportVariantAnalysisResults', async (variantAnalysisId: number) => {
|
||||||
|
await exportVariantAnalysisResults(ctx, variantAnalysisManager, variantAnalysisId);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
ctx.subscriptions.push(
|
ctx.subscriptions.push(
|
||||||
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
|
||||||
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);
|
||||||
@@ -1251,7 +1268,8 @@ async function activateWithInstalledDistribution(
|
|||||||
void logger.log('Reading query history');
|
void logger.log('Reading query history');
|
||||||
await qhm.readQueryHistory();
|
await qhm.readQueryHistory();
|
||||||
|
|
||||||
const dbModule = await initializeDbModule(ctx);
|
const app = new ExtensionApp(ctx);
|
||||||
|
const dbModule = await initializeDbModule(app);
|
||||||
ctx.subscriptions.push(dbModule);
|
ctx.subscriptions.push(dbModule);
|
||||||
|
|
||||||
void logger.log('Successfully finished extension initialization.');
|
void logger.log('Successfully finished extension initialization.');
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class MockGitHubApiServer extends DisposableObject {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.server.listen();
|
this.server.listen({ onUnhandledRequest: 'bypass' });
|
||||||
this._isListening = true;
|
this._isListening = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -478,6 +478,10 @@ export interface CopyRepositoryListMessage {
|
|||||||
filterSort?: RepositoriesFilterSortStateWithIds;
|
filterSort?: RepositoriesFilterSortStateWithIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportResultsMessage {
|
||||||
|
t: 'exportResults';
|
||||||
|
}
|
||||||
|
|
||||||
export interface OpenLogsMessage {
|
export interface OpenLogsMessage {
|
||||||
t: 'openLogs';
|
t: 'openLogs';
|
||||||
}
|
}
|
||||||
@@ -497,5 +501,6 @@ export type FromVariantAnalysisMessage =
|
|||||||
| OpenQueryFileMessage
|
| OpenQueryFileMessage
|
||||||
| OpenQueryTextMessage
|
| OpenQueryTextMessage
|
||||||
| CopyRepositoryListMessage
|
| CopyRepositoryListMessage
|
||||||
|
| ExportResultsMessage
|
||||||
| OpenLogsMessage
|
| OpenLogsMessage
|
||||||
| CancelVariantAnalysisMessage;
|
| CancelVariantAnalysisMessage;
|
||||||
|
|||||||
@@ -1267,8 +1267,22 @@ export class QueryHistoryManager extends DisposableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleExportResults(): Promise<void> {
|
async handleExportResults(
|
||||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults');
|
singleItem: QueryHistoryInfo,
|
||||||
|
multiSelect: QueryHistoryInfo[],
|
||||||
|
): Promise<void> {
|
||||||
|
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||||
|
|
||||||
|
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote queries and variant analysis only
|
||||||
|
if (finalSingleItem.t === 'remote') {
|
||||||
|
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
|
||||||
|
} else if (finalSingleItem.t === 'variant-analysis') {
|
||||||
|
await commands.executeCommand('codeQL.exportVariantAnalysisResults', finalSingleItem.variantAnalysis.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addQuery(item: QueryHistoryInfo) {
|
addQuery(item: QueryHistoryInfo) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
|
|
||||||
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
|
import { window, commands, Uri, ExtensionContext, workspace, ViewColumn } from 'vscode';
|
||||||
import { Credentials } from '../authentication';
|
import { Credentials } from '../authentication';
|
||||||
import { UserCancellationException } from '../commandRunner';
|
import { UserCancellationException } from '../commandRunner';
|
||||||
import { showInformationMessageWithAction } from '../helpers';
|
import { showInformationMessageWithAction } from '../helpers';
|
||||||
@@ -9,36 +9,54 @@ import { logger } from '../logging';
|
|||||||
import { QueryHistoryManager } from '../query-history';
|
import { QueryHistoryManager } from '../query-history';
|
||||||
import { createGist } from './gh-api/gh-api-client';
|
import { createGist } from './gh-api/gh-api-client';
|
||||||
import { RemoteQueriesManager } from './remote-queries-manager';
|
import { RemoteQueriesManager } from './remote-queries-manager';
|
||||||
import { generateMarkdown } from './remote-queries-markdown-generation';
|
import {
|
||||||
|
generateMarkdown,
|
||||||
|
generateVariantAnalysisMarkdown,
|
||||||
|
MarkdownFile,
|
||||||
|
} from './remote-queries-markdown-generation';
|
||||||
import { RemoteQuery } from './remote-query';
|
import { RemoteQuery } from './remote-query';
|
||||||
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
|
||||||
import { RemoteQueryHistoryItem } from './remote-query-history-item';
|
|
||||||
import { pluralize } from '../pure/word';
|
import { pluralize } from '../pure/word';
|
||||||
|
import { VariantAnalysisManager } from './variant-analysis-manager';
|
||||||
|
import { assertNever } from '../pure/helpers-pure';
|
||||||
|
import {
|
||||||
|
VariantAnalysis,
|
||||||
|
VariantAnalysisScannedRepository,
|
||||||
|
VariantAnalysisScannedRepositoryResult
|
||||||
|
} from './shared/variant-analysis';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the results of the given or currently-selected remote query.
|
* Exports the results of the currently-selected remote query or variant analysis.
|
||||||
|
*/
|
||||||
|
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
|
||||||
|
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||||
|
if (!queryHistoryItem || queryHistoryItem.t === 'local') {
|
||||||
|
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryHistoryItem.t === 'remote') {
|
||||||
|
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
|
||||||
|
} else if (queryHistoryItem.t === 'variant-analysis') {
|
||||||
|
return commands.executeCommand('codeQL.exportVariantAnalysisResults', queryHistoryItem.variantAnalysis.id);
|
||||||
|
} else {
|
||||||
|
assertNever(queryHistoryItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the results of the given remote query.
|
||||||
* The user is prompted to select the export format.
|
* The user is prompted to select the export format.
|
||||||
*/
|
*/
|
||||||
export async function exportRemoteQueryResults(
|
export async function exportRemoteQueryResults(
|
||||||
queryHistoryManager: QueryHistoryManager,
|
queryHistoryManager: QueryHistoryManager,
|
||||||
remoteQueriesManager: RemoteQueriesManager,
|
remoteQueriesManager: RemoteQueriesManager,
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
queryId?: string,
|
queryId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let queryHistoryItem: RemoteQueryHistoryItem;
|
const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId);
|
||||||
if (queryId) {
|
if (!queryHistoryItem) {
|
||||||
const query = queryHistoryManager.getRemoteQueryById(queryId);
|
void logger.log(`Could not find query with id ${queryId}`);
|
||||||
if (!query) {
|
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||||
void logger.log(`Could not find query with id ${queryId}`);
|
|
||||||
throw new Error('There was an error when trying to retrieve variant analysis information');
|
|
||||||
}
|
|
||||||
queryHistoryItem = query;
|
|
||||||
} else {
|
|
||||||
const query = queryHistoryManager.getCurrentQueryHistoryItem();
|
|
||||||
if (!query || query.t !== 'remote') {
|
|
||||||
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
|
|
||||||
}
|
|
||||||
queryHistoryItem = query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queryHistoryItem.completed) {
|
if (!queryHistoryItem.completed) {
|
||||||
@@ -49,32 +67,107 @@ export async function exportRemoteQueryResults(
|
|||||||
const query = queryHistoryItem.remoteQuery;
|
const query = queryHistoryItem.remoteQuery;
|
||||||
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
|
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
|
||||||
|
|
||||||
|
const exportFormat = await determineExportFormat();
|
||||||
|
if (!exportFormat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportDirectory = await queryHistoryManager.getQueryHistoryItemDirectory(queryHistoryItem);
|
||||||
|
|
||||||
|
await exportRemoteQueryAnalysisResults(ctx, exportDirectory, query, analysesResults, exportFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportRemoteQueryAnalysisResults(
|
||||||
|
ctx: ExtensionContext,
|
||||||
|
exportDirectory: string,
|
||||||
|
query: RemoteQuery,
|
||||||
|
analysesResults: AnalysisResults[],
|
||||||
|
exportFormat: 'gist' | 'local',
|
||||||
|
) {
|
||||||
|
const description = buildGistDescription(query, analysesResults);
|
||||||
|
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
|
||||||
|
|
||||||
|
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the results of the given or currently-selected remote query.
|
||||||
|
* The user is prompted to select the export format.
|
||||||
|
*/
|
||||||
|
export async function exportVariantAnalysisResults(
|
||||||
|
ctx: ExtensionContext,
|
||||||
|
variantAnalysisManager: VariantAnalysisManager,
|
||||||
|
variantAnalysisId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
|
||||||
|
if (!variantAnalysis) {
|
||||||
|
void logger.log(`Could not find variant analysis with id ${variantAnalysisId}`);
|
||||||
|
throw new Error('There was an error when trying to retrieve variant analysis information');
|
||||||
|
}
|
||||||
|
|
||||||
|
void logger.log(`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`);
|
||||||
|
|
||||||
|
const exportFormat = await determineExportFormat();
|
||||||
|
if (!exportFormat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* getAnalysesResults(): AsyncGenerator<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]> {
|
||||||
|
if (!variantAnalysis?.scannedRepos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const repo of variantAnalysis.scannedRepos) {
|
||||||
|
if (repo.resultCount == 0) {
|
||||||
|
yield [repo, {
|
||||||
|
variantAnalysisId: variantAnalysis.id,
|
||||||
|
repositoryId: repo.repository.id,
|
||||||
|
}];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await variantAnalysisManager.loadResults(variantAnalysis.id, repo.repository.fullName, {
|
||||||
|
skipCacheStore: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield [repo, result];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportDirectory = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
|
||||||
|
|
||||||
|
await exportVariantAnalysisAnalysisResults(ctx, exportDirectory, variantAnalysis, getAnalysesResults(), exportFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportVariantAnalysisAnalysisResults(
|
||||||
|
ctx: ExtensionContext,
|
||||||
|
exportDirectory: string,
|
||||||
|
variantAnalysis: VariantAnalysis,
|
||||||
|
analysesResults: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
|
||||||
|
exportFormat: 'gist' | 'local',
|
||||||
|
) {
|
||||||
|
const description = buildVariantAnalysisGistDescription(variantAnalysis);
|
||||||
|
const markdownFiles = await generateVariantAnalysisMarkdown(variantAnalysis, analysesResults, 'gist');
|
||||||
|
|
||||||
|
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the format in which to export the results, from the given export options.
|
||||||
|
*/
|
||||||
|
async function determineExportFormat(): Promise<'gist' | 'local' | undefined> {
|
||||||
const gistOption = {
|
const gistOption = {
|
||||||
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
|
||||||
};
|
};
|
||||||
const localMarkdownOption = {
|
const localMarkdownOption = {
|
||||||
label: '$(markdown) Save as markdown',
|
label: '$(markdown) Save as markdown',
|
||||||
};
|
};
|
||||||
const exportFormat = await determineExportFormat(gistOption, localMarkdownOption);
|
|
||||||
|
|
||||||
if (exportFormat === gistOption) {
|
|
||||||
await exportResultsToGist(ctx, query, analysesResults);
|
|
||||||
} else if (exportFormat === localMarkdownOption) {
|
|
||||||
const queryDirectoryPath = await queryHistoryManager.getQueryHistoryItemDirectory(
|
|
||||||
queryHistoryItem
|
|
||||||
);
|
|
||||||
await exportResultsToLocalMarkdown(queryDirectoryPath, query, analysesResults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the format in which to export the results, from the given export options.
|
|
||||||
*/
|
|
||||||
async function determineExportFormat(
|
|
||||||
...options: { label: string }[]
|
|
||||||
): Promise<QuickPickItem> {
|
|
||||||
const exportFormat = await window.showQuickPick(
|
const exportFormat = await window.showQuickPick(
|
||||||
options,
|
[
|
||||||
|
gistOption,
|
||||||
|
localMarkdownOption,
|
||||||
|
],
|
||||||
{
|
{
|
||||||
placeHolder: 'Select export format',
|
placeHolder: 'Select export format',
|
||||||
canPickMany: false,
|
canPickMany: false,
|
||||||
@@ -84,20 +177,38 @@ async function determineExportFormat(
|
|||||||
if (!exportFormat || !exportFormat.label) {
|
if (!exportFormat || !exportFormat.label) {
|
||||||
throw new UserCancellationException('No export format selected', true);
|
throw new UserCancellationException('No export format selected', true);
|
||||||
}
|
}
|
||||||
return exportFormat;
|
|
||||||
|
if (exportFormat === gistOption) {
|
||||||
|
return 'gist';
|
||||||
|
}
|
||||||
|
if (exportFormat === localMarkdownOption) {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function exportResults(
|
||||||
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
|
|
||||||
*/
|
|
||||||
export async function exportResultsToGist(
|
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
query: RemoteQuery,
|
exportDirectory: string,
|
||||||
analysesResults: AnalysisResults[]
|
description: string,
|
||||||
): Promise<void> {
|
markdownFiles: MarkdownFile[],
|
||||||
|
exportFormat: 'gist' | 'local',
|
||||||
|
) {
|
||||||
|
if (exportFormat === 'gist') {
|
||||||
|
await exportToGist(ctx, description, markdownFiles);
|
||||||
|
} else if (exportFormat === 'local') {
|
||||||
|
await exportToLocalMarkdown(exportDirectory, markdownFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportToGist(
|
||||||
|
ctx: ExtensionContext,
|
||||||
|
description: string,
|
||||||
|
markdownFiles: MarkdownFile[]
|
||||||
|
) {
|
||||||
const credentials = await Credentials.initialize(ctx);
|
const credentials = await Credentials.initialize(ctx);
|
||||||
const description = buildGistDescription(query, analysesResults);
|
|
||||||
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
|
|
||||||
// Convert markdownFiles to the appropriate format for uploading to gist
|
// Convert markdownFiles to the appropriate format for uploading to gist
|
||||||
const gistFiles = markdownFiles.reduce((acc, cur) => {
|
const gistFiles = markdownFiles.reduce((acc, cur) => {
|
||||||
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
|
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
|
||||||
@@ -128,16 +239,25 @@ const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResul
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the results of a remote query to markdown and saves the files locally
|
* Builds Gist description
|
||||||
* in the query directory (where query results and metadata are also saved).
|
* Ex: Empty Block (Go) x results (y repositories)
|
||||||
*/
|
*/
|
||||||
async function exportResultsToLocalMarkdown(
|
const buildVariantAnalysisGistDescription = (variantAnalysis: VariantAnalysis) => {
|
||||||
queryDirectoryPath: string,
|
const resultCount = variantAnalysis.scannedRepos?.reduce((acc, item) => acc + (item.resultCount ?? 0), 0) ?? 0;
|
||||||
query: RemoteQuery,
|
const resultLabel = pluralize(resultCount, 'result', 'results');
|
||||||
analysesResults: AnalysisResults[]
|
|
||||||
|
const repositoryLabel = variantAnalysis.scannedRepos?.length ? `(${pluralize(variantAnalysis.scannedRepos.length, 'repository', 'repositories')})` : '';
|
||||||
|
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the results of an exported query to local markdown files.
|
||||||
|
*/
|
||||||
|
async function exportToLocalMarkdown(
|
||||||
|
exportDirectory: string,
|
||||||
|
markdownFiles: MarkdownFile[],
|
||||||
) {
|
) {
|
||||||
const markdownFiles = generateMarkdown(query, analysesResults, 'local');
|
const exportedResultsPath = path.join(exportDirectory, 'exported-results');
|
||||||
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
|
|
||||||
await fs.ensureDir(exportedResultsPath);
|
await fs.ensureDir(exportedResultsPath);
|
||||||
for (const markdownFile of markdownFiles) {
|
for (const markdownFile of markdownFiles) {
|
||||||
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);
|
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { parseHighlightedLine, shouldHighlightLine } from '../pure/sarif-utils';
|
|||||||
import { convertNonPrintableChars } from '../text-utils';
|
import { convertNonPrintableChars } from '../text-utils';
|
||||||
import { RemoteQuery } from './remote-query';
|
import { RemoteQuery } from './remote-query';
|
||||||
import { AnalysisAlert, AnalysisRawResults, AnalysisResults, CodeSnippet, FileLink, getAnalysisResultCount, HighlightedRegion } from './shared/analysis-result';
|
import { AnalysisAlert, AnalysisRawResults, AnalysisResults, CodeSnippet, FileLink, getAnalysisResultCount, HighlightedRegion } from './shared/analysis-result';
|
||||||
|
import {
|
||||||
|
VariantAnalysis,
|
||||||
|
VariantAnalysisScannedRepository,
|
||||||
|
VariantAnalysisScannedRepositoryResult
|
||||||
|
} from './shared/variant-analysis';
|
||||||
|
|
||||||
export type MarkdownLinkType = 'local' | 'gist';
|
export type MarkdownLinkType = 'local' | 'gist';
|
||||||
|
|
||||||
@@ -57,6 +62,51 @@ export function generateMarkdown(
|
|||||||
return [summaryFile, ...resultsFiles];
|
return [summaryFile, ...resultsFiles];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates markdown files with variant analysis results.
|
||||||
|
*/
|
||||||
|
export async function generateVariantAnalysisMarkdown(
|
||||||
|
variantAnalysis: VariantAnalysis,
|
||||||
|
results: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
|
||||||
|
linkType: MarkdownLinkType
|
||||||
|
): Promise<MarkdownFile[]> {
|
||||||
|
const resultsFiles: MarkdownFile[] = [];
|
||||||
|
// Generate summary file with links to individual files
|
||||||
|
const summaryFile: MarkdownFile = generateVariantAnalysisMarkdownSummary(variantAnalysis);
|
||||||
|
for await (const [scannedRepo, result] of results) {
|
||||||
|
if (scannedRepo.resultCount === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append nwo and results count to the summary table
|
||||||
|
const fullName = scannedRepo.repository.fullName;
|
||||||
|
const fileName = createFileName(fullName);
|
||||||
|
const link = createRelativeLink(fileName, linkType);
|
||||||
|
summaryFile.content.push(`| ${fullName} | [${scannedRepo.resultCount} result(s)](${link}) |`);
|
||||||
|
|
||||||
|
// Generate individual markdown file for each repository
|
||||||
|
const resultsFileContent = [
|
||||||
|
`### ${scannedRepo.repository.fullName}`,
|
||||||
|
''
|
||||||
|
];
|
||||||
|
if (result.interpretedResults) {
|
||||||
|
for (const interpretedResult of result.interpretedResults) {
|
||||||
|
const individualResult = generateMarkdownForInterpretedResult(interpretedResult, variantAnalysis.query.language);
|
||||||
|
resultsFileContent.push(...individualResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.rawResults) {
|
||||||
|
const rawResultTable = generateMarkdownForRawResults(result.rawResults);
|
||||||
|
resultsFileContent.push(...rawResultTable);
|
||||||
|
}
|
||||||
|
resultsFiles.push({
|
||||||
|
fileName: fileName,
|
||||||
|
content: resultsFileContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [summaryFile, ...resultsFiles];
|
||||||
|
}
|
||||||
|
|
||||||
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
// Title
|
// Title
|
||||||
@@ -95,6 +145,44 @@ export function generateMarkdownSummary(query: RemoteQuery): MarkdownFile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateVariantAnalysisMarkdownSummary(variantAnalysis: VariantAnalysis): MarkdownFile {
|
||||||
|
const lines: string[] = [];
|
||||||
|
// Title
|
||||||
|
lines.push(
|
||||||
|
`### Results for "${variantAnalysis.query.name}"`,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expandable section containing query text
|
||||||
|
const queryCodeBlock = [
|
||||||
|
'```ql',
|
||||||
|
...variantAnalysis.query.text.split('\n'),
|
||||||
|
'```',
|
||||||
|
];
|
||||||
|
lines.push(
|
||||||
|
...buildExpandableMarkdownSection('Query', queryCodeBlock)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Padding between sections
|
||||||
|
lines.push(
|
||||||
|
'<br />',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Summary table
|
||||||
|
lines.push(
|
||||||
|
'### Summary',
|
||||||
|
'',
|
||||||
|
'| Repository | Results |',
|
||||||
|
'| --- | --- |',
|
||||||
|
);
|
||||||
|
// nwo and result count will be appended to this table
|
||||||
|
return {
|
||||||
|
fileName: '_summary',
|
||||||
|
content: lines
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function generateMarkdownForInterpretedResult(interpretedResult: AnalysisAlert, language: string): string[] {
|
function generateMarkdownForInterpretedResult(interpretedResult: AnalysisAlert, language: string): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(createMarkdownRemoteFileRef(
|
lines.push(createMarkdownRemoteFileRef(
|
||||||
@@ -296,11 +384,11 @@ export function createMarkdownRemoteFileRef(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an expandable markdown section of the form:
|
* Builds an expandable markdown section of the form:
|
||||||
* <details>
|
* <details>
|
||||||
* <summary>title</summary>
|
* <summary>title</summary>
|
||||||
*
|
*
|
||||||
* contents
|
* contents
|
||||||
*
|
*
|
||||||
* </details>
|
* </details>
|
||||||
*/
|
*/
|
||||||
function buildExpandableMarkdownSection(title: string, contents: string[]): string[] {
|
function buildExpandableMarkdownSection(title: string, contents: string[]): string[] {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class RemoteQueriesView extends AbstractWebview<ToRemoteQueriesMessage, F
|
|||||||
await this.downloadAllAnalysesResults(msg);
|
await this.downloadAllAnalysesResults(msg);
|
||||||
break;
|
break;
|
||||||
case 'remoteQueryExportResults':
|
case 'remoteQueryExportResults':
|
||||||
await commands.executeCommand('codeQL.exportVariantAnalysisResults', msg.queryId);
|
await commands.executeCommand('codeQL.exportRemoteQueryResults', msg.queryId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assertNever(msg);
|
assertNever(msg);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { getErrorMessage } from '../pure/helpers-pure';
|
import { getErrorMessage } from '../pure/helpers-pure';
|
||||||
import { VariantAnalysisView } from './variant-analysis-view';
|
import { VariantAnalysisView } from './variant-analysis-view';
|
||||||
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||||
import { VariantAnalysisResultsManager } from './variant-analysis-results-manager';
|
import { LoadResultsOptions, VariantAnalysisResultsManager } from './variant-analysis-results-manager';
|
||||||
import { getControllerRepo, getQueryName, prepareRemoteQueryRun } from './run-remote-query';
|
import { getControllerRepo, getQueryName, prepareRemoteQueryRun } from './run-remote-query';
|
||||||
import {
|
import {
|
||||||
processUpdatedVariantAnalysis,
|
processUpdatedVariantAnalysis,
|
||||||
@@ -214,13 +214,13 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
|||||||
return this.variantAnalyses.size;
|
return this.variantAnalyses.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadResults(variantAnalysisId: number, repositoryFullName: string): Promise<void> {
|
public async loadResults(variantAnalysisId: number, repositoryFullName: string, options?: LoadResultsOptions): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||||
if (!variantAnalysis) {
|
if (!variantAnalysis) {
|
||||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.variantAnalysisResultsManager.loadResults(variantAnalysisId, this.getVariantAnalysisStorageLocation(variantAnalysisId), repositoryFullName);
|
return this.variantAnalysisResultsManager.loadResults(variantAnalysisId, this.getVariantAnalysisStorageLocation(variantAnalysisId), repositoryFullName, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async variantAnalysisRecordExists(variantAnalysisId: number): Promise<boolean> {
|
private async variantAnalysisRecordExists(variantAnalysisId: number): Promise<boolean> {
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export type ResultDownloadedEvent = {
|
|||||||
repoTask: VariantAnalysisRepositoryTask;
|
repoTask: VariantAnalysisRepositoryTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LoadResultsOptions = {
|
||||||
|
// If true, when results are loaded from storage, they will not be stored in the cache. This reduces memory usage if
|
||||||
|
// results are only needed temporarily (e.g. for exporting results to a different format).
|
||||||
|
skipCacheStore?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class VariantAnalysisResultsManager extends DisposableObject {
|
export class VariantAnalysisResultsManager extends DisposableObject {
|
||||||
private static readonly REPO_TASK_FILENAME = 'repo_task.json';
|
private static readonly REPO_TASK_FILENAME = 'repo_task.json';
|
||||||
private static readonly RESULTS_DIRECTORY = 'results';
|
private static readonly RESULTS_DIRECTORY = 'results';
|
||||||
@@ -86,11 +92,19 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
|||||||
public async loadResults(
|
public async loadResults(
|
||||||
variantAnalysisId: number,
|
variantAnalysisId: number,
|
||||||
variantAnalysisStoragePath: string,
|
variantAnalysisStoragePath: string,
|
||||||
repositoryFullName: string
|
repositoryFullName: string,
|
||||||
|
options?: LoadResultsOptions,
|
||||||
): Promise<VariantAnalysisScannedRepositoryResult> {
|
): Promise<VariantAnalysisScannedRepositoryResult> {
|
||||||
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
|
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repositoryFullName));
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
if (options?.skipCacheStore) {
|
||||||
|
return this.loadResultsFromStorage(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadResultsIntoMemory(variantAnalysisId, variantAnalysisStoragePath, repositoryFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadResultsIntoMemory(
|
private async loadResultsIntoMemory(
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
|||||||
case 'copyRepositoryList':
|
case 'copyRepositoryList':
|
||||||
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId, msg.filterSort);
|
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId, msg.filterSort);
|
||||||
break;
|
break;
|
||||||
|
case 'exportResults':
|
||||||
|
void commands.executeCommand('codeQL.exportVariantAnalysisResults', this.variantAnalysisId);
|
||||||
|
break;
|
||||||
case 'openLogs':
|
case 'openLogs':
|
||||||
await this.openLogs();
|
await this.openLogs();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ const stopQuery = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportResults = () => {
|
||||||
|
vscode.postMessage({
|
||||||
|
t: 'exportResults',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const openLogs = () => {
|
const openLogs = () => {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
t: 'openLogs',
|
t: 'openLogs',
|
||||||
@@ -110,7 +116,7 @@ export function VariantAnalysis({
|
|||||||
onViewQueryTextClick={openQueryText}
|
onViewQueryTextClick={openQueryText}
|
||||||
onStopQueryClick={stopQuery}
|
onStopQueryClick={stopQuery}
|
||||||
onCopyRepositoryListClick={copyRepositoryList}
|
onCopyRepositoryListClick={copyRepositoryList}
|
||||||
onExportResultsClick={() => console.log('Export results')}
|
onExportResultsClick={exportResults}
|
||||||
onViewLogsClick={openLogs}
|
onViewLogsClick={openLogs}
|
||||||
/>
|
/>
|
||||||
<VariantAnalysisOutcomePanels
|
<VariantAnalysisOutcomePanels
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ describe('db panel', async () => {
|
|||||||
globalStoragePath,
|
globalStoragePath,
|
||||||
workspaceStoragePath
|
workspaceStoragePath
|
||||||
});
|
});
|
||||||
|
await fs.ensureDir(workspaceStoragePath);
|
||||||
|
|
||||||
const app = new ExtensionApp(extensionContext);
|
const app = new ExtensionApp(extensionContext);
|
||||||
|
|
||||||
dbConfigStore = new DbConfigStore(app);
|
dbConfigStore = new DbConfigStore(app);
|
||||||
dbManager = new DbManager(dbConfigStore);
|
dbManager = new DbManager(app, dbConfigStore);
|
||||||
|
|
||||||
// Create a modified version of the DbPanel module that allows
|
// Create a modified version of the DbPanel module that allows
|
||||||
// us to override the creation of the DbTreeDataProvider
|
// us to override the creation of the DbTreeDataProvider
|
||||||
@@ -63,14 +65,16 @@ describe('db panel', async () => {
|
|||||||
|
|
||||||
it('should render default local and remote nodes when the config is empty', async () => {
|
it('should render default local and remote nodes when the config is empty', async () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [],
|
repositoryLists: [],
|
||||||
repositories: []
|
owners: [],
|
||||||
},
|
repositories: []
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,30 +113,32 @@ describe('db panel', async () => {
|
|||||||
|
|
||||||
it('should render remote repository list nodes', async () => {
|
it('should render remote repository list nodes', async () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [
|
remote: {
|
||||||
{
|
repositoryLists: [
|
||||||
name: 'my-list-1',
|
{
|
||||||
repositories: [
|
name: 'my-list-1',
|
||||||
'owner1/repo1',
|
repositories: [
|
||||||
'owner1/repo2'
|
'owner1/repo1',
|
||||||
]
|
'owner1/repo2'
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
name: 'my-list-2',
|
{
|
||||||
repositories: [
|
name: 'my-list-2',
|
||||||
'owner1/repo1',
|
repositories: [
|
||||||
'owner2/repo1',
|
'owner1/repo1',
|
||||||
'owner2/repo2'
|
'owner2/repo1',
|
||||||
]
|
'owner2/repo2'
|
||||||
},
|
]
|
||||||
],
|
},
|
||||||
owners: [],
|
],
|
||||||
repositories: []
|
owners: [],
|
||||||
},
|
repositories: []
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,14 +167,16 @@ describe('db panel', async () => {
|
|||||||
|
|
||||||
it('should render owner list nodes', async () => {
|
it('should render owner list nodes', async () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: ['owner1', 'owner2'],
|
repositoryLists: [],
|
||||||
repositories: []
|
owners: ['owner1', 'owner2'],
|
||||||
},
|
repositories: []
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,14 +202,16 @@ describe('db panel', async () => {
|
|||||||
|
|
||||||
it('should render repository nodes', async () => {
|
it('should render repository nodes', async () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [],
|
repositoryLists: [],
|
||||||
repositories: ['owner1/repo1', 'owner1/repo2']
|
owners: [],
|
||||||
},
|
repositories: ['owner1/repo1', 'owner1/repo2']
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,8 +238,10 @@ describe('db panel', async () => {
|
|||||||
async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
|
async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
|
||||||
await fs.writeJson(dbConfigFilePath, dbConfig);
|
await fs.writeJson(dbConfigFilePath, dbConfig);
|
||||||
|
|
||||||
// Once we have watching of the db config, this can happen
|
// Ideally we would just initialise the db config store at the start
|
||||||
// at the start of the test.
|
// of each test and then rely on the file watcher to update the config.
|
||||||
|
// However, this requires adding sleep to the tests to allow for the
|
||||||
|
// file watcher to catch up, so we instead initialise the config store here.
|
||||||
await dbConfigStore.initialize();
|
await dbConfigStore.initialize();
|
||||||
dbTreeDataProvider = new DbTreeDataProvider(dbManager);
|
dbTreeDataProvider = new DbTreeDataProvider(dbManager);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -495,6 +495,96 @@ describe('query-history', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleCopyRepoList', () => {
|
||||||
|
it('should not call a command for a local query', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||||
|
|
||||||
|
const item = localQueryHistory[4];
|
||||||
|
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||||
|
|
||||||
|
expect(executeCommandSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy repo list for a single remote query', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item = remoteQueryHistory[1];
|
||||||
|
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||||
|
expect(executeCommandSpy).to.have.been.calledWith('codeQL.copyRepoList', item.queryId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not copy repo list for multiple remote queries', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item1 = remoteQueryHistory[1];
|
||||||
|
const item2 = remoteQueryHistory[3];
|
||||||
|
await queryHistoryManager.handleCopyRepoList(item1, [item1, item2]);
|
||||||
|
expect(executeCommandSpy).not.to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy repo list for a single variant analysis', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item = variantAnalysisHistory[1];
|
||||||
|
await queryHistoryManager.handleCopyRepoList(item, [item]);
|
||||||
|
expect(executeCommandSpy).to.have.been.calledWith('codeQL.copyVariantAnalysisRepoList', item.variantAnalysis.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not copy repo list for multiple variant analyses', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item1 = variantAnalysisHistory[1];
|
||||||
|
const item2 = variantAnalysisHistory[3];
|
||||||
|
await queryHistoryManager.handleCopyRepoList(item1, [item1, item2]);
|
||||||
|
expect(executeCommandSpy).not.to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleExportResults', () => {
|
||||||
|
it('should not call a command for a local query', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||||
|
|
||||||
|
const item = localQueryHistory[4];
|
||||||
|
await queryHistoryManager.handleExportResults(item, [item]);
|
||||||
|
|
||||||
|
expect(executeCommandSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export results for a single remote query', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item = remoteQueryHistory[1];
|
||||||
|
await queryHistoryManager.handleExportResults(item, [item]);
|
||||||
|
expect(executeCommandSpy).to.have.been.calledWith('codeQL.exportRemoteQueryResults', item.queryId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not export results for multiple remote queries', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item1 = remoteQueryHistory[1];
|
||||||
|
const item2 = remoteQueryHistory[3];
|
||||||
|
await queryHistoryManager.handleExportResults(item1, [item1, item2]);
|
||||||
|
expect(executeCommandSpy).not.to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export results for a single variant analysis', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item = variantAnalysisHistory[1];
|
||||||
|
await queryHistoryManager.handleExportResults(item, [item]);
|
||||||
|
expect(executeCommandSpy).to.have.been.calledWith('codeQL.exportVariantAnalysisResults', item.variantAnalysis.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not export results for multiple variant analyses', async () => {
|
||||||
|
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||||
|
|
||||||
|
const item1 = variantAnalysisHistory[1];
|
||||||
|
const item2 = variantAnalysisHistory[3];
|
||||||
|
await queryHistoryManager.handleExportResults(item1, [item1, item2]);
|
||||||
|
expect(executeCommandSpy).not.to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('determineSelection', () => {
|
describe('determineSelection', () => {
|
||||||
const singleItem = 'a';
|
const singleItem = 'a';
|
||||||
const multipleItems = ['b', 'c', 'd'];
|
const multipleItems = ['b', 'c', 'd'];
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { createMockExtensionContext } from '../index';
|
|||||||
import { Credentials } from '../../../authentication';
|
import { Credentials } from '../../../authentication';
|
||||||
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
import { MarkdownFile } from '../../../remote-queries/remote-queries-markdown-generation';
|
||||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||||
import { exportResultsToGist } from '../../../remote-queries/export-results';
|
import { exportRemoteQueryAnalysisResults } from '../../../remote-queries/export-results';
|
||||||
|
|
||||||
const proxyquire = pq.noPreserveCache();
|
const proxyquire = pq.noPreserveCache();
|
||||||
|
|
||||||
describe('export results', async function() {
|
describe('export results', async function() {
|
||||||
describe('exportResultsToGist', async function() {
|
describe('exportRemoteQueryAnalysisResults', async function() {
|
||||||
let sandbox: sinon.SinonSandbox;
|
let sandbox: sinon.SinonSandbox;
|
||||||
let mockCredentials: Credentials;
|
let mockCredentials: Credentials;
|
||||||
let mockResponse: sinon.SinonStub<any, Promise<{ status: number }>>;
|
let mockResponse: sinon.SinonStub<any, Promise<{ status: number }>>;
|
||||||
@@ -47,7 +47,7 @@ describe('export results', async function() {
|
|||||||
const query = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/query.json'), 'utf8'));
|
const query = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/query.json'), 'utf8'));
|
||||||
const analysesResults = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/analyses-results.json'), 'utf8'));
|
const analysesResults = JSON.parse(await fs.readFile(path.join(__dirname, '../data/remote-queries/query-with-results/analyses-results.json'), 'utf8'));
|
||||||
|
|
||||||
await exportResultsToGist(ctx, query, analysesResults);
|
await exportRemoteQueryAnalysisResults(ctx, '', query, analysesResults, 'gist');
|
||||||
|
|
||||||
expect(mockCreateGist.calledOnce).to.be.true;
|
expect(mockCreateGist.calledOnce).to.be.true;
|
||||||
expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)');
|
expect(mockCreateGist.firstCall.args[1]).to.equal('Shell command built from environment values (javascript) 3 results (10 repositories)');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { App } from '../../src/common/app';
|
import { App, AppMode } from '../../src/common/app';
|
||||||
import { AppEvent, AppEventEmitter } from '../../src/common/events';
|
import { AppEvent, AppEventEmitter } from '../../src/common/events';
|
||||||
import { Disposable } from '../../src/pure/disposable-object';
|
import { Disposable } from '../../src/pure/disposable-object';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export function createMockApp({
|
|||||||
extensionPath = '/mock/extension/path',
|
extensionPath = '/mock/extension/path',
|
||||||
workspaceStoragePath = '/mock/workspace/storage/path',
|
workspaceStoragePath = '/mock/workspace/storage/path',
|
||||||
globalStoragePath = '/mock/global/storage/path',
|
globalStoragePath = '/mock/global/storage/path',
|
||||||
createEventEmitter = <T>() => new MockAppEventEmitter<T>()
|
createEventEmitter = <T>() => new MockAppEventEmitter<T>(),
|
||||||
}: {
|
}: {
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
workspaceStoragePath?: string,
|
workspaceStoragePath?: string,
|
||||||
@@ -14,10 +14,12 @@ export function createMockApp({
|
|||||||
createEventEmitter?: <T>() => AppEventEmitter<T>
|
createEventEmitter?: <T>() => AppEventEmitter<T>
|
||||||
}): App {
|
}): App {
|
||||||
return {
|
return {
|
||||||
createEventEmitter,
|
mode: AppMode.Test,
|
||||||
|
subscriptions: [],
|
||||||
extensionPath,
|
extensionPath,
|
||||||
workspaceStoragePath,
|
workspaceStoragePath,
|
||||||
globalStoragePath
|
globalStoragePath,
|
||||||
|
createEventEmitter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"databases": {
|
||||||
|
"remote": {
|
||||||
|
"repositoryLists": [],
|
||||||
|
"owners": [],
|
||||||
|
"repositories": []
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"lists": [],
|
||||||
|
"databases": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,52 @@
|
|||||||
{
|
{
|
||||||
"remote": {
|
"databases": {
|
||||||
"repositoryLists": [
|
"remote": {
|
||||||
{
|
"repositoryLists": [
|
||||||
"name": "repoList1",
|
{
|
||||||
"repositories": ["foo/bar", "foo/baz"]
|
"name": "repoList1",
|
||||||
}
|
"repositories": ["foo/bar", "foo/baz"]
|
||||||
],
|
}
|
||||||
"owners": [],
|
],
|
||||||
"repositories": ["owner/repo1", "owner/repo2", "owner/repo3"]
|
"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/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"local": {
|
"selected": {
|
||||||
"lists": [
|
"kind": "configDefined",
|
||||||
{
|
"value": "path.to.database"
|
||||||
"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/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ describe('db config store', async () => {
|
|||||||
await configStore.initialize();
|
await configStore.initialize();
|
||||||
|
|
||||||
expect(await fs.pathExists(configPath)).to.be.true;
|
expect(await fs.pathExists(configPath)).to.be.true;
|
||||||
|
|
||||||
const config = configStore.getConfig().value;
|
const config = configStore.getConfig().value;
|
||||||
expect(config.remote.repositoryLists).to.be.empty;
|
expect(config.databases.remote.repositoryLists).to.be.empty;
|
||||||
expect(config.remote.owners).to.be.empty;
|
expect(config.databases.remote.owners).to.be.empty;
|
||||||
expect(config.remote.repositories).to.be.empty;
|
expect(config.databases.remote.repositories).to.be.empty;
|
||||||
expect(config.local.lists).to.be.empty;
|
expect(config.databases.local.lists).to.be.empty;
|
||||||
expect(config.local.databases).to.be.empty;
|
expect(config.databases.local.databases).to.be.empty;
|
||||||
|
expect(config.selected).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load an existing config', async () => {
|
it('should load an existing config', async () => {
|
||||||
@@ -46,20 +48,20 @@ describe('db config store', async () => {
|
|||||||
await configStore.initialize();
|
await configStore.initialize();
|
||||||
|
|
||||||
const config = configStore.getConfig().value;
|
const config = configStore.getConfig().value;
|
||||||
expect(config.remote.repositoryLists).to.have.length(1);
|
expect(config.databases.remote.repositoryLists).to.have.length(1);
|
||||||
expect(config.remote.repositoryLists[0]).to.deep.equal({
|
expect(config.databases.remote.repositoryLists[0]).to.deep.equal({
|
||||||
name: 'repoList1',
|
name: 'repoList1',
|
||||||
repositories: ['foo/bar', 'foo/baz']
|
repositories: ['foo/bar', 'foo/baz']
|
||||||
});
|
});
|
||||||
expect(config.remote.owners).to.be.empty;
|
expect(config.databases.remote.owners).to.be.empty;
|
||||||
expect(config.remote.repositories).to.have.length(3);
|
expect(config.databases.remote.repositories).to.have.length(3);
|
||||||
expect(config.remote.repositories).to.deep.equal([
|
expect(config.databases.remote.repositories).to.deep.equal([
|
||||||
'owner/repo1',
|
'owner/repo1',
|
||||||
'owner/repo2',
|
'owner/repo2',
|
||||||
'owner/repo3',
|
'owner/repo3',
|
||||||
]);
|
]);
|
||||||
expect(config.local.lists).to.have.length(2);
|
expect(config.databases.local.lists).to.have.length(2);
|
||||||
expect(config.local.lists[0]).to.deep.equal({
|
expect(config.databases.local.lists[0]).to.deep.equal({
|
||||||
name: 'localList1',
|
name: 'localList1',
|
||||||
databases: [
|
databases: [
|
||||||
{
|
{
|
||||||
@@ -70,13 +72,32 @@ describe('db config store', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(config.local.databases).to.have.length(1);
|
expect(config.databases.local.databases).to.have.length(1);
|
||||||
expect(config.local.databases[0]).to.deep.equal({
|
expect(config.databases.local.databases[0]).to.deep.equal({
|
||||||
name: 'example-db',
|
name: 'example-db',
|
||||||
dateAdded: 1668096927267,
|
dateAdded: 1668096927267,
|
||||||
language: 'ruby',
|
language: 'ruby',
|
||||||
storagePath: '/path/to/database/',
|
storagePath: '/path/to/database/',
|
||||||
});
|
});
|
||||||
|
expect(config.selected).to.deep.equal({
|
||||||
|
kind: 'configDefined',
|
||||||
|
value: 'path.to.database',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load an existing config without selected db', async () => {
|
||||||
|
const testDataStoragePathWithout = path.join(__dirname, 'data', 'without-selected');
|
||||||
|
|
||||||
|
const app = createMockApp({
|
||||||
|
extensionPath,
|
||||||
|
workspaceStoragePath: testDataStoragePathWithout
|
||||||
|
});
|
||||||
|
|
||||||
|
const configStore = new DbConfigStore(app);
|
||||||
|
await configStore.initialize();
|
||||||
|
|
||||||
|
const config = configStore.getConfig().value;
|
||||||
|
expect(config.selected).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow modification of the config', async () => {
|
it('should not allow modification of the config', async () => {
|
||||||
@@ -88,9 +109,9 @@ describe('db config store', async () => {
|
|||||||
await configStore.initialize();
|
await configStore.initialize();
|
||||||
|
|
||||||
const config = configStore.getConfig().value;
|
const config = configStore.getConfig().value;
|
||||||
config.remote.repositoryLists = [];
|
config.databases.remote.repositoryLists = [];
|
||||||
|
|
||||||
const reRetrievedConfig = configStore.getConfig().value;
|
const reRetrievedConfig = configStore.getConfig().value;
|
||||||
expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1);
|
expect(reRetrievedConfig.databases.remote.repositoryLists).to.have.length(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ describe('db config validation', async () => {
|
|||||||
// We're intentionally bypassing the type check because we'd
|
// We're intentionally bypassing the type check because we'd
|
||||||
// like to make sure validation errors are highlighted.
|
// like to make sure validation errors are highlighted.
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
'remote': {
|
'databases': {
|
||||||
'repositoryLists': [
|
'remote': {
|
||||||
{
|
'repositoryLists': [
|
||||||
'name': 'repoList1',
|
{
|
||||||
'repositories': ['foo/bar', 'foo/baz']
|
'name': 'repoList1',
|
||||||
}
|
'repositories': ['foo/bar', 'foo/baz']
|
||||||
],
|
}
|
||||||
'repositories': ['owner/repo1', 'owner/repo2', 'owner/repo3'],
|
],
|
||||||
'somethingElse': 'bar'
|
'repositories': ['owner/repo1', 'owner/repo2', 'owner/repo3'],
|
||||||
|
'somethingElse': 'bar'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as any as DbConfig;
|
} as any as DbConfig;
|
||||||
|
|
||||||
@@ -27,8 +29,8 @@ describe('db config validation', async () => {
|
|||||||
|
|
||||||
expect(validationOutput).to.have.length(3);
|
expect(validationOutput).to.have.length(3);
|
||||||
|
|
||||||
expect(validationOutput[0]).to.deep.equal(' must have required property \'local\'');
|
expect(validationOutput[0]).to.deep.equal('/databases must have required property \'local\'');
|
||||||
expect(validationOutput[1]).to.deep.equal('/remote must have required property \'owners\'');
|
expect(validationOutput[1]).to.deep.equal('/databases/remote must have required property \'owners\'');
|
||||||
expect(validationOutput[2]).to.deep.equal('/remote must NOT have additional properties');
|
expect(validationOutput[2]).to.deep.equal('/databases/remote must NOT have additional properties');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ describe('db tree creator', () => {
|
|||||||
describe('createRemoteTree', () => {
|
describe('createRemoteTree', () => {
|
||||||
it('should build root node and system defined lists', () => {
|
it('should build root node and system defined lists', () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [],
|
repositoryLists: [],
|
||||||
repositories: []
|
owners: [],
|
||||||
|
repositories: []
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
local: {
|
|
||||||
lists: [],
|
|
||||||
databases: []
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||||
@@ -46,32 +48,34 @@ describe('db tree creator', () => {
|
|||||||
|
|
||||||
it('should create remote user defined list nodes', () => {
|
it('should create remote user defined list nodes', () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [
|
remote: {
|
||||||
{
|
repositoryLists: [
|
||||||
name: 'my-list-1',
|
{
|
||||||
repositories: [
|
name: 'my-list-1',
|
||||||
'owner1/repo1',
|
repositories: [
|
||||||
'owner1/repo2',
|
'owner1/repo1',
|
||||||
'owner2/repo1'
|
'owner1/repo2',
|
||||||
]
|
'owner2/repo1'
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
name: 'my-list-2',
|
{
|
||||||
repositories: [
|
name: 'my-list-2',
|
||||||
'owner3/repo1',
|
repositories: [
|
||||||
'owner3/repo2',
|
'owner3/repo1',
|
||||||
'owner4/repo1'
|
'owner3/repo2',
|
||||||
]
|
'owner4/repo1'
|
||||||
}
|
]
|
||||||
],
|
}
|
||||||
owners: [],
|
],
|
||||||
repositories: []
|
owners: [],
|
||||||
|
repositories: []
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
local: {
|
|
||||||
lists: [],
|
|
||||||
databases: []
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbTreeRoot = createRemoteTree(dbConfig);
|
const dbTreeRoot = createRemoteTree(dbConfig);
|
||||||
@@ -85,16 +89,16 @@ describe('db tree creator', () => {
|
|||||||
expect(repositoryListNodes.length).to.equal(2);
|
expect(repositoryListNodes.length).to.equal(2);
|
||||||
expect(repositoryListNodes[0]).to.deep.equal({
|
expect(repositoryListNodes[0]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteUserDefinedList,
|
kind: DbItemKind.RemoteUserDefinedList,
|
||||||
listName: dbConfig.remote.repositoryLists[0].name,
|
listName: dbConfig.databases.remote.repositoryLists[0].name,
|
||||||
repos: dbConfig.remote.repositoryLists[0].repositories.map((repo) => ({
|
repos: dbConfig.databases.remote.repositoryLists[0].repositories.map((repo) => ({
|
||||||
kind: DbItemKind.RemoteRepo,
|
kind: DbItemKind.RemoteRepo,
|
||||||
repoFullName: repo
|
repoFullName: repo
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
expect(repositoryListNodes[1]).to.deep.equal({
|
expect(repositoryListNodes[1]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteUserDefinedList,
|
kind: DbItemKind.RemoteUserDefinedList,
|
||||||
listName: dbConfig.remote.repositoryLists[1].name,
|
listName: dbConfig.databases.remote.repositoryLists[1].name,
|
||||||
repos: dbConfig.remote.repositoryLists[1].repositories.map((repo) => ({
|
repos: dbConfig.databases.remote.repositoryLists[1].repositories.map((repo) => ({
|
||||||
kind: DbItemKind.RemoteRepo,
|
kind: DbItemKind.RemoteRepo,
|
||||||
repoFullName: repo
|
repoFullName: repo
|
||||||
}))
|
}))
|
||||||
@@ -103,17 +107,19 @@ describe('db tree creator', () => {
|
|||||||
|
|
||||||
it('should create remote owner nodes', () => {
|
it('should create remote owner nodes', () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [
|
repositoryLists: [],
|
||||||
'owner1',
|
owners: [
|
||||||
'owner2'
|
'owner1',
|
||||||
],
|
'owner2'
|
||||||
repositories: []
|
],
|
||||||
},
|
repositories: []
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,28 +134,30 @@ describe('db tree creator', () => {
|
|||||||
expect(ownerNodes.length).to.equal(2);
|
expect(ownerNodes.length).to.equal(2);
|
||||||
expect(ownerNodes[0]).to.deep.equal({
|
expect(ownerNodes[0]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteOwner,
|
kind: DbItemKind.RemoteOwner,
|
||||||
ownerName: dbConfig.remote.owners[0]
|
ownerName: dbConfig.databases.remote.owners[0]
|
||||||
});
|
});
|
||||||
expect(ownerNodes[1]).to.deep.equal({
|
expect(ownerNodes[1]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteOwner,
|
kind: DbItemKind.RemoteOwner,
|
||||||
ownerName: dbConfig.remote.owners[1]
|
ownerName: dbConfig.databases.remote.owners[1]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create remote repo nodes', () => {
|
it('should create remote repo nodes', () => {
|
||||||
const dbConfig: DbConfig = {
|
const dbConfig: DbConfig = {
|
||||||
remote: {
|
databases: {
|
||||||
repositoryLists: [],
|
remote: {
|
||||||
owners: [],
|
repositoryLists: [],
|
||||||
repositories: [
|
owners: [],
|
||||||
'owner1/repo1',
|
repositories: [
|
||||||
'owner1/repo2',
|
'owner1/repo1',
|
||||||
'owner2/repo1'
|
'owner1/repo2',
|
||||||
]
|
'owner2/repo1'
|
||||||
},
|
]
|
||||||
local: {
|
},
|
||||||
lists: [],
|
local: {
|
||||||
databases: []
|
lists: [],
|
||||||
|
databases: []
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,15 +172,15 @@ describe('db tree creator', () => {
|
|||||||
expect(repoNodes.length).to.equal(3);
|
expect(repoNodes.length).to.equal(3);
|
||||||
expect(repoNodes[0]).to.deep.equal({
|
expect(repoNodes[0]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteRepo,
|
kind: DbItemKind.RemoteRepo,
|
||||||
repoFullName: dbConfig.remote.repositories[0]
|
repoFullName: dbConfig.databases.remote.repositories[0]
|
||||||
});
|
});
|
||||||
expect(repoNodes[1]).to.deep.equal({
|
expect(repoNodes[1]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteRepo,
|
kind: DbItemKind.RemoteRepo,
|
||||||
repoFullName: dbConfig.remote.repositories[1]
|
repoFullName: dbConfig.databases.remote.repositories[1]
|
||||||
});
|
});
|
||||||
expect(repoNodes[2]).to.deep.equal({
|
expect(repoNodes[2]).to.deep.equal({
|
||||||
kind: DbItemKind.RemoteRepo,
|
kind: DbItemKind.RemoteRepo,
|
||||||
repoFullName: dbConfig.remote.repositories[2]
|
repoFullName: dbConfig.databases.remote.repositories[2]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,112 +4,138 @@
|
|||||||
"$schema": {
|
"$schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"remote": {
|
"databases": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"repositoryLists": {
|
"remote": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"properties": {
|
||||||
"type": "object",
|
"repositoryLists": {
|
||||||
"properties": {
|
"type": "array",
|
||||||
"name": {
|
"items": {
|
||||||
"type": "string"
|
"type": "object",
|
||||||
},
|
"properties": {
|
||||||
"repositories": {
|
"name": {
|
||||||
"type": "array",
|
"type": "string"
|
||||||
"items": {
|
},
|
||||||
"type": "string",
|
"repositories": {
|
||||||
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
"type": "array",
|
||||||
}
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "repositories"],
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "repositories"],
|
"owners": {
|
||||||
"additionalProperties": false
|
"type": "array",
|
||||||
}
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9-_\\.]+$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repositories": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["repositoryLists", "owners", "repositories"],
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"owners": {
|
"local": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"properties": {
|
||||||
"type": "string",
|
"lists": {
|
||||||
"pattern": "^[a-zA-Z0-9-_\\.]+$"
|
"type": "array",
|
||||||
}
|
"items": {
|
||||||
},
|
"type": "object",
|
||||||
"repositories": {
|
"properties": {
|
||||||
"type": "array",
|
"name": {
|
||||||
"items": {
|
"type": "string"
|
||||||
"type": "string",
|
},
|
||||||
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
"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": ["repositoryLists", "owners", "repositories"],
|
"required": ["remote", "local"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"local": {
|
"selected": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"lists": {
|
"kind": {
|
||||||
"type": "array",
|
"type": "string",
|
||||||
"items": {
|
"enum": ["configDefined", "remoteSystemDefinedList"]
|
||||||
"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": {
|
"value": {
|
||||||
"type": "array",
|
"type": "string"
|
||||||
"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"],
|
"required": ["kind", "value"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["remote", "local"],
|
"required": ["databases"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user