Merge pull request #1290 from github/aeisenberg/remote-history-label-editing
Allow remote query items to have their labels edited
This commit is contained in:
@@ -224,7 +224,7 @@
|
||||
},
|
||||
"codeQL.queryHistory.format": {
|
||||
"type": "string",
|
||||
"default": "%q on %d - %s, %r result count [%t]",
|
||||
"default": "%q on %d - %s, %r [%t]",
|
||||
"markdownDescription": "Default string for how to label query history items.\n* %t is the time of the query\n* %q is the human-readable query name\n* %f is the query file name\n* %d is the database name\n* %r is the number of results\n* %s is a status string"
|
||||
},
|
||||
"codeQL.queryHistory.ttl": {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli
|
||||
import resultsDiff from './resultsDiff';
|
||||
import { CompletedLocalQueryInfo } from '../query-results';
|
||||
import { getErrorMessage } from '../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../history-item-label-provider';
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
@@ -39,6 +40,7 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
private databaseManager: DatabaseManager,
|
||||
private cliServer: CodeQLCliServer,
|
||||
private logger: Logger,
|
||||
private labelProvider: HistoryItemLabelProvider,
|
||||
private showQueryResultsCallback: (
|
||||
item: CompletedLocalQueryInfo
|
||||
) => Promise<void>
|
||||
@@ -81,12 +83,12 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
// since we split the description into several rows
|
||||
// only run interpolation if the label is user-defined
|
||||
// otherwise we will wind up with duplicated rows
|
||||
name: from.getShortLabel(),
|
||||
name: this.labelProvider.getShortLabel(from),
|
||||
status: from.completedQuery.statusString,
|
||||
time: from.startTime,
|
||||
},
|
||||
toQuery: {
|
||||
name: to.getShortLabel(),
|
||||
name: this.labelProvider.getShortLabel(to),
|
||||
status: to.completedQuery.statusString,
|
||||
time: to.startTime,
|
||||
},
|
||||
|
||||
@@ -96,6 +96,7 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -447,6 +448,7 @@ async function activateWithInstalledDistribution(
|
||||
showResultsForCompletedQuery(item, WebviewReveal.Forced);
|
||||
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
|
||||
await fs.ensureDir(queryStorageDir);
|
||||
const labelProvider = new HistoryItemLabelProvider(queryHistoryConfigurationListener);
|
||||
|
||||
void logger.log('Initializing query history.');
|
||||
const qhm = new QueryHistoryManager(
|
||||
@@ -455,6 +457,7 @@ async function activateWithInstalledDistribution(
|
||||
queryStorageDir,
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
labelProvider,
|
||||
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
|
||||
showResultsForComparison(from, to),
|
||||
);
|
||||
@@ -466,8 +469,9 @@ async function activateWithInstalledDistribution(
|
||||
});
|
||||
|
||||
ctx.subscriptions.push(qhm);
|
||||
|
||||
void logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger, labelProvider);
|
||||
ctx.subscriptions.push(intm);
|
||||
|
||||
void logger.log('Initializing compare panel interface.');
|
||||
@@ -476,6 +480,7 @@ async function activateWithInstalledDistribution(
|
||||
dbm,
|
||||
cliServer,
|
||||
queryServerLogger,
|
||||
labelProvider,
|
||||
showResults
|
||||
);
|
||||
ctx.subscriptions.push(cmpm);
|
||||
@@ -525,7 +530,7 @@ async function activateWithInstalledDistribution(
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
|
||||
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
|
||||
const item = new LocalQueryInfo(initialInfo, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
|
||||
|
||||
82
extensions/ql-vscode/src/history-item-label-provider.ts
Normal file
82
extensions/ql-vscode/src/history-item-label-provider.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { env } from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
|
||||
interface InterpolateReplacements {
|
||||
t: string; // Start time
|
||||
q: string; // Query name
|
||||
d: string; // Database/Controller repo name
|
||||
r: string; // Result count/Empty
|
||||
s: string; // Status
|
||||
f: string; // Query file name
|
||||
'%': '%'; // Percent sign
|
||||
}
|
||||
|
||||
export class HistoryItemLabelProvider {
|
||||
constructor(private config: QueryHistoryConfig) {
|
||||
/**/
|
||||
}
|
||||
|
||||
getLabel(item: QueryHistoryInfo) {
|
||||
const replacements = item.t === 'local'
|
||||
? this.getLocalInterpolateReplacements(item)
|
||||
: this.getRemoteInterpolateReplacements(item);
|
||||
|
||||
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
|
||||
|
||||
return this.interpolate(rawLabel, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a user-specified label for this query, interpolate and use that.
|
||||
* Otherwise, use the raw name of this query.
|
||||
*
|
||||
* @returns the name of the query, unless there is a custom label for this query.
|
||||
*/
|
||||
getShortLabel(item: QueryHistoryInfo): string {
|
||||
return item.userSpecifiedLabel
|
||||
? this.getLabel(item)
|
||||
: item.t === 'local'
|
||||
? item.getQueryName()
|
||||
: item.remoteQuery.queryName;
|
||||
}
|
||||
|
||||
|
||||
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
|
||||
return rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
|
||||
const { resultCount = 0, statusString = 'in progress' } = item.completedQuery || {};
|
||||
return {
|
||||
t: item.startTime,
|
||||
q: item.getQueryName(),
|
||||
d: item.initialInfo.databaseInfo.name,
|
||||
r: `${resultCount} results`,
|
||||
s: statusString,
|
||||
f: item.getQueryFileName(),
|
||||
'%': '%',
|
||||
};
|
||||
}
|
||||
|
||||
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
|
||||
return {
|
||||
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
|
||||
q: item.remoteQuery.queryName,
|
||||
|
||||
// There is no database name for remote queries. Instead use the controller repository name.
|
||||
d: `${item.remoteQuery.controllerRepository.owner}/${item.remoteQuery.controllerRepository.name}`,
|
||||
|
||||
// There is no synchronous way to get the results count.
|
||||
r: '',
|
||||
s: item.status,
|
||||
f: path.basename(item.remoteQuery.queryFilePath),
|
||||
'%': '%'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-type
|
||||
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
|
||||
import { PAGE_SIZE } from './config';
|
||||
import { CompletedLocalQueryInfo } from './query-results';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -136,7 +137,8 @@ export class InterfaceManager extends DisposableObject {
|
||||
public ctx: vscode.ExtensionContext,
|
||||
private databaseManager: DatabaseManager,
|
||||
public cliServer: CodeQLCliServer,
|
||||
public logger: Logger
|
||||
public logger: Logger,
|
||||
private labelProvider: HistoryItemLabelProvider
|
||||
) {
|
||||
super();
|
||||
this.push(this._diagnosticCollection);
|
||||
@@ -416,7 +418,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// more asynchronous message to not so abruptly interrupt
|
||||
// user's workflow by immediately revealing the panel.
|
||||
const showButton = 'View Results';
|
||||
const queryName = fullQuery.getShortLabel();
|
||||
const queryName = this.labelProvider.getShortLabel(fullQuery);
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
|
||||
}.`,
|
||||
@@ -483,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
database: fullQuery.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
metadata: fullQuery.completedQuery.query.metadata,
|
||||
queryName: fullQuery.label,
|
||||
queryName: this.labelProvider.getLabel(fullQuery),
|
||||
queryPath: fullQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
@@ -516,7 +518,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
resultSetNames,
|
||||
pageSize: interpretedPageSize(this._interpretation),
|
||||
numPages: numInterpretedPages(this._interpretation),
|
||||
queryName: this._displayedQuery.label,
|
||||
queryName: this.labelProvider.getLabel(this._displayedQuery),
|
||||
queryPath: this._displayedQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
@@ -601,7 +603,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
database: results.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.completedQuery.query.metadata,
|
||||
queryName: results.label,
|
||||
queryName: this.labelProvider.getLabel(results),
|
||||
queryPath: results.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { QueryStatus } from './query-status';
|
||||
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
|
||||
import * as fs from 'fs-extra';
|
||||
import { CliVersionConstraint } from './cli';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
import { Credentials } from './authentication';
|
||||
import { cancelRemoteQuery } from './remote-queries/gh-actions-api-client';
|
||||
|
||||
@@ -123,7 +124,10 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
|
||||
private current: QueryHistoryInfo | undefined;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
constructor(
|
||||
extensionPath: string,
|
||||
private readonly labelProvider: HistoryItemLabelProvider,
|
||||
) {
|
||||
super();
|
||||
this.failedIconPath = path.join(
|
||||
extensionPath,
|
||||
@@ -140,13 +144,13 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
}
|
||||
|
||||
async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
|
||||
const treeItem = new TreeItem(element.label);
|
||||
const treeItem = new TreeItem(this.labelProvider.getLabel(element));
|
||||
|
||||
treeItem.command = {
|
||||
title: 'Query History Item',
|
||||
command: 'codeQLQueryHistory.itemClicked',
|
||||
arguments: [element],
|
||||
tooltip: element.failureReason || element.label
|
||||
tooltip: element.failureReason || this.labelProvider.getLabel(element)
|
||||
};
|
||||
|
||||
// Populate the icon and the context value. We use the context value to
|
||||
@@ -185,8 +189,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
): ProviderResult<QueryHistoryInfo[]> {
|
||||
return element ? [] : this.history.sort((h1, h2) => {
|
||||
|
||||
const h1Label = h1.label.toLowerCase();
|
||||
const h2Label = h2.label.toLowerCase();
|
||||
const h1Label = this.labelProvider.getLabel(h1).toLowerCase();
|
||||
const h2Label = this.labelProvider.getLabel(h2).toLowerCase();
|
||||
|
||||
const h1Date = h1.t === 'local'
|
||||
? h1.initialInfo.start.getTime()
|
||||
@@ -315,12 +319,13 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
._onWillOpenQueryItem.event;
|
||||
|
||||
constructor(
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
private queryStorageDir: string,
|
||||
private ctx: ExtensionContext,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private doCompareCallback: (
|
||||
private readonly qs: QueryServerClient,
|
||||
private readonly dbm: DatabaseManager,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private readonly labelProvider: HistoryItemLabelProvider,
|
||||
private readonly doCompareCallback: (
|
||||
from: CompletedLocalQueryInfo,
|
||||
to: CompletedLocalQueryInfo
|
||||
) => Promise<void>
|
||||
@@ -334,7 +339,8 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.queryMetadataStorageLocation = path.join((ctx.storageUri || ctx.globalStorageUri).fsPath, WORKSPACE_QUERY_HISTORY_FILE);
|
||||
|
||||
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
|
||||
ctx.extensionPath
|
||||
ctx.extensionPath,
|
||||
this.labelProvider
|
||||
));
|
||||
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
|
||||
treeDataProvider: this.treeDataProvider,
|
||||
@@ -537,7 +543,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
|
||||
async readQueryHistory(): Promise<void> {
|
||||
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
|
||||
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
|
||||
const history = await slurpQueryHistory(this.queryMetadataStorageLocation);
|
||||
this.treeDataProvider.allHistory = history;
|
||||
this.treeDataProvider.allHistory.forEach((item) => {
|
||||
this._onDidAddQueryItem.fire(item);
|
||||
@@ -605,7 +611,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
// Remote queries can be removed locally, but not remotely.
|
||||
// The user must cancel the query on GitHub Actions explicitly.
|
||||
this.treeDataProvider.remove(item);
|
||||
void logger.log(`Deleted ${item.label}.`);
|
||||
void logger.log(`Deleted ${this.labelProvider.getLabel(item)}.`);
|
||||
if (item.status === QueryStatus.InProgress) {
|
||||
void logger.log('The variant analysis is still running on GitHub Actions. To cancel there, you must go to the workflow run in your browser.');
|
||||
}
|
||||
@@ -652,20 +658,20 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
): Promise<void> {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
// TODO will support remote queries
|
||||
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
|
||||
if (!this.assertSingleQuery(finalMultiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await window.showInputBox({
|
||||
prompt: 'Label:',
|
||||
placeHolder: '(use default)',
|
||||
value: finalSingleItem.label,
|
||||
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
|
||||
value: finalSingleItem.userSpecifiedLabel ?? '',
|
||||
title: 'Set query label',
|
||||
prompt: 'Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.',
|
||||
});
|
||||
// undefined response means the user cancelled the dialog; don't change anything
|
||||
if (response !== undefined) {
|
||||
// Interpret empty string response as 'go back to using default'
|
||||
finalSingleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
|
||||
finalSingleItem.userSpecifiedLabel = response === '' ? undefined : response;
|
||||
await this.refreshTreeView();
|
||||
}
|
||||
}
|
||||
@@ -895,7 +901,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
query.resultsPaths.interpretedResultsPath
|
||||
);
|
||||
} else {
|
||||
const label = finalSingleItem.label;
|
||||
const label = this.labelProvider.getLabel(finalSingleItem);
|
||||
void showAndLogInformationMessage(
|
||||
`Query ${label} has no interpreted results.`
|
||||
);
|
||||
@@ -1084,7 +1090,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
otherQuery.initialInfo.databaseInfo.name === dbName
|
||||
)
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
label: this.labelProvider.getLabel(item),
|
||||
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
|
||||
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
|
||||
query: item as CompletedLocalQueryInfo,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
SarifInterpretationData,
|
||||
GraphInterpretationData
|
||||
} from './pure/interface-types';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { DatabaseInfo } from './pure/interface-types';
|
||||
import { QueryStatus } from './query-status';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
@@ -218,7 +217,6 @@ export class LocalQueryInfo {
|
||||
public completedQuery: CompletedQueryInfo | undefined;
|
||||
public evalLogLocation: string | undefined;
|
||||
public evalLogSummaryLocation: string | undefined;
|
||||
private config: QueryHistoryConfig | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
|
||||
@@ -226,11 +224,8 @@ export class LocalQueryInfo {
|
||||
*/
|
||||
constructor(
|
||||
public readonly initialInfo: InitialQueryInfo,
|
||||
config: QueryHistoryConfig,
|
||||
private cancellationSource?: CancellationTokenSource // used to cancel in progress queries
|
||||
) {
|
||||
this.setConfig(config);
|
||||
}
|
||||
) { /**/ }
|
||||
|
||||
cancel() {
|
||||
this.cancellationSource?.cancel();
|
||||
@@ -243,43 +238,12 @@ export class LocalQueryInfo {
|
||||
return this.initialInfo.start.toLocaleString(env.language);
|
||||
}
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { resultCount = 0, statusString = 'in progress' } = this.completedQuery || {};
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: this.startTime,
|
||||
q: this.getQueryName(),
|
||||
d: this.initialInfo.databaseInfo.name,
|
||||
r: resultCount.toString(),
|
||||
s: statusString,
|
||||
f: this.getQueryFileName(),
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
get userSpecifiedLabel() {
|
||||
return this.initialInfo.userSpecifiedLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a label for this query that includes interpolated values.
|
||||
*/
|
||||
get label(): string {
|
||||
return this.interpolate(
|
||||
this.initialInfo.userSpecifiedLabel ?? this.config?.format ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoids getting the default label for the query.
|
||||
* If there is a custom label for this query, interpolate and use that.
|
||||
* Otherwise, use the name of the query.
|
||||
*
|
||||
* @returns the name of the query, unless there is a custom label for this query.
|
||||
*/
|
||||
getShortLabel(): string {
|
||||
return this.initialInfo.userSpecifiedLabel
|
||||
? this.interpolate(this.initialInfo.userSpecifiedLabel)
|
||||
: this.getQueryName();
|
||||
set userSpecifiedLabel(label: string | undefined) {
|
||||
this.initialInfo.userSpecifiedLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,21 +306,4 @@ export class LocalQueryInfo {
|
||||
return QueryStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `config` property must not be serialized since it contains a listerner
|
||||
* for global configuration changes. Instead, It should be set when the query
|
||||
* is deserialized.
|
||||
*
|
||||
* @param config the global query history config object
|
||||
*/
|
||||
setConfig(config: QueryHistoryConfig) {
|
||||
// avoid serializing config property
|
||||
Object.defineProperty(this, 'config', {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
value: config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { asyncFilter, getErrorMessage, getErrorStack } from './pure/helpers-pure';
|
||||
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
|
||||
import { QueryEvaluationInfo } from './run-queries';
|
||||
|
||||
export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConfig): Promise<QueryHistoryInfo[]> {
|
||||
export async function slurpQueryHistory(fsPath: string): Promise<QueryHistoryInfo[]> {
|
||||
try {
|
||||
if (!(await fs.pathExists(fsPath))) {
|
||||
return [];
|
||||
@@ -29,10 +28,6 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
|
||||
if (q.t === 'local') {
|
||||
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
|
||||
|
||||
// The config object is a global, se we need to set it explicitly
|
||||
// and ensure it is not serialized to JSON.
|
||||
q.setConfig(config);
|
||||
|
||||
// Date instances are serialized as strings. Need to
|
||||
// convert them back to Date instances.
|
||||
(q.initialInfo as any).start = new Date(q.initialInfo.start);
|
||||
|
||||
@@ -110,7 +110,6 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
status: QueryStatus.InProgress,
|
||||
completed: false,
|
||||
queryId,
|
||||
label: query.queryName,
|
||||
remoteQuery: query,
|
||||
};
|
||||
await this.prepareStorageDirectory(queryHistoryItem);
|
||||
@@ -151,7 +150,7 @@ export class RemoteQueriesManager extends DisposableObject {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.label}`);
|
||||
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.remoteQuery.queryName}`);
|
||||
queryItem.status = QueryStatus.Failed;
|
||||
}
|
||||
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
|
||||
|
||||
@@ -10,6 +10,6 @@ export interface RemoteQueryHistoryItem {
|
||||
status: QueryStatus;
|
||||
completed: boolean;
|
||||
readonly queryId: string,
|
||||
label: string; // TODO, the query label should have interpolation like local queries
|
||||
remoteQuery: RemoteQuery;
|
||||
userSpecifiedLabel?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { env } from 'vscode';
|
||||
import { expect } from 'chai';
|
||||
import { QueryHistoryConfig } from '../../config';
|
||||
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
|
||||
import { CompletedLocalQueryInfo, CompletedQueryInfo, InitialQueryInfo } from '../../query-results';
|
||||
import { RemoteQueryHistoryItem } from '../../remote-queries/remote-query-history-item';
|
||||
|
||||
|
||||
describe('HistoryItemLabelProvider', () => {
|
||||
|
||||
let labelProvider: HistoryItemLabelProvider;
|
||||
let config: QueryHistoryConfig;
|
||||
const date = new Date('2022-01-01T00:00:00.000Z');
|
||||
const dateStr = date.toLocaleString(env.language);
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
format: 'xxx %q xxx'
|
||||
} as unknown as QueryHistoryConfig;
|
||||
labelProvider = new HistoryItemLabelProvider(config);
|
||||
});
|
||||
|
||||
describe('local queries', () => {
|
||||
it('should interpolate query when user specified', () => {
|
||||
const fqi = createMockLocalQueryInfo('xxx');
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
|
||||
|
||||
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
|
||||
|
||||
fqi.userSpecifiedLabel = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
|
||||
});
|
||||
|
||||
it('should interpolate query when not user specified', () => {
|
||||
const fqi = createMockLocalQueryInfo();
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
|
||||
|
||||
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %`);
|
||||
|
||||
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name db-name in progress query-file.ql 456 results %::${dateStr} query-name db-name in progress query-file.ql 456 results %`);
|
||||
});
|
||||
|
||||
it('should get query short label', () => {
|
||||
const fqi = createMockLocalQueryInfo('xxx');
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
delete (fqi as any).userSpecifiedLabel;
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
|
||||
});
|
||||
|
||||
function createMockLocalQueryInfo(userSpecifiedLabel?: string) {
|
||||
return {
|
||||
t: 'local',
|
||||
userSpecifiedLabel,
|
||||
startTime: date.toLocaleString(env.language),
|
||||
getQueryFileName() {
|
||||
return 'query-file.ql';
|
||||
},
|
||||
getQueryName() {
|
||||
return 'query-name';
|
||||
},
|
||||
initialInfo: {
|
||||
databaseInfo: {
|
||||
databaseUri: 'unused',
|
||||
name: 'db-name'
|
||||
}
|
||||
} as unknown as InitialQueryInfo,
|
||||
completedQuery: {
|
||||
resultCount: 456,
|
||||
statusString: 'in progress',
|
||||
} as unknown as CompletedQueryInfo,
|
||||
} as unknown as CompletedLocalQueryInfo;
|
||||
}
|
||||
});
|
||||
|
||||
describe('remote queries', () => {
|
||||
it('should interpolate query when user specified', () => {
|
||||
const fqi = createMockRemoteQueryInfo('xxx');
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx');
|
||||
|
||||
fqi.userSpecifiedLabel = '%t %q %d %s %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
|
||||
|
||||
fqi.userSpecifiedLabel = '%t %q %d %s %%::%t %q %d %s %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress %::${dateStr} query-name github/vscode-codeql-integration-tests in progress %`);
|
||||
});
|
||||
|
||||
it('should interpolate query when not user specified', () => {
|
||||
const fqi = createMockRemoteQueryInfo();
|
||||
|
||||
expect(labelProvider.getLabel(fqi)).to.eq('xxx query-name xxx');
|
||||
|
||||
|
||||
config.format = '%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
|
||||
|
||||
config.format = '%t %q %d %s %f %r %%::%t %q %d %s %f %r %%';
|
||||
expect(labelProvider.getLabel(fqi)).to.eq(`${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %::${dateStr} query-name github/vscode-codeql-integration-tests in progress query-file.ql %`);
|
||||
});
|
||||
|
||||
it('should get query short label', () => {
|
||||
const fqi = createMockRemoteQueryInfo('xxx');
|
||||
|
||||
// fall back on user specified if one exists.
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('xxx');
|
||||
|
||||
// use query name if no user-specified label exists
|
||||
delete (fqi as any).userSpecifiedLabel;
|
||||
expect(labelProvider.getShortLabel(fqi)).to.eq('query-name');
|
||||
});
|
||||
|
||||
function createMockRemoteQueryInfo(userSpecifiedLabel?: string) {
|
||||
return {
|
||||
t: 'remote',
|
||||
userSpecifiedLabel,
|
||||
remoteQuery: {
|
||||
executionStartTime: date.getTime(),
|
||||
queryName: 'query-name',
|
||||
queryFilePath: 'query-file.ql',
|
||||
controllerRepository: {
|
||||
owner: 'github',
|
||||
name: 'vscode-codeql-integration-tests'
|
||||
}
|
||||
},
|
||||
status: 'in progress',
|
||||
} as unknown as RemoteQueryHistoryItem;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { logger } from '../../logging';
|
||||
import { registerQueryHistoryScubber } from '../../query-history-scrubber';
|
||||
import { QueryHistoryManager, HistoryTreeDataProvider, SortOrder } from '../../query-history';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
|
||||
import { QueryHistoryConfigListener } from '../../config';
|
||||
import { QueryHistoryConfig, QueryHistoryConfigListener } from '../../config';
|
||||
import * as messages from '../../pure/messages';
|
||||
import { QueryServerClient } from '../../queryserver-client';
|
||||
import { LocalQueryInfo, InitialQueryInfo } from '../../query-results';
|
||||
@@ -17,6 +17,7 @@ import * as tmp from 'tmp-promise';
|
||||
import { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, THREE_HOURS_IN_MS } from '../../pure/helpers-pure';
|
||||
import { tmpDir } from '../../helpers';
|
||||
import { getErrorMessage } from '../../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../../history-item-label-provider';
|
||||
|
||||
describe('query-history', () => {
|
||||
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
|
||||
@@ -308,8 +309,12 @@ describe('query-history', () => {
|
||||
|
||||
describe('HistoryTreeDataProvider', () => {
|
||||
let historyTreeDataProvider: HistoryTreeDataProvider;
|
||||
let labelProvider: HistoryItemLabelProvider;
|
||||
beforeEach(() => {
|
||||
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file(mockExtensionLocation).fsPath);
|
||||
labelProvider = new HistoryItemLabelProvider({
|
||||
/**/
|
||||
} as QueryHistoryConfig);
|
||||
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file(mockExtensionLocation).fsPath, labelProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -324,7 +329,7 @@ describe('query-history', () => {
|
||||
title: 'Query History Item',
|
||||
command: 'codeQLQueryHistory.itemClicked',
|
||||
arguments: [mockQuery],
|
||||
tooltip: mockQuery.label,
|
||||
tooltip: labelProvider.getLabel(mockQuery),
|
||||
});
|
||||
expect(treeItem.label).to.contain('hucairz');
|
||||
expect(treeItem.contextValue).to.eq('rawResultsItem');
|
||||
@@ -507,9 +512,17 @@ describe('query-history', () => {
|
||||
function item(label: string, start: number, t = 'local', resultCount?: number) {
|
||||
if (t === 'local') {
|
||||
return {
|
||||
label,
|
||||
getQueryName() {
|
||||
return label;
|
||||
},
|
||||
getQueryFileName() {
|
||||
return label + '.ql';
|
||||
},
|
||||
initialInfo: {
|
||||
start: new Date(start),
|
||||
databaseInfo: {
|
||||
name: 'test',
|
||||
}
|
||||
},
|
||||
completedQuery: {
|
||||
resultCount,
|
||||
@@ -518,9 +531,16 @@ describe('query-history', () => {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label,
|
||||
status: 'success',
|
||||
remoteQuery: {
|
||||
queryFilePath: label + '.ql',
|
||||
queryName: label,
|
||||
executionStartTime: start,
|
||||
controllerRepository: {
|
||||
name: 'test',
|
||||
owner: 'user',
|
||||
},
|
||||
repositories: []
|
||||
},
|
||||
t
|
||||
};
|
||||
@@ -535,7 +555,6 @@ describe('query-history', () => {
|
||||
start: new Date(),
|
||||
queryPath: 'hucairz'
|
||||
} as InitialQueryInfo,
|
||||
configListener,
|
||||
{
|
||||
dispose: () => { /**/ },
|
||||
} as vscode.CancellationTokenSource
|
||||
@@ -749,6 +768,7 @@ describe('query-history', () => {
|
||||
extensionPath: vscode.Uri.file('/x/y/z').fsPath,
|
||||
} as vscode.ExtensionContext,
|
||||
configListener,
|
||||
new HistoryItemLabelProvider({} as QueryHistoryConfig),
|
||||
doCompareCallback
|
||||
);
|
||||
qhm.onWillOpenQueryItem(selectedCallback);
|
||||
|
||||
@@ -4,18 +4,15 @@ import * as fs from 'fs-extra';
|
||||
import * as sinon from 'sinon';
|
||||
import { LocalQueryInfo, InitialQueryInfo, interpretResultsSarif } from '../../query-results';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
|
||||
import { QueryHistoryConfig } from '../../config';
|
||||
import { EvaluationResult, QueryResultType } from '../../pure/messages';
|
||||
import { DatabaseInfo, SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
|
||||
import { CodeQLCliServer, SourceInfo } from '../../cli';
|
||||
import { CancellationTokenSource, Uri, env } from 'vscode';
|
||||
import { CancellationTokenSource, Uri } from 'vscode';
|
||||
import { tmpDir } from '../../helpers';
|
||||
import { slurpQueryHistory, splatQueryHistory } from '../../query-serialization';
|
||||
|
||||
describe('query-results', () => {
|
||||
let disposeSpy: sinon.SinonSpy;
|
||||
let onDidChangeQueryHistoryConfigurationSpy: sinon.SinonSpy;
|
||||
let mockConfig: QueryHistoryConfig;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let queryPath: string;
|
||||
let cnt = 0;
|
||||
@@ -23,8 +20,6 @@ describe('query-results', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
disposeSpy = sandbox.spy();
|
||||
onDidChangeQueryHistoryConfigurationSpy = sandbox.spy();
|
||||
mockConfig = mockQueryHistoryConfig();
|
||||
queryPath = path.join(Uri.file(tmpDir.name).fsPath, `query-${cnt++}`);
|
||||
});
|
||||
|
||||
@@ -33,17 +28,6 @@ describe('query-results', () => {
|
||||
});
|
||||
|
||||
describe('FullQueryInfo', () => {
|
||||
it('should interpolate', () => {
|
||||
const fqi = createMockFullQueryInfo();
|
||||
const date = new Date('2022-01-01T00:00:00.000Z');
|
||||
const dateStr = date.toLocaleString(env.language);
|
||||
(fqi.initialInfo as any).start = date;
|
||||
|
||||
expect(fqi.interpolate('xxx')).to.eq('xxx');
|
||||
expect(fqi.interpolate('%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %`);
|
||||
expect(fqi.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %::${dateStr} hucairz a in progress %`);
|
||||
});
|
||||
|
||||
it('should get the query name', () => {
|
||||
const fqi = createMockFullQueryInfo();
|
||||
|
||||
@@ -83,23 +67,6 @@ describe('query-results', () => {
|
||||
expect(fqi.getQueryFileName()).to.eq('yz:1');
|
||||
});
|
||||
|
||||
it('should get the label', () => {
|
||||
const fqi = createMockFullQueryInfo('db-name');
|
||||
|
||||
// the %q from the config is now replaced by the file name of the query
|
||||
expect(fqi.label).to.eq('from config hucairz');
|
||||
|
||||
// the %q from the config is now replaced by the name of the query
|
||||
// in the metadata
|
||||
fqi.completeThisQuery(createMockQueryWithResults(queryPath));
|
||||
expect(fqi.label).to.eq('from config vwx');
|
||||
|
||||
// replace the config with a user specified label
|
||||
// must be interpolated
|
||||
fqi.initialInfo.userSpecifiedLabel = 'user specified label %d';
|
||||
expect(fqi.label).to.eq('user specified label db-name');
|
||||
});
|
||||
|
||||
it('should get the getResultsPath', () => {
|
||||
const query = createMockQueryWithResults(queryPath);
|
||||
const fqi = createMockFullQueryInfo('a', query);
|
||||
@@ -283,7 +250,7 @@ describe('query-results', () => {
|
||||
|
||||
// splat and slurp
|
||||
await splatQueryHistory(allHistory, allHistoryPath);
|
||||
const allHistoryActual = await slurpQueryHistory(allHistoryPath, mockConfig);
|
||||
const allHistoryActual = await slurpQueryHistory(allHistoryPath);
|
||||
|
||||
// the dispose methods will be different. Ignore them.
|
||||
allHistoryActual.forEach(info => {
|
||||
@@ -325,7 +292,7 @@ describe('query-results', () => {
|
||||
queries: allHistory
|
||||
}), 'utf8');
|
||||
|
||||
const allHistoryActual = await slurpQueryHistory(badPath, mockConfig);
|
||||
const allHistoryActual = await slurpQueryHistory(badPath);
|
||||
// version number is invalid. Should return an empty array.
|
||||
expect(allHistoryActual).to.deep.eq([]);
|
||||
});
|
||||
@@ -386,7 +353,6 @@ describe('query-results', () => {
|
||||
isQuickEval: false,
|
||||
id: `some-id-${dbName}`,
|
||||
} as InitialQueryInfo,
|
||||
mockQueryHistoryConfig(),
|
||||
{
|
||||
dispose: () => { /**/ },
|
||||
} as CancellationTokenSource
|
||||
@@ -400,12 +366,4 @@ describe('query-results', () => {
|
||||
}
|
||||
return fqi;
|
||||
}
|
||||
|
||||
function mockQueryHistoryConfig(): QueryHistoryConfig {
|
||||
return {
|
||||
onDidChangeConfiguration: onDidChangeQueryHistoryConfigurationSpy,
|
||||
ttlInMillis: 999999,
|
||||
format: 'from config %q'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DisposableBucket } from '../../disposable-bucket';
|
||||
import { testDisposeHandler } from '../../test-dispose-handler';
|
||||
import { walkDirectory } from '../../../helpers';
|
||||
import { getErrorMessage } from '../../../pure/helpers-pure';
|
||||
import { HistoryItemLabelProvider } from '../../../history-item-label-provider';
|
||||
|
||||
/**
|
||||
* Tests for remote queries and how they interact with the query history manager.
|
||||
@@ -71,6 +72,7 @@ describe('Remote queries and query history manager', function() {
|
||||
{
|
||||
onDidChangeConfiguration: () => new DisposableBucket(),
|
||||
} as unknown as QueryHistoryConfig,
|
||||
new HistoryItemLabelProvider({} as QueryHistoryConfig),
|
||||
asyncNoop
|
||||
);
|
||||
disposables.push(qhm);
|
||||
|
||||
Reference in New Issue
Block a user