Added support for importing from fusion tables.
git-svn-id: http://google-refine.googlecode.com/svn/trunk@2239 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
parent
ebede9b424
commit
0693205430
@ -0,0 +1,33 @@
|
|||||||
|
<div bind="wizardHeader" class="gdata-importing-wizard-header"><div class="grid-layout layout-tightest layout-full"><table><tr>
|
||||||
|
<td width="1%"><button bind="startOverButton" class="button">« Start Over</button></td>
|
||||||
|
<td width="98%">Configure Parsing Options</td>
|
||||||
|
<td style="text-align: right;">Project name</td>
|
||||||
|
<td width="1%"><input class="inline" type="text" size="30" bind="projectNameInput" /></td>
|
||||||
|
<td width="1%"><button bind="createProjectButton" class="button button-primary">Create Project »</button></td>
|
||||||
|
</tr></table></div></div>
|
||||||
|
|
||||||
|
<div bind="dataPanel" class="gdata-importing-parsing-data-panel"></div>
|
||||||
|
|
||||||
|
<div bind="progressPanel" class="gdata-importing-progress-data-panel">
|
||||||
|
<img src="images/large-spinner.gif" /> Updating preview ...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div bind="controlPanel" class="gdata-importing-parsing-control-panel"><div class="grid-layout layout-normal"><table>
|
||||||
|
<tr>
|
||||||
|
<td>Options</td>
|
||||||
|
<td><button class="button" bind="previewButton">Update Preview</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><div class="grid-layout layout-tightest"><table>
|
||||||
|
<tr><td width="1%"><input type="checkbox" bind="skipCheckbox" /></td><td>Discard initial</td>
|
||||||
|
<td><input bind="skipInput" type="text" class="lightweight" size="2" value="0" /> row(s) of data</td></tr>
|
||||||
|
<tr><td width="1%"><input type="checkbox" bind="limitCheckbox" /></td><td>Load at most</td>
|
||||||
|
<td><input bind="limitInput" type="text" class="lightweight" size="2" value="0" /> row(s) of data</td></tr>
|
||||||
|
|
||||||
|
<tr><td width="1%"><input type="checkbox" bind="storeBlankRowsCheckbox" /></td>
|
||||||
|
<td colspan="2">Store blank rows</td></tr>
|
||||||
|
<tr><td width="1%"><input type="checkbox" bind="storeBlankCellsAsNullsCheckbox" /></td>
|
||||||
|
<td colspan="2">Store blank cells as nulls</td></tr>
|
||||||
|
</table></div></td>
|
||||||
|
</tr>
|
||||||
|
</table></div></div>
|
@ -137,17 +137,15 @@ Refine.GDataSourceUI.prototype._renderDocuments = function(o) {
|
|||||||
td = tr.insertCell(tr.cells.length);
|
td = tr.insertCell(tr.cells.length);
|
||||||
$('<span>')
|
$('<span>')
|
||||||
.addClass('gdata-doc-authors')
|
.addClass('gdata-doc-authors')
|
||||||
.text(doc.authors.join(', '))
|
.text((doc.authors) ? doc.authors.join(', ') : '<unknown>')
|
||||||
.appendTo(td);
|
.appendTo(td);
|
||||||
|
|
||||||
td = tr.insertCell(tr.cells.length);
|
td = tr.insertCell(tr.cells.length);
|
||||||
if (doc.updated) {
|
$('<span>')
|
||||||
$('<span>')
|
.addClass('gdata-doc-date')
|
||||||
.addClass('gdata-doc-date')
|
.text((doc.updated) ? formatRelativeDate(doc.updated) : '<unknown>')
|
||||||
.text(formatRelativeDate(doc.updated))
|
.attr('title', (doc.updated) ? doc.updated : '<unknown>')
|
||||||
.attr('title', doc.updated)
|
.appendTo(td);
|
||||||
.appendTo(td);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var docs = o.documents;
|
var docs = o.documents;
|
||||||
|
@ -84,7 +84,6 @@ Refine.GDataImportingController.prototype.getOptions = function() {
|
|||||||
var options = {
|
var options = {
|
||||||
docUrl: this._doc.docSelfLink,
|
docUrl: this._doc.docSelfLink,
|
||||||
docType: this._doc.type,
|
docType: this._doc.type,
|
||||||
sheetUrl: this._sheetUrl
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var parseIntDefault = function(s, def) {
|
var parseIntDefault = function(s, def) {
|
||||||
@ -99,22 +98,25 @@ Refine.GDataImportingController.prototype.getOptions = function() {
|
|||||||
return def;
|
return def;
|
||||||
};
|
};
|
||||||
|
|
||||||
this._parsingPanelElmts.sheetRecordContainer.find('input').each(function() {
|
if (this._doc.type != 'table') {
|
||||||
if (this.checked) {
|
this._parsingPanelElmts.sheetRecordContainer.find('input').each(function() {
|
||||||
options.sheetUrl = this.getAttribute('sheetUrl');
|
if (this.checked) {
|
||||||
}
|
options.sheetUrl = this.getAttribute('sheetUrl');
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (this._parsingPanelElmts.ignoreCheckbox[0].checked) {
|
if (this._parsingPanelElmts.ignoreCheckbox[0].checked) {
|
||||||
options.ignoreLines = parseIntDefault(this._parsingPanelElmts.ignoreInput[0].value, -1);
|
options.ignoreLines = parseIntDefault(this._parsingPanelElmts.ignoreInput[0].value, -1);
|
||||||
} else {
|
} else {
|
||||||
options.ignoreLines = -1;
|
options.ignoreLines = -1;
|
||||||
}
|
}
|
||||||
if (this._parsingPanelElmts.headerLinesCheckbox[0].checked) {
|
if (this._parsingPanelElmts.headerLinesCheckbox[0].checked) {
|
||||||
options.headerLines = parseIntDefault(this._parsingPanelElmts.headerLinesInput[0].value, 0);
|
options.headerLines = parseIntDefault(this._parsingPanelElmts.headerLinesInput[0].value, 0);
|
||||||
} else {
|
} else {
|
||||||
options.headerLines = 0;
|
options.headerLines = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._parsingPanelElmts.skipCheckbox[0].checked) {
|
if (this._parsingPanelElmts.skipCheckbox[0].checked) {
|
||||||
options.skipDataLines = parseIntDefault(this._parsingPanelElmts.skipInput[0].value, 0);
|
options.skipDataLines = parseIntDefault(this._parsingPanelElmts.skipInput[0].value, 0);
|
||||||
} else {
|
} else {
|
||||||
@ -135,7 +137,10 @@ Refine.GDataImportingController.prototype._showParsingPanel = function() {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this._parsingPanel.unbind().empty().html(
|
this._parsingPanel.unbind().empty().html(
|
||||||
DOM.loadHTML("gdata", "scripts/index/gdata-parsing-panel.html"));
|
DOM.loadHTML("gdata",
|
||||||
|
this._doc.type == 'table' ?
|
||||||
|
'scripts/index/gdata-fusion-tables-parsing-panel.html' :
|
||||||
|
'scripts/index/gdata-parsing-panel.html'));
|
||||||
this._parsingPanelElmts = DOM.bind(this._parsingPanel);
|
this._parsingPanelElmts = DOM.bind(this._parsingPanel);
|
||||||
|
|
||||||
if (this._parsingPanelResizer) {
|
if (this._parsingPanelResizer) {
|
||||||
@ -184,30 +189,33 @@ Refine.GDataImportingController.prototype._showParsingPanel = function() {
|
|||||||
|
|
||||||
this._parsingPanelElmts.projectNameInput[0].value = this._doc.title;
|
this._parsingPanelElmts.projectNameInput[0].value = this._doc.title;
|
||||||
|
|
||||||
var sheetTable = this._parsingPanelElmts.sheetRecordContainer[0];
|
if (this._doc.type != 'table') {
|
||||||
$.each(this._options.worksheets, function(i, v) {
|
var sheetTable = this._parsingPanelElmts.sheetRecordContainer[0];
|
||||||
var tr = sheetTable.insertRow(sheetTable.rows.length);
|
$.each(this._options.worksheets, function(i, v) {
|
||||||
var td0 = $(tr.insertCell(0)).attr('width', '1%');
|
var tr = sheetTable.insertRow(sheetTable.rows.length);
|
||||||
var checkbox = $('<input>')
|
var td0 = $(tr.insertCell(0)).attr('width', '1%');
|
||||||
.attr('type', 'radio')
|
var checkbox = $('<input>')
|
||||||
.attr('name', 'gdata-importing-parsing-worksheet')
|
.attr('type', 'radio')
|
||||||
.attr('sheetUrl', this.link)
|
.attr('name', 'gdata-importing-parsing-worksheet')
|
||||||
.appendTo(td0);
|
.attr('sheetUrl', this.link)
|
||||||
if (i === 0) {
|
.appendTo(td0);
|
||||||
checkbox.attr('checked', 'true');
|
if (i === 0) {
|
||||||
}
|
checkbox.attr('checked', 'true');
|
||||||
$(tr.insertCell(1)).text(this.name);
|
}
|
||||||
$(tr.insertCell(2)).text(this.rows + ' rows');
|
$(tr.insertCell(1)).text(this.name);
|
||||||
});
|
$(tr.insertCell(2)).text(this.rows + ' rows');
|
||||||
|
});
|
||||||
|
|
||||||
if (this._options.ignoreLines > 0) {
|
if (this._options.ignoreLines > 0) {
|
||||||
this._parsingPanelElmts.ignoreCheckbox.attr("checked", "checked");
|
this._parsingPanelElmts.ignoreCheckbox.attr("checked", "checked");
|
||||||
this._parsingPanelElmts.ignoreInput[0].value = this._options.ignoreLines.toString();
|
this._parsingPanelElmts.ignoreInput[0].value = this._options.ignoreLines.toString();
|
||||||
}
|
}
|
||||||
if (this._options.headerLines > 0) {
|
if (this._options.headerLines > 0) {
|
||||||
this._parsingPanelElmts.headerLinesCheckbox.attr("checked", "checked");
|
this._parsingPanelElmts.headerLinesCheckbox.attr("checked", "checked");
|
||||||
this._parsingPanelElmts.headerLinesInput[0].value = this._options.headerLines.toString();
|
this._parsingPanelElmts.headerLinesInput[0].value = this._options.headerLines.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._options.limit > 0) {
|
if (this._options.limit > 0) {
|
||||||
this._parsingPanelElmts.limitCheckbox.attr("checked", "checked");
|
this._parsingPanelElmts.limitCheckbox.attr("checked", "checked");
|
||||||
this._parsingPanelElmts.limitInput[0].value = this._options.limit.toString();
|
this._parsingPanelElmts.limitInput[0].value = this._options.limit.toString();
|
||||||
@ -262,7 +270,7 @@ Refine.GDataImportingController.prototype._updatePreview = function() {
|
|||||||
"options" : JSON.stringify(this.getOptions())
|
"options" : JSON.stringify(this.getOptions())
|
||||||
},
|
},
|
||||||
function(result) {
|
function(result) {
|
||||||
if (result.code == "ok") {
|
if (result.status == "ok") {
|
||||||
self._getPreviewData(function(projectData) {
|
self._getPreviewData(function(projectData) {
|
||||||
self._parsingPanelElmts.progressPanel.hide();
|
self._parsingPanelElmts.progressPanel.hide();
|
||||||
self._parsingPanelElmts.dataPanel.show();
|
self._parsingPanelElmts.dataPanel.show();
|
||||||
@ -271,7 +279,7 @@ Refine.GDataImportingController.prototype._updatePreview = function() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self._parsingPanelElmts.progressPanel.hide();
|
self._parsingPanelElmts.progressPanel.hide();
|
||||||
alert('Errors:\n' + result.errors.join('\n'));
|
alert('Errors:\n' + Refine.CreateProjectUI.composeErrorMessage(job));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"json"
|
"json"
|
||||||
@ -345,10 +353,11 @@ Refine.GDataImportingController.prototype._createProject = function() {
|
|||||||
return "projectID" in job.config;
|
return "projectID" in job.config;
|
||||||
},
|
},
|
||||||
function(jobID, job) {
|
function(jobID, job) {
|
||||||
|
window.clearInterval(timerID);
|
||||||
document.location = "project?project=" + job.config.projectID;
|
document.location = "project?project=" + job.config.projectID;
|
||||||
},
|
},
|
||||||
function(job) {
|
function(job) {
|
||||||
alert(job.config.error + '\n' + job.config.errorDetails);
|
alert(Refine.CreateProjectUI.composeErrorMessage(job));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -33,7 +33,7 @@ public class AuthorizeCommand extends Command {
|
|||||||
|
|
||||||
String requestUrl = AuthSubUtil.getRequestUrl(
|
String requestUrl = AuthSubUtil.getRequestUrl(
|
||||||
authorizedUrl.toExternalForm(), // execution continues at authorized on redirect
|
authorizedUrl.toExternalForm(), // execution continues at authorized on redirect
|
||||||
"https://docs.google.com/feeds https://spreadsheets.google.com/feeds",
|
"https://spreadsheets.google.com/feeds https://www.google.com/fusiontables/api/query",
|
||||||
false,
|
false,
|
||||||
true);
|
true);
|
||||||
response.sendRedirect(requestUrl);
|
response.sendRedirect(requestUrl);
|
||||||
|
@ -28,9 +28,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.refine.extension.gdata;
|
package com.google.refine.extension.gdata;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.regex.MatchResult;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.google.gdata.client.GoogleService;
|
||||||
|
import com.google.gdata.client.Service.GDataRequest;
|
||||||
|
import com.google.gdata.client.Service.GDataRequest.RequestType;
|
||||||
import com.google.gdata.client.docs.DocsService;
|
import com.google.gdata.client.docs.DocsService;
|
||||||
import com.google.gdata.client.spreadsheet.FeedURLFactory;
|
import com.google.gdata.client.spreadsheet.FeedURLFactory;
|
||||||
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
||||||
|
import com.google.gdata.util.ContentType;
|
||||||
|
import com.google.gdata.util.ServiceException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Tom Morris <tfmorris@gmail.com>
|
* @author Tom Morris <tfmorris@gmail.com>
|
||||||
@ -60,4 +74,54 @@ abstract public class GDataExtension {
|
|||||||
service.setAuthSubToken(token);
|
service.setAuthSubToken(token);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public GoogleService getFusionTablesGoogleService(String token) {
|
||||||
|
GoogleService service = new GoogleService("fusiontables", SERVICE_APP_NAME);
|
||||||
|
service.setAuthSubToken(token);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
final static private String FUSION_TABLES_SERVICE_URL =
|
||||||
|
"https://www.google.com/fusiontables/api/query";
|
||||||
|
|
||||||
|
final static private Pattern CSV_VALUE_PATTERN =
|
||||||
|
Pattern.compile("([^,\\r\\n\"]*|\"(([^\"]*\"\")*[^\"]*)\")(,|\\r?\\n)");
|
||||||
|
|
||||||
|
static public List<List<String>> runFusionTablesSelect(GoogleService service, String selectQuery)
|
||||||
|
throws IOException, ServiceException {
|
||||||
|
|
||||||
|
URL url = new URL(FUSION_TABLES_SERVICE_URL + "?sql=" +
|
||||||
|
URLEncoder.encode(selectQuery, "UTF-8"));
|
||||||
|
GDataRequest request = service.getRequestFactory().getRequest(
|
||||||
|
RequestType.QUERY, url, ContentType.TEXT_PLAIN);
|
||||||
|
|
||||||
|
request.execute();
|
||||||
|
|
||||||
|
List<List<String>> rows = new ArrayList<List<String>>();
|
||||||
|
List<String> row = null;
|
||||||
|
|
||||||
|
Scanner scanner = new Scanner(request.getResponseStream(), "UTF-8");
|
||||||
|
while (scanner.hasNextLine()) {
|
||||||
|
scanner.findWithinHorizon(CSV_VALUE_PATTERN, 0);
|
||||||
|
MatchResult match = scanner.match();
|
||||||
|
String quotedString = match.group(2);
|
||||||
|
String decoded = quotedString == null ? match.group(1) : quotedString.replaceAll("\"\"", "\"");
|
||||||
|
|
||||||
|
if (row == null) {
|
||||||
|
row = new ArrayList<String>();
|
||||||
|
}
|
||||||
|
row.add(decoded);
|
||||||
|
|
||||||
|
if (!match.group(4).equals(",")) {
|
||||||
|
if (row != null) {
|
||||||
|
rows.add(row);
|
||||||
|
row = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row != null) {
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import com.google.gdata.client.docs.DocsService;
|
import com.google.gdata.client.GoogleService;
|
||||||
import com.google.gdata.client.spreadsheet.CellQuery;
|
import com.google.gdata.client.spreadsheet.CellQuery;
|
||||||
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
||||||
import com.google.gdata.data.spreadsheet.Cell;
|
import com.google.gdata.data.spreadsheet.Cell;
|
||||||
@ -75,19 +75,19 @@ public class GDataImporter {
|
|||||||
SpreadsheetService service = GDataExtension.getSpreadsheetService(token);
|
SpreadsheetService service = GDataExtension.getSpreadsheetService(token);
|
||||||
parse(
|
parse(
|
||||||
service,
|
service,
|
||||||
job.project,
|
project,
|
||||||
job.metadata,
|
metadata,
|
||||||
job,
|
job,
|
||||||
limit,
|
limit,
|
||||||
options,
|
options,
|
||||||
exceptions
|
exceptions
|
||||||
);
|
);
|
||||||
} else if ("table".equals(docType)) {
|
} else if ("table".equals(docType)) {
|
||||||
DocsService service = GDataExtension.getDocsService(token);
|
GoogleService service = GDataExtension.getFusionTablesGoogleService(token);
|
||||||
parse(
|
parse(
|
||||||
service,
|
service,
|
||||||
job.project,
|
project,
|
||||||
job.metadata,
|
metadata,
|
||||||
job,
|
job,
|
||||||
limit,
|
limit,
|
||||||
options,
|
options,
|
||||||
@ -165,21 +165,6 @@ public class GDataImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static public void parse(
|
|
||||||
DocsService service,
|
|
||||||
Project project,
|
|
||||||
ProjectMetadata metadata,
|
|
||||||
final ImportingJob job,
|
|
||||||
int limit,
|
|
||||||
JSONObject options,
|
|
||||||
List<Exception> exceptions) {
|
|
||||||
|
|
||||||
String docUrlString = JSONUtilities.getString(options, "docUrl", null);
|
|
||||||
if (docUrlString != null) {
|
|
||||||
// TODO[dfhuynh]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static private void setProgress(ImportingJob job, String fileSource, int percent) {
|
static private void setProgress(ImportingJob job, String fileSource, int percent) {
|
||||||
JSONObject progress = JSONUtilities.getObject(job.config, "progress");
|
JSONObject progress = JSONUtilities.getObject(job.config, "progress");
|
||||||
if (progress == null) {
|
if (progress == null) {
|
||||||
@ -287,4 +272,210 @@ public class GDataImporter {
|
|||||||
return rowsOfCells;
|
return rowsOfCells;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public void parse(
|
||||||
|
GoogleService service,
|
||||||
|
Project project,
|
||||||
|
ProjectMetadata metadata,
|
||||||
|
final ImportingJob job,
|
||||||
|
int limit,
|
||||||
|
JSONObject options,
|
||||||
|
List<Exception> exceptions) {
|
||||||
|
|
||||||
|
String docUrlString = JSONUtilities.getString(options, "docUrl", null);
|
||||||
|
if (docUrlString == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int equal = docUrlString.lastIndexOf('=');
|
||||||
|
if (equal < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String id = docUrlString.substring(equal + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<FTColumnData> columns = new ArrayList<GDataImporter.FTColumnData>();
|
||||||
|
List<List<String>> rows = GDataExtension.runFusionTablesSelect(service, "DESCRIBE " + id);
|
||||||
|
if (rows.size() > 1) {
|
||||||
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
|
List<String> row = rows.get(i);
|
||||||
|
if (row.size() >= 2) {
|
||||||
|
FTColumnData cd = new FTColumnData();
|
||||||
|
cd.name = row.get(1);
|
||||||
|
cd.type = FTColumnType.STRING;
|
||||||
|
|
||||||
|
if (row.size() > 2) {
|
||||||
|
String type = row.get(2).toLowerCase();
|
||||||
|
if (type.equals("number")) {
|
||||||
|
cd.type = FTColumnType.NUMBER;
|
||||||
|
} else if (type.equals("datetime")) {
|
||||||
|
cd.type = FTColumnType.DATETIME;
|
||||||
|
} else if (type.equals("location")) {
|
||||||
|
cd.type = FTColumnType.LOCATION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columns.add(cd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(job, docUrlString, -1);
|
||||||
|
|
||||||
|
// Force these options for the next call because each fusion table
|
||||||
|
// is strictly structured with a single line of headers.
|
||||||
|
JSONUtilities.safePut(options, "ignoreLines", 0); // number of blank lines at the beginning to ignore
|
||||||
|
JSONUtilities.safePut(options, "headerLines", 1); // number of header lines
|
||||||
|
|
||||||
|
TabularImportingParserBase.readTable(
|
||||||
|
project,
|
||||||
|
metadata,
|
||||||
|
job,
|
||||||
|
new FusionTableBatchRowReader(job, docUrlString, service, id, columns, 100),
|
||||||
|
docUrlString,
|
||||||
|
limit,
|
||||||
|
options,
|
||||||
|
exceptions
|
||||||
|
);
|
||||||
|
setProgress(job, docUrlString, 100);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
exceptions.add(e);
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
exceptions.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static private enum FTColumnType {
|
||||||
|
STRING,
|
||||||
|
NUMBER,
|
||||||
|
DATETIME,
|
||||||
|
LOCATION
|
||||||
|
}
|
||||||
|
|
||||||
|
final static private class FTColumnData {
|
||||||
|
String name;
|
||||||
|
FTColumnType type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static private class FusionTableBatchRowReader implements TableDataReader {
|
||||||
|
final ImportingJob job;
|
||||||
|
final String fileSource;
|
||||||
|
|
||||||
|
final GoogleService service;
|
||||||
|
final List<FTColumnData> columns;
|
||||||
|
final int batchSize;
|
||||||
|
|
||||||
|
final String baseQuery;
|
||||||
|
|
||||||
|
int nextRow = 0; // 0-based
|
||||||
|
int batchRowStart = 0; // 0-based
|
||||||
|
boolean end = false;
|
||||||
|
List<List<Object>> rowsOfCells = null;
|
||||||
|
boolean usedHeaders = false;
|
||||||
|
|
||||||
|
public FusionTableBatchRowReader(ImportingJob job, String fileSource,
|
||||||
|
GoogleService service, String tableId, List<FTColumnData> columns,
|
||||||
|
int batchSize) {
|
||||||
|
this.job = job;
|
||||||
|
this.fileSource = fileSource;
|
||||||
|
this.service = service;
|
||||||
|
this.columns = columns;
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
sb.append("SELECT ");
|
||||||
|
|
||||||
|
boolean first = true;
|
||||||
|
for (FTColumnData cd : columns) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
sb.append(",");
|
||||||
|
}
|
||||||
|
sb.append("'");
|
||||||
|
sb.append(cd.name);
|
||||||
|
sb.append("'");
|
||||||
|
}
|
||||||
|
sb.append(" FROM ");
|
||||||
|
sb.append(tableId);
|
||||||
|
|
||||||
|
baseQuery = sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getNextRowOfCells() throws IOException {
|
||||||
|
if (!usedHeaders) {
|
||||||
|
List<Object> row = new ArrayList<Object>(columns.size());
|
||||||
|
for (FTColumnData cd : columns) {
|
||||||
|
row.add(cd.name);
|
||||||
|
}
|
||||||
|
usedHeaders = true;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowsOfCells == null || (nextRow >= batchRowStart + rowsOfCells.size() && !end)) {
|
||||||
|
int newBatchRowStart = batchRowStart + (rowsOfCells == null ? 0 : rowsOfCells.size());
|
||||||
|
try {
|
||||||
|
rowsOfCells = getRowsOfCells(newBatchRowStart);
|
||||||
|
batchRowStart = newBatchRowStart;
|
||||||
|
|
||||||
|
setProgress(job, fileSource, -1 /* batchRowStart * 100 / totalRows */);
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowsOfCells != null && nextRow - batchRowStart < rowsOfCells.size()) {
|
||||||
|
return rowsOfCells.get(nextRow++ - batchRowStart);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<List<Object>> getRowsOfCells(int startRow) throws IOException, ServiceException {
|
||||||
|
List<List<Object>> rowsOfCells = new ArrayList<List<Object>>(batchSize);
|
||||||
|
|
||||||
|
String query = baseQuery + " OFFSET " + startRow + " LIMIT " + batchSize;
|
||||||
|
|
||||||
|
List<List<String>> rows = GDataExtension.runFusionTablesSelect(service, query);
|
||||||
|
if (rows.size() > 1) {
|
||||||
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
|
List<String> row = rows.get(i);
|
||||||
|
List<Object> rowOfCells = new ArrayList<Object>(row.size());
|
||||||
|
for (int j = 0; j < row.size() && j < columns.size(); j++) {
|
||||||
|
String text = row.get(j);
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
rowOfCells.add(null);
|
||||||
|
} else {
|
||||||
|
FTColumnData cd = columns.get(j);
|
||||||
|
if (cd.type == FTColumnType.NUMBER) {
|
||||||
|
try {
|
||||||
|
rowOfCells.add(Long.parseLong(text));
|
||||||
|
continue;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
double d = Double.parseDouble(text);
|
||||||
|
if (!Double.isInfinite(d) && !Double.isNaN(d)) {
|
||||||
|
rowOfCells.add(d);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rowOfCells.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowsOfCells.add(rowOfCells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end = rows.size() < batchSize + 1;
|
||||||
|
return rowsOfCells;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -49,17 +49,13 @@ import org.json.JSONException;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.json.JSONWriter;
|
import org.json.JSONWriter;
|
||||||
|
|
||||||
import com.google.gdata.client.Query;
|
import com.google.gdata.client.GoogleService;
|
||||||
import com.google.gdata.client.docs.DocsService;
|
import com.google.gdata.client.docs.DocsService;
|
||||||
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
import com.google.gdata.client.spreadsheet.SpreadsheetService;
|
||||||
import com.google.gdata.data.Category;
|
|
||||||
import com.google.gdata.data.DateTime;
|
import com.google.gdata.data.DateTime;
|
||||||
import com.google.gdata.data.Person;
|
import com.google.gdata.data.Person;
|
||||||
import com.google.gdata.data.docs.DocumentListEntry;
|
|
||||||
import com.google.gdata.data.docs.DocumentListFeed;
|
|
||||||
import com.google.gdata.data.spreadsheet.SpreadsheetEntry;
|
import com.google.gdata.data.spreadsheet.SpreadsheetEntry;
|
||||||
import com.google.gdata.data.spreadsheet.SpreadsheetFeed;
|
import com.google.gdata.data.spreadsheet.SpreadsheetFeed;
|
||||||
import com.google.gdata.data.spreadsheet.TableEntry;
|
|
||||||
import com.google.gdata.data.spreadsheet.WorksheetEntry;
|
import com.google.gdata.data.spreadsheet.WorksheetEntry;
|
||||||
import com.google.gdata.util.ServiceException;
|
import com.google.gdata.util.ServiceException;
|
||||||
|
|
||||||
@ -67,6 +63,7 @@ import com.google.refine.ProjectManager;
|
|||||||
import com.google.refine.ProjectMetadata;
|
import com.google.refine.ProjectMetadata;
|
||||||
import com.google.refine.RefineServlet;
|
import com.google.refine.RefineServlet;
|
||||||
import com.google.refine.commands.HttpUtilities;
|
import com.google.refine.commands.HttpUtilities;
|
||||||
|
import com.google.refine.importing.DefaultImportingController;
|
||||||
import com.google.refine.importing.ImportingController;
|
import com.google.refine.importing.ImportingController;
|
||||||
import com.google.refine.importing.ImportingJob;
|
import com.google.refine.importing.ImportingJob;
|
||||||
import com.google.refine.importing.ImportingManager;
|
import com.google.refine.importing.ImportingManager;
|
||||||
@ -125,11 +122,10 @@ public class GDataImportingController implements ImportingController {
|
|||||||
writer.array();
|
writer.array();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DocsService service = GDataExtension.getDocsService(token);
|
listSpreadsheets(GDataExtension.getDocsService(token), writer);
|
||||||
listSpreadsheets(service, writer);
|
listFusionTables(GDataExtension.getFusionTablesGoogleService(token), writer);
|
||||||
listDocumentsOfType(service, writer, "http://schemas.google.com/docs/2007#table");
|
|
||||||
} catch (ServiceException e) {
|
} catch (ServiceException e) {
|
||||||
// TODO: just ignore?
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.endArray();
|
writer.endArray();
|
||||||
@ -169,36 +165,27 @@ public class GDataImportingController implements ImportingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void listDocumentsOfType(DocsService service, JSONWriter writer, String type)
|
private void listFusionTables(GoogleService service, JSONWriter writer)
|
||||||
throws IOException, ServiceException, JSONException {
|
throws IOException, ServiceException, JSONException {
|
||||||
URL feedUrl = new URL("https://docs.google.com/feeds/default/private/full");
|
|
||||||
|
|
||||||
Query query = new Query(feedUrl);
|
List<List<String>> rows = GDataExtension.runFusionTablesSelect(service, "SHOW TABLES");
|
||||||
query.addCategoryFilter(
|
if (rows.size() > 1) { // excluding headers
|
||||||
new Query.CategoryFilter(
|
for (int i = 1; i < rows.size(); i++) {
|
||||||
new Category("http://schemas.google.com/g/2005#kind", type)));
|
List<String> row = rows.get(i);
|
||||||
|
if (row.size() >= 2) {
|
||||||
DocumentListFeed feed = service.query(query, DocumentListFeed.class);
|
String id = row.get(0);
|
||||||
for (DocumentListEntry entry : feed.getEntries()) {
|
String name = row.get(1);
|
||||||
writer.object();
|
String link = "https://www.google.com/fusiontables/DataSource?dsrcid=" + id;
|
||||||
writer.key("docId"); writer.value(entry.getId());
|
|
||||||
writer.key("docLink"); writer.value(entry.getHtmlLink().getHref());
|
writer.object();
|
||||||
writer.key("docSelfLink"); writer.value(entry.getSelfLink().getHref());
|
writer.key("docId"); writer.value(id);
|
||||||
writer.key("title"); writer.value(entry.getTitle().getPlainText());
|
writer.key("docLink"); writer.value(link);
|
||||||
writer.key("type"); writer.value(entry.getType());
|
writer.key("docSelfLink"); writer.value(link);
|
||||||
|
writer.key("title"); writer.value(name);
|
||||||
DateTime updated = entry.getUpdated();
|
writer.key("type"); writer.value("table");
|
||||||
if (updated != null) {
|
writer.endObject();
|
||||||
writer.key("updated"); writer.value(updated.toStringRfc822());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.key("authors"); writer.array();
|
|
||||||
for (Person person : entry.getAuthors()) {
|
|
||||||
writer.value(person.getName());
|
|
||||||
}
|
|
||||||
writer.endArray();
|
|
||||||
|
|
||||||
writer.endObject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,16 +208,17 @@ public class GDataImportingController implements ImportingController {
|
|||||||
JSONUtilities.safePut(result, "status", "ok");
|
JSONUtilities.safePut(result, "status", "ok");
|
||||||
JSONUtilities.safePut(result, "options", options);
|
JSONUtilities.safePut(result, "options", options);
|
||||||
|
|
||||||
JSONUtilities.safePut(options, "ignoreLines", -1); // number of blank lines at the beginning to ignore
|
|
||||||
JSONUtilities.safePut(options, "headerLines", 1); // number of header lines
|
|
||||||
JSONUtilities.safePut(options, "skipDataLines", 0); // number of initial data lines to skip
|
JSONUtilities.safePut(options, "skipDataLines", 0); // number of initial data lines to skip
|
||||||
JSONUtilities.safePut(options, "storeBlankRows", true);
|
JSONUtilities.safePut(options, "storeBlankRows", true);
|
||||||
JSONUtilities.safePut(options, "storeBlankCellsAsNulls", true);
|
JSONUtilities.safePut(options, "storeBlankCellsAsNulls", true);
|
||||||
|
|
||||||
JSONArray worksheets = new JSONArray();
|
|
||||||
JSONUtilities.safePut(options, "worksheets", worksheets);
|
|
||||||
|
|
||||||
if ("spreadsheet".equals(type)) {
|
if ("spreadsheet".equals(type)) {
|
||||||
|
JSONUtilities.safePut(options, "ignoreLines", -1); // number of blank lines at the beginning to ignore
|
||||||
|
JSONUtilities.safePut(options, "headerLines", 1); // number of header lines
|
||||||
|
|
||||||
|
JSONArray worksheets = new JSONArray();
|
||||||
|
JSONUtilities.safePut(options, "worksheets", worksheets);
|
||||||
|
|
||||||
SpreadsheetService spreadsheetService = GDataExtension.getSpreadsheetService(token);
|
SpreadsheetService spreadsheetService = GDataExtension.getSpreadsheetService(token);
|
||||||
SpreadsheetEntry spreadsheetEntry = spreadsheetService.getEntry(url, SpreadsheetEntry.class);
|
SpreadsheetEntry spreadsheetEntry = spreadsheetService.getEntry(url, SpreadsheetEntry.class);
|
||||||
for (WorksheetEntry worksheetEntry : spreadsheetEntry.getWorksheets()) {
|
for (WorksheetEntry worksheetEntry : spreadsheetEntry.getWorksheets()) {
|
||||||
@ -242,15 +230,7 @@ public class GDataImportingController implements ImportingController {
|
|||||||
JSONUtilities.append(worksheets, worksheetO);
|
JSONUtilities.append(worksheets, worksheetO);
|
||||||
}
|
}
|
||||||
} else if ("table".equals(type)) {
|
} else if ("table".equals(type)) {
|
||||||
DocsService docsService = GDataExtension.getDocsService(token);
|
// No metadata for a fusion table.
|
||||||
TableEntry tableEntry = docsService.getEntry(url, TableEntry.class);
|
|
||||||
|
|
||||||
JSONObject worksheetO = new JSONObject();
|
|
||||||
JSONUtilities.safePut(worksheetO, "name", tableEntry.getTitle().getPlainText());
|
|
||||||
JSONUtilities.safePut(worksheetO, "rows", -1);
|
|
||||||
JSONUtilities.safePut(worksheetO, "link", tableEntry.getSelfLink().getHref());
|
|
||||||
|
|
||||||
JSONUtilities.append(worksheets, worksheetO);
|
|
||||||
}
|
}
|
||||||
/* TODO: else */
|
/* TODO: else */
|
||||||
|
|
||||||
@ -307,15 +287,13 @@ public class GDataImportingController implements ImportingController {
|
|||||||
if (exceptions.size() == 0) {
|
if (exceptions.size() == 0) {
|
||||||
job.project.update(); // update all internal models, indexes, caches, etc.
|
job.project.update(); // update all internal models, indexes, caches, etc.
|
||||||
|
|
||||||
writer.key("code"); writer.value("ok");
|
writer.key("status"); writer.value("ok");
|
||||||
} else {
|
} else {
|
||||||
writer.key("code"); writer.value("error");
|
writer.key("status"); writer.value("error");
|
||||||
|
|
||||||
writer.key("errors");
|
writer.key("errors");
|
||||||
writer.array();
|
writer.array();
|
||||||
for (Exception e : exceptions) {
|
DefaultImportingController.writeErrors(writer, exceptions);
|
||||||
writer.value(e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
writer.endArray();
|
writer.endArray();
|
||||||
}
|
}
|
||||||
writer.endObject();
|
writer.endObject();
|
||||||
@ -374,12 +352,18 @@ public class GDataImportingController implements ImportingController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!job.canceled) {
|
if (!job.canceled) {
|
||||||
project.update(); // update all internal models, indexes, caches, etc.
|
if (exceptions.size() > 0) {
|
||||||
|
JSONUtilities.safePut(job.config, "errors",
|
||||||
ProjectManager.singleton.registerProject(project, pm);
|
DefaultImportingController.convertErrorsToJsonArray(exceptions));
|
||||||
|
JSONUtilities.safePut(job.config, "state", "error");
|
||||||
JSONUtilities.safePut(job.config, "projectID", project.id);
|
} else {
|
||||||
JSONUtilities.safePut(job.config, "state", "created-project");
|
project.update(); // update all internal models, indexes, caches, etc.
|
||||||
|
|
||||||
|
ProjectManager.singleton.registerProject(project, pm);
|
||||||
|
|
||||||
|
JSONUtilities.safePut(job.config, "state", "created-project");
|
||||||
|
JSONUtilities.safePut(job.config, "projectID", project.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
@ -34,6 +34,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
package com.google.refine.importing;
|
package com.google.refine.importing;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -173,7 +175,28 @@ public class DefaultImportingController implements ImportingController {
|
|||||||
|
|
||||||
ImportingUtilities.previewParse(job, format, optionObj, exceptions);
|
ImportingUtilities.previewParse(job, format, optionObj, exceptions);
|
||||||
|
|
||||||
HttpUtilities.respond(response, "ok", "done");
|
Writer w = response.getWriter();
|
||||||
|
JSONWriter writer = new JSONWriter(w);
|
||||||
|
try {
|
||||||
|
writer.object();
|
||||||
|
if (exceptions.size() == 0) {
|
||||||
|
job.project.update(); // update all internal models, indexes, caches, etc.
|
||||||
|
|
||||||
|
writer.key("status"); writer.value("ok");
|
||||||
|
} else {
|
||||||
|
writer.key("status"); writer.value("error");
|
||||||
|
writer.key("errors");
|
||||||
|
writer.array();
|
||||||
|
writeErrors(writer, exceptions);
|
||||||
|
writer.endArray();
|
||||||
|
}
|
||||||
|
writer.endObject();
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new ServletException(e);
|
||||||
|
} finally {
|
||||||
|
w.flush();
|
||||||
|
w.close();
|
||||||
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
throw new ServletException(e);
|
throw new ServletException(e);
|
||||||
}
|
}
|
||||||
@ -252,4 +275,33 @@ public class DefaultImportingController implements ImportingController {
|
|||||||
w.close();
|
w.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public void writeErrors(JSONWriter writer, List<Exception> exceptions) throws JSONException {
|
||||||
|
for (Exception e : exceptions) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(sw));
|
||||||
|
|
||||||
|
writer.object();
|
||||||
|
writer.key("message");
|
||||||
|
writer.value(e.getLocalizedMessage());
|
||||||
|
writer.key("stack");
|
||||||
|
writer.value(sw.toString());
|
||||||
|
writer.endObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static public JSONArray convertErrorsToJsonArray(List<Exception> exceptions) {
|
||||||
|
JSONArray a = new JSONArray();
|
||||||
|
for (Exception e : exceptions) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(sw));
|
||||||
|
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
JSONUtilities.safePut(o, "message", e.getLocalizedMessage());
|
||||||
|
JSONUtilities.safePut(o, "stack", sw.toString());
|
||||||
|
JSONUtilities.append(a, o);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -911,12 +911,18 @@ public class ImportingUtilities {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!job.canceled) {
|
if (!job.canceled) {
|
||||||
project.update(); // update all internal models, indexes, caches, etc.
|
if (exceptions.size() == 0) {
|
||||||
|
project.update(); // update all internal models, indexes, caches, etc.
|
||||||
ProjectManager.singleton.registerProject(project, pm);
|
|
||||||
|
ProjectManager.singleton.registerProject(project, pm);
|
||||||
JSONUtilities.safePut(job.config, "projectID", project.id);
|
|
||||||
JSONUtilities.safePut(job.config, "state", "created-project");
|
JSONUtilities.safePut(job.config, "projectID", project.id);
|
||||||
|
JSONUtilities.safePut(job.config, "state", "created-project");
|
||||||
|
} else {
|
||||||
|
JSONUtilities.safePut(job.config, "state", "error");
|
||||||
|
JSONUtilities.safePut(job.config, "errors",
|
||||||
|
DefaultImportingController.convertErrorsToJsonArray(exceptions));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,3 +245,9 @@ Refine.CreateProjectUI.prototype.showImportJobError = function(message, stack) {
|
|||||||
self.showSourceSelectionPanel();
|
self.showSourceSelectionPanel();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Refine.CreateProjectUI.composeErrorMessage = function(job) {
|
||||||
|
var messages = [];
|
||||||
|
$.each(job.config.errors, function() { messages.push(this.message) });
|
||||||
|
return messages.join('\n');
|
||||||
|
};
|
||||||
|
@ -280,7 +280,7 @@ Refine.DefaultImportingController.prototype._createProject = function() {
|
|||||||
document.location = "project?project=" + job.config.projectID;
|
document.location = "project?project=" + job.config.projectID;
|
||||||
},
|
},
|
||||||
function(job) {
|
function(job) {
|
||||||
alert(job.config.error + '\n' + job.config.errorDetails);
|
alert('Errors:\n' + Refine.CreateProjectUI.composeErrorMessage(job));
|
||||||
self._onImportJobReady();
|
self._onImportJobReady();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -48,7 +48,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
.default-importing-file-selection-file-panel > table {
|
.default-importing-file-selection-file-panel > table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.default-importing-file-selection-file-panel > table > tbody > tr > td,
|
.default-importing-file-selection-file-panel > table > tbody > tr > td,
|
||||||
.default-importing-file-selection-file-panel > table > tbody > tr > th {
|
.default-importing-file-selection-file-panel > table > tbody > tr > th {
|
||||||
padding: @padding_tight @padding_normal;
|
padding: @padding_tight @padding_normal;
|
||||||
|
Loading…
Reference in New Issue
Block a user