Commit 59f5aa7b authored by Orin Jaworski's avatar Orin Jaworski Committed by Commit Bot

[omnibox] Add batch export capability to chrome://omnibox

This CL makes it possible to use chrome://omnibox as a batch query
processing tool, transforming an input set of queries to an output
set of exports.  This is currently done by detecting the type of
import data, but the functionality can easily be wired into dedicated
UI elements if we want it to be more discoverable.

This CL also fixes a bug in generated export filenames: it was
including extra spaces due to code formatting, but now the spaces
are removed and the date/time text is shortened to the essentials.

Bug: 964528
Change-Id: I09036af82aaf9c4ade9f7dc929c338c85159e77d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1614711
Commit-Queue: Orin Jaworski <orinj@chromium.org>
Reviewed-by: default avatarmanuk hovanesian <manukh@chromium.org>
Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661120}
parent 72191aff
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
...@@ -181,21 +181,29 @@ ...@@ -181,21 +181,29 @@
</span> </span>
</div> </div>
<div class="buttons-column"> <div class="buttons-column">
<label id="import-clipboard" <label id="import-clipboard"
class="row icon-button button drag-container" accesskey="v" class="row icon-button button drag-container" accesskey="v"
tabindex="0" tabindex="0"
title="Import JSON from clipboard. Accepts dragged text and files as well."> title="Import JSON from clipboard. Accepts dragged text and files as well.">
<i class="icon copy-icon"></i> <i class="icon copy-icon"></i>
<span>Paste [<span class="accesskey">v</span>]</span> <span>Paste [<span class="accesskey">v</span>]</span>
</label> </label>
<label id="import-file" <label id="import-file"
class="row icon-button button drag-container" accesskey="m" class="row icon-button button drag-container" accesskey="m"
tabindex="0" tabindex="0"
title="Upload previously downloaded responses. Accepts dragged text and files as well."> title="Upload previously downloaded responses to load them. Accepts dragged text and files as well.">
<input id="import-file-input" type="file" accept=".json"> <input id="import-file-input" type="file" accept=".json">
<i class="icon copy-icon"></i> <i class="icon copy-icon"></i>
<span>Upload [<span class="accesskey">m</span>]</span> <span>Upload [<span class="accesskey">m</span>]</span>
</label> </label>
<label id="process-batch"
class="row icon-button button drag-container" accesskey="b"
tabindex="0"
title="Upload a query batch specifier to download a batch export. Accepts dragged text and files as well.">
<input id="process-batch-input" type="file" accept=".json">
<i class="icon copy-icon"></i>
<span>Process <span class="accesskey">b</span>atch</span>
</label>
</div> </div>
</div> </div>
<div id="imported-warning" class="row" hidden> <div id="imported-warning" class="row" hidden>
......
...@@ -27,6 +27,14 @@ ...@@ -27,6 +27,14 @@
*/ */
let Request; let Request;
/**
* @typedef {{
* batchMode: string,
* batchQueryInputs: Array<QueryInputs>,
* }}
*/
let BatchSpecifier;
/** /**
* @typedef {{ * @typedef {{
* queryInputs: QueryInputs, * queryInputs: QueryInputs,
...@@ -154,6 +162,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -154,6 +162,8 @@ document.addEventListener('DOMContentLoaded', () => {
omniboxInput.addEventListener( omniboxInput.addEventListener(
'filter-input-changed', e => omniboxOutput.updateFilterText(e.detail)); 'filter-input-changed', e => omniboxOutput.updateFilterText(e.detail));
omniboxInput.addEventListener('import', e => exportDelegate.import(e.detail)); omniboxInput.addEventListener('import', e => exportDelegate.import(e.detail));
omniboxInput.addEventListener(
'process-batch', e => exportDelegate.processBatchData(e.detail));
omniboxInput.addEventListener( omniboxInput.addEventListener(
'export-clipboard', () => exportDelegate.exportClipboard()); 'export-clipboard', () => exportDelegate.exportClipboard());
omniboxInput.addEventListener( omniboxInput.addEventListener(
...@@ -178,16 +188,83 @@ class ExportDelegate { ...@@ -178,16 +188,83 @@ class ExportDelegate {
this.omniboxOutput_ = omniboxOutput; this.omniboxOutput_ = omniboxOutput;
} }
/** @param {OmniboxExport} importData */ /**
* Import a single data item previously exported.
* @param {OmniboxExport} importData
* @return {boolean} true if a single data item was imported for viewing;
* false if import failed.
*/
import(importData) { import(importData) {
if (!validateImportData_(importData)) { if (!validateImportData_(importData)) {
return; // TODO(manukh): Make use of this return value to fix the UI state
// bug in omnibox_input.js -- see the related TODO there.
return false;
} }
this.omniboxInput_.queryInputs = importData.queryInputs; this.omniboxInput_.queryInputs = importData.queryInputs;
this.omniboxInput_.displayInputs = importData.displayInputs; this.omniboxInput_.displayInputs = importData.displayInputs;
this.omniboxOutput_.updateQueryInputs(importData.queryInputs); this.omniboxOutput_.updateQueryInputs(importData.queryInputs);
this.omniboxOutput_.updateDisplayInputs(importData.displayInputs); this.omniboxOutput_.updateDisplayInputs(importData.displayInputs);
this.omniboxOutput_.setResponsesHistory(importData.responsesHistory); this.omniboxOutput_.setResponsesHistory(importData.responsesHistory);
return true;
}
/**
* This is the worker function that transforms query inputs to accumulate
* batch exports, then finally initiates a download for the complete set.
* @param {!Array<!QueryInputs>} batchQueryInputs
*/
async processBatch(batchQueryInputs) {
const batchExports = [];
for (const queryInputs of batchQueryInputs) {
const omniboxResponse = await browserProxy
.makeRequest(
queryInputs.inputText, queryInputs.resetAutocompleteController,
queryInputs.cursorPosition, queryInputs.zeroSuggest,
queryInputs.preventInlineAutocomplete, queryInputs.preferKeyword,
queryInputs.currentUrl, queryInputs.pageClassification, false);
const exportData = {
queryInputs,
// TODO(orinj|manukh): Make the schema consistent and remove
// the extra level of array nesting. [[This]] is done for now
// so that elements can be extracted in the form import expects.
responsesHistory: [[omniboxResponse]],
displayInputs: this.omniboxInput_.displayInputs,
};
batchExports.push(exportData);
}
const fileName = `omnibox_batch_${ExportDelegate.getTimeStamp()}.json`;
const batchData = { appVersion: navigator.appVersion, batchExports };
ExportDelegate.download_(batchData, fileName);
}
/**
* Event handler for uploaded batch processing specifier data, kicks off
* the processBatch asynchronous pipeline.
* @param {!BatchSpecifier} processBatchData
*/
processBatchData(processBatchData) {
if (processBatchData.batchMode && processBatchData.batchQueryInputs) {
this.processBatch(processBatchData.batchQueryInputs);
} else {
const expected = {
batchMode: "combined",
batchQueryInputs: [
{
inputText: "example input text",
cursorPosition: 18,
resetAutocompleteController: false,
cursorLock: false,
zeroSuggest: false,
preventInlineAutocomplete: false,
preferKeyword: false,
currentUrl: "",
pageClassification: "4"
}
],
};
console.error(`Invalid batch specifier data. Expected format: \n${
JSON.stringify(expected, null, 2)}`);
}
} }
exportClipboard() { exportClipboard() {
...@@ -197,8 +274,9 @@ class ExportDelegate { ...@@ -197,8 +274,9 @@ class ExportDelegate {
exportFile() { exportFile() {
const exportData = this.exportData_; const exportData = this.exportData_;
const fileName = `omnibox_debug_export_${exportData.queryInputs.inputText}_ const timeStamp = ExportDelegate.getTimeStamp();
${new Date().toISOString()}.json`; const fileName =
`omnibox_debug_export_${exportData.queryInputs.inputText}_${timeStamp}.json`;
ExportDelegate.download_(exportData, fileName); ExportDelegate.download_(exportData, fileName);
} }
...@@ -225,6 +303,12 @@ class ExportDelegate { ...@@ -225,6 +303,12 @@ class ExportDelegate {
a.download = fileName; a.download = fileName;
a.click(); a.click();
} }
/** @return {string} A sortable timestamp string for use in filenames. */
static getTimeStamp() {
const iso = new Date().toISOString();
return iso.replace(/:/g, '').split('.')[0];
}
} }
/** /**
......
...@@ -84,10 +84,15 @@ class OmniboxInput extends OmniboxElement { ...@@ -84,10 +84,15 @@ class OmniboxInput extends OmniboxElement {
.addEventListener('click', this.onImportClipboard_.bind(this)); .addEventListener('click', this.onImportClipboard_.bind(this));
this.$$('#import-file-input') this.$$('#import-file-input')
.addEventListener('input', this.onImportFile_.bind(this)); .addEventListener('input', this.onImportFile_.bind(this));
this.$$('#process-batch-input')
.addEventListener('input', this.onProcessBatchFile_.bind(this));
['#import-clipboard', '#import-file'].forEach(query => { ['#import-clipboard', '#import-file'].forEach(query => {
this.setupDragListeners_(this.$$(query)); this.setupDragListeners_(this.$$(query));
this.$$(query).addEventListener('drop', this.onImportDropped_.bind(this)); this.$$(query).addEventListener('drop', this.onImportDropped_.bind(this));
}); });
this.setupDragListeners_(this.$$('#process-batch'));
this.$$('#process-batch')
.addEventListener('drop', this.onProcessBatchDropped_.bind(this));
} }
/** /**
...@@ -251,6 +256,11 @@ class OmniboxInput extends OmniboxElement { ...@@ -251,6 +256,11 @@ class OmniboxInput extends OmniboxElement {
this.importFile_(event.target.files[0]); this.importFile_(event.target.files[0]);
} }
/** @private @param {!Event} event */
onProcessBatchFile_(event) {
this.processBatchFile_(event.target.files[0]);
}
/** @private @param {!Event} event */ /** @private @param {!Event} event */
onImportDropped_(event) { onImportDropped_(event) {
const dragText = event.dataTransfer.getData('Text'); const dragText = event.dataTransfer.getData('Text');
...@@ -261,23 +271,31 @@ class OmniboxInput extends OmniboxElement { ...@@ -261,23 +271,31 @@ class OmniboxInput extends OmniboxElement {
} }
} }
/** @private @param {!Event} event */
onProcessBatchDropped_(event) {
const dragText = event.dataTransfer.getData('Text');
if (dragText) {
this.processBatch_(dragText);
} else if (event.dataTransfer.files[0]) {
this.processBatchFile_(event.dataTransfer.files[0]);
}
}
/** @private @param {!File} file */ /** @private @param {!File} file */
importFile_(file) { importFile_(file) {
const reader = new FileReader(); OmniboxInput.readFile_(file).then(this.import_.bind(this));
reader.onloadend = () => { }
if (reader.readyState === FileReader.DONE) {
this.import_(/** @type {string} */ (reader.result)); /** @private @param {!File} file */
} else { processBatchFile_(file) {
console.error('error importing, unable to read file:', reader.error); OmniboxInput.readFile_(file).then(this.processBatch_.bind(this));
}
};
reader.readAsText(file);
} }
/** @private @param {string} importString */ /** @private @param {string} importString */
import_(importString) { import_(importString) {
try { try {
const importData = JSON.parse(importString); const importData = JSON.parse(importString);
// TODO(manukh): If import fails, this UI state change shouldn't happen.
this.$$('#imported-warning').hidden = false; this.$$('#imported-warning').hidden = false;
this.dispatchEvent(new CustomEvent('import', {detail: importData})); this.dispatchEvent(new CustomEvent('import', {detail: importData}));
} catch (error) { } catch (error) {
...@@ -285,6 +303,32 @@ class OmniboxInput extends OmniboxElement { ...@@ -285,6 +303,32 @@ class OmniboxInput extends OmniboxElement {
} }
} }
/** @private @param {string} processBatchString */
processBatch_(processBatchString) {
try {
const processBatchData = JSON.parse(processBatchString);
this.dispatchEvent(
new CustomEvent('process-batch', {detail: processBatchData}));
} catch (error) {
console.error('error during process batch, invalid json:', error);
}
}
/** @private @param {!File} file */
static readFile_(file) {
return new Promise(resolve => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.readyState === FileReader.DONE) {
resolve(/** @type {string} */(reader.result));
} else {
console.error('error importing, unable to read file:', reader.error);
}
};
reader.readAsText(file);
});
}
/** @return {DisplayInputs} */ /** @return {DisplayInputs} */
static get defaultDisplayInputs() { static get defaultDisplayInputs() {
return { return {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment